feat: add structural equality class because java object equality stinks

This commit is contained in:
Orion Kindel 2023-04-21 14:00:33 -05:00
parent 0d2d7505f8
commit f718d9b482
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
22 changed files with 523 additions and 159 deletions

View File

@ -15,13 +15,13 @@ public class ClientObserveStream {
public ClientObserveStream(Client client, Message message) { public ClientObserveStream(Client client, Message message) {
this.state = State.OPEN; this.state = State.OPEN;
this.client = client; this.client = client;
this.message = message.modify().option(Observe.REGISTER).build(); this.message = message.buildCopy().option(Observe.REGISTER).build();
this.buffered = Optional.of(client.send(this.message)); this.buffered = Optional.of(client.send(this.message));
} }
public CompletableFuture<Void> close() { public CompletableFuture<Void> close() {
return this.client.send( return this.client.send(
this.message.modify().option(Observe.DEREGISTER).unsetId().build() this.message.buildCopy().option(Observe.DEREGISTER).unsetId().build()
) )
.thenAccept(m -> { .thenAccept(m -> {
this.state = State.CLOSED; this.state = State.CLOSED;
@ -29,7 +29,7 @@ public class ClientObserveStream {
} }
public CompletableFuture<Message> next() { public CompletableFuture<Message> next() {
if (this.state == State.CLOSED) { if (State.eq.test(State.CLOSED, this.state)) {
throw new RuntimeException( throw new RuntimeException(
"ClientObserveStream.next() invoked after .close()" "ClientObserveStream.next() invoked after .close()"
); );
@ -47,6 +47,8 @@ public class ClientObserveStream {
public static final class State { public static final class State {
public static final Eq<State> eq = Eq.int_.contramap((State s) -> s.state);
public static final State OPEN = new State(0); public static final State OPEN = new State(0);
public static final State CLOSED = new State(1); public static final State CLOSED = new State(1);
@ -56,14 +58,10 @@ public class ClientObserveStream {
this.state = state; this.state = state;
} }
public boolean equals(State other) {
return this.state == other.state;
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case State s -> this.equals(s); case State s -> State.eq.test(this, s);
default -> false; default -> false;
}; };
} }

View File

@ -0,0 +1,87 @@
package dev.toad;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
public final class Eq<T> {
public static final Eq<Short> short_ = new Eq<>((a, b) -> a == b);
public static final Eq<Integer> int_ = new Eq<>((a, b) -> a == b);
public static final Eq<Long> long_ = new Eq<>((a, b) -> a == b);
public static final Eq<String> string = new Eq<>((a, b) ->
(a != null && b != null && a.equals(b)) || (a == null && b == null)
);
public static final Eq<byte[]> byteArray = new Eq<>((a, b) ->
Arrays.equals(a, b)
);
public static final Eq<java.net.InetSocketAddress> socketAddress = new Eq<>(
(a, b) ->
a.equals(b)
);
public static <T> Eq<T> all(List<Eq<T>> eqs) {
return new Eq<>((a, b) -> eqs.stream().allMatch(eq -> eq.test(a, b)));
}
public static <T> Eq<Optional<T>> optional(Eq<T> eq) {
return new Eq<>((a, b) -> {
if (!a.isEmpty() && !b.isEmpty()) {
return eq.test(a.get(), b.get());
} else {
return a.isEmpty() && b.isEmpty();
}
});
}
public static <K, V> Eq<Map<K, V>> map(Eq<V> eq) {
return new Eq<>((a, b) -> {
if (a.entrySet().size() != b.entrySet().size()) {
return false;
}
for (var ent : a.entrySet()) {
var val = b.get(ent.getKey());
if (val == null || !eq.test(val, ent.getValue())) {
return false;
}
}
return true;
});
}
public static <T> Eq<List<T>> list(Eq<T> eq) {
return new Eq<>((a, b) -> {
if (a.size() != b.size()) {
return false;
}
for (int i = 0; i < a.size(); i++) {
if (!eq.test(a.get(i), b.get(i))) {
return false;
}
}
return true;
});
}
final BiFunction<T, T, Boolean> eq;
public Eq(BiFunction<T, T, Boolean> eq) {
this.eq = eq;
}
public boolean test(T a, T b) {
return this.eq.apply(a, b);
}
public <U> Eq<U> contramap(Function<U, T> from) {
return new Eq<>((a, b) -> this.test(from.apply(a), from.apply(b)));
}
}

View File

@ -110,9 +110,7 @@ public final class Server {
CompletableFuture.completedFuture(Optional.empty()); CompletableFuture.completedFuture(Optional.empty());
public static final Function<Message, Result> notFound = m -> { public static final Function<Message, Result> notFound = m -> {
return Middleware.respond( return Middleware.respond(m.buildResponse().code(Code.NOT_FOUND).build());
m.modify().unsetId().code(Code.NOT_FOUND).build()
);
}; };
public static final BiFunction<Message, Throwable, Result> debugExceptionHandler = public static final BiFunction<Message, Throwable, Result> debugExceptionHandler =
@ -126,8 +124,7 @@ public final class Server {
); );
var rep = m var rep = m
.modify() .buildResponse()
.unsetId()
.code(Code.INTERNAL_SERVER_ERROR) .code(Code.INTERNAL_SERVER_ERROR)
.payload(Payload.text(e.toString())) .payload(Payload.text(e.toString()))
.build(); .build();
@ -144,7 +141,7 @@ public final class Server {
String.format("while handling %s", m.toDebugString()), String.format("while handling %s", m.toDebugString()),
e e
); );
var rep = m.modify().unsetId().code(Code.INTERNAL_SERVER_ERROR).build(); var rep = m.buildResponse().code(Code.INTERNAL_SERVER_ERROR).build();
return Middleware.respond(rep); return Middleware.respond(rep);
}; };
@ -358,8 +355,11 @@ public final class Server {
public Builder put(String path, Function<Message, Middleware.Result> f) { public Builder put(String path, Function<Message, Middleware.Result> f) {
return this.when( return this.when(
m -> m ->
m.code().equals(Code.PUT) && Code.eq.test(m.code(), Code.PUT) &&
m.getPath().map(p -> p.matches(path)).orElse(path == ""), m
.getPath()
.map(p -> p.matches(path))
.orElse(path == null || path.isEmpty()),
f f
); );
} }
@ -367,8 +367,11 @@ public final class Server {
public Builder post(String path, Function<Message, Middleware.Result> f) { public Builder post(String path, Function<Message, Middleware.Result> f) {
return this.when( return this.when(
m -> m ->
m.code().equals(Code.POST) && Code.eq.test(m.code(), Code.POST) &&
m.getPath().map(p -> p.matches(path)).orElse(path == ""), m
.getPath()
.map(p -> p.matches(path))
.orElse(path == null || path.isEmpty()),
f f
); );
} }
@ -376,8 +379,11 @@ public final class Server {
public Builder delete(String path, Function<Message, Middleware.Result> f) { public Builder delete(String path, Function<Message, Middleware.Result> f) {
return this.when( return this.when(
m -> m ->
m.code().equals(Code.DELETE) && Code.eq.test(m.code(), Code.DELETE) &&
m.getPath().map(p -> p.matches(path)).orElse(path == ""), m
.getPath()
.map(p -> p.matches(path))
.orElse(path == null || path.isEmpty()),
f f
); );
} }
@ -385,7 +391,7 @@ public final class Server {
public Builder get(String path, Function<Message, Middleware.Result> f) { public Builder get(String path, Function<Message, Middleware.Result> f) {
return this.when( return this.when(
m -> m ->
m.code().equals(Code.GET) && Code.eq.test(m.code(), Code.GET) &&
m m
.getPath() .getPath()
.map(p -> p.matches(path)) .map(p -> p.matches(path))

View File

@ -208,7 +208,8 @@ public final class Toad implements AutoCloseable {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case Config o -> o.concurrency == this.concurrency && o.msg == this.msg; case Config o -> o.concurrency.equals(this.concurrency) &&
o.msg.equals(this.msg);
default -> false; default -> false;
}; };
} }
@ -258,11 +259,11 @@ public final class Toad implements AutoCloseable {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case Msg o -> o.tokenSeed == this.tokenSeed && case Msg o -> o.tokenSeed.equals(this.tokenSeed) &&
o.probingRateBytesPerSecond == this.probingRateBytesPerSecond && o.probingRateBytesPerSecond.equals(this.probingRateBytesPerSecond) &&
o.multicastResponseLeisure == this.multicastResponseLeisure && o.multicastResponseLeisure.equals(this.multicastResponseLeisure) &&
o.con == this.con && o.con.equals(this.con) &&
o.non == this.non; o.non.equals(this.non);
default -> false; default -> false;
}; };
} }
@ -362,9 +363,11 @@ public final class Toad implements AutoCloseable {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case Con o -> this.ackedRetryStrategy == o.ackedRetryStrategy && case Con o -> this.ackedRetryStrategy.equals(
this.unackedRetryStrategy == o.unackedRetryStrategy && o.ackedRetryStrategy
this.maxAttempts == o.maxAttempts; ) &&
this.unackedRetryStrategy.equals(o.unackedRetryStrategy) &&
this.maxAttempts.equals(o.maxAttempts);
default -> false; default -> false;
}; };
} }
@ -435,8 +438,8 @@ public final class Toad implements AutoCloseable {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case Non o -> this.retryStrategy == o.retryStrategy && case Non o -> this.retryStrategy.equals(o.retryStrategy) &&
this.maxAttempts == o.maxAttempts; this.maxAttempts.equals(o.maxAttempts);
default -> false; default -> false;
}; };
} }

View File

@ -1,13 +1,22 @@
package dev.toad.msg; package dev.toad.msg;
import dev.toad.Debug; import dev.toad.Debug;
import dev.toad.Eq;
import dev.toad.ffi.u8; import dev.toad.ffi.u8;
import java.util.List;
public final class Code implements Debug { public final class Code implements Debug {
final u8 clazz; final u8 clazz;
final u8 detail; final u8 detail;
public static Eq<Code> eq = Eq.all(
List.of(
Eq.short_.contramap(Code::codeClass),
Eq.short_.contramap(Code::codeDetail)
)
);
public static final Code EMPTY = new Code(0, 0); public static final Code EMPTY = new Code(0, 0);
public static final Code GET = new Code(0, 1); public static final Code GET = new Code(0, 1);
@ -112,17 +121,10 @@ public final class Code implements Debug {
return this.toString(); return this.toString();
} }
public boolean equals(Code other) {
return (
this.codeClass() == other.codeClass() &&
this.codeDetail() == other.codeDetail()
);
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case Code c -> c.equals(this); case Code c -> Code.eq.test(c, this);
default -> false; default -> false;
}; };
} }

View File

@ -1,12 +1,15 @@
package dev.toad.msg; package dev.toad.msg;
import dev.toad.Debug; import dev.toad.Debug;
import dev.toad.Eq;
import dev.toad.ffi.u16; import dev.toad.ffi.u16;
public final class Id implements Debug { public final class Id implements Debug {
public static native Id defaultId(); public static native Id defaultId();
public static final Eq<Id> eq = Eq.int_.contramap(Id::toInt);
final u16 id; final u16 id;
public Id(int id) { public Id(int id) {
@ -17,6 +20,14 @@ public final class Id implements Debug {
return this.id.intValue(); return this.id.intValue();
} }
@Override
public boolean equals(Object other) {
return switch (other) {
case Id i -> Id.eq.test(this, i);
default -> false;
};
}
@Override @Override
public String toDebugString() { public String toDebugString() {
return String.format("Id(%d)", this.toInt()); return String.format("Id(%d)", this.toInt());

View File

@ -1,6 +1,7 @@
package dev.toad.msg; package dev.toad.msg;
import dev.toad.Debug; import dev.toad.Debug;
import dev.toad.Eq;
import dev.toad.msg.option.Accept; import dev.toad.msg.option.Accept;
import dev.toad.msg.option.ContentFormat; import dev.toad.msg.option.ContentFormat;
import dev.toad.msg.option.Host; import dev.toad.msg.option.Host;
@ -35,8 +36,12 @@ public interface Message extends Debug {
public byte[] toBytes(); public byte[] toBytes();
public default dev.toad.msg.build.Message modify() { public default dev.toad.msg.build.Message buildCopy() {
return dev.toad.msg.build.Message.from(this); return dev.toad.msg.build.Message.copyOf(this);
}
public default dev.toad.msg.build.MessageNeeds.Code buildResponse() {
return dev.toad.msg.build.Message.respondTo(this);
} }
public default Optional<Option> getOption(long number) { public default Optional<Option> getOption(long number) {
@ -91,17 +96,24 @@ public interface Message extends Debug {
} }
} }
public default boolean equals(Message o) { public static Eq<Message> eq() {
return ( return Eq.all(
this.addr().equals(o.addr()) && List.of(
this.options().equals(o.options()) && Eq.optional(Eq.socketAddress).contramap(m -> m.addr()),
this.id().equals(o.id()) && Eq.list(Option.eq()).contramap(m -> m.options()),
this.token().equals(o.token()) && Code.eq.contramap(m -> m.code()),
this.type().equals(o.type()) && Id.eq.contramap(m -> m.id()),
this.payload().equals(o.payload()) Token.eq.contramap(m -> m.token()),
Type.eq.contramap(m -> m.type()),
Payload.eq.contramap(m -> m.payload())
)
); );
} }
public default boolean equals(Message m) {
return Message.eq().test(this, m);
}
@Override @Override
public default String toDebugString() { public default String toDebugString() {
return ( return (

View File

@ -1,6 +1,7 @@
package dev.toad.msg; package dev.toad.msg;
import dev.toad.Debug; import dev.toad.Debug;
import dev.toad.Eq;
import dev.toad.msg.option.*; import dev.toad.msg.option.*;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -10,8 +11,17 @@ public interface Option extends Debug {
public List<OptionValue> values(); public List<OptionValue> values();
public static Eq<Option> eq() {
return Eq.all(
List.of(
Eq.long_.contramap(o -> o.number()),
Eq.list(OptionValue.eq()).contramap(Option::values)
)
);
}
public default boolean equals(Option o) { public default boolean equals(Option o) {
return this.number() == o.number() && this.values().equals(o.values()); return Option.eq().test(this, o);
} }
@Override @Override

View File

@ -1,6 +1,7 @@
package dev.toad.msg; package dev.toad.msg;
import dev.toad.Debug; import dev.toad.Debug;
import dev.toad.Eq;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -11,6 +12,10 @@ public interface OptionValue extends Debug {
public dev.toad.msg.owned.OptionValue toOwned(); public dev.toad.msg.owned.OptionValue toOwned();
public static Eq<OptionValue> eq() {
return Eq.byteArray.contramap(OptionValue::asBytes);
}
public default boolean equals(OptionValue o) { public default boolean equals(OptionValue o) {
return this.asBytes().equals(o.asBytes()); return this.asBytes().equals(o.asBytes());
} }

View File

@ -1,9 +1,12 @@
package dev.toad.msg; package dev.toad.msg;
import dev.toad.Debug; import dev.toad.Debug;
import dev.toad.Eq;
import dev.toad.msg.option.ContentFormat; import dev.toad.msg.option.ContentFormat;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public final class Payload implements Debug { public final class Payload implements Debug {
@ -11,6 +14,19 @@ public final class Payload implements Debug {
final byte[] bytes; final byte[] bytes;
final Optional<ContentFormat> contentFormat; final Optional<ContentFormat> contentFormat;
public static final Eq<Payload> eq = Eq.all(
List.of(
Eq.optional(ContentFormat.eq).contramap(Payload::contentFormat),
Eq.byteArray.contramap(Payload::bytes)
)
);
public static boolean equals(Payload a, Payload b) {
return (
Arrays.equals(a.bytes, b.bytes) && a.contentFormat.equals(b.contentFormat)
);
}
public Payload() { public Payload() {
this.contentFormat = Optional.empty(); this.contentFormat = Optional.empty();
this.bytes = new byte[] {}; this.bytes = new byte[] {};
@ -63,6 +79,14 @@ public final class Payload implements Debug {
return new Payload(ContentFormat.OCTET_STREAM, bytes); return new Payload(ContentFormat.OCTET_STREAM, bytes);
} }
@Override
public boolean equals(Object other) {
return switch (other) {
case Payload p -> Payload.eq.test(this, p);
default -> false;
};
}
@Override @Override
public String toDebugString() { public String toDebugString() {
if (this.contentFormat.map(ContentFormat::isUtf8Text).orElse(false)) { if (this.contentFormat.map(ContentFormat::isUtf8Text).orElse(false)) {

View File

@ -1,12 +1,16 @@
package dev.toad.msg; package dev.toad.msg;
import dev.toad.Debug; import dev.toad.Debug;
import dev.toad.Eq;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
public final class Token implements Debug { public final class Token implements Debug {
public static native Token defaultToken(); public static native Token defaultToken();
public static final Eq<Token> eq = Eq.byteArray.contramap(Token::toBytes);
final byte[] bytes; final byte[] bytes;
public Token(byte[] bytes) { public Token(byte[] bytes) {
@ -30,7 +34,7 @@ public final class Token implements Debug {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case Token t -> t.bytes.equals(this.bytes); case Token t -> Token.eq.test(this, t);
default -> false; default -> false;
}; };
} }

View File

@ -1,6 +1,7 @@
package dev.toad.msg; package dev.toad.msg;
import dev.toad.Debug; import dev.toad.Debug;
import dev.toad.Eq;
public enum Type implements Debug { public enum Type implements Debug {
CON(1), CON(1),
@ -8,6 +9,8 @@ public enum Type implements Debug {
ACK(3), ACK(3),
RESET(4); RESET(4);
public static final Eq<Type> eq = new Eq<>((a, b) -> a == b);
private Type(int val) {} private Type(int val) {}
public String toString() { public String toString() {

View File

@ -35,21 +35,39 @@ public final class Message
Message() {} Message() {}
public static Message from(dev.toad.msg.Message other) { public static MessageNeeds.Code respondTo(dev.toad.msg.Message other) {
return Message.respondTo(other, false);
}
public static MessageNeeds.Code respondTo(
dev.toad.msg.Message other,
boolean conResponseToNonRequest
) {
// prettier-ignore
Type type = Type.eq.test(other.type(), Type.CON) ? Type.ACK
: conResponseToNonRequest ? Type.CON
: Type.NON;
return Message.copyOf(other).unsetId().type(type);
}
public static Message copyOf(dev.toad.msg.Message other) {
var builder = new Message(); var builder = new Message();
Function<dev.toad.msg.Option, Long> key = o -> o.number(); Function<dev.toad.msg.Option, Long> key;
Function<dev.toad.msg.Option, ArrayList<dev.toad.msg.owned.OptionValue>> value = Function<dev.toad.msg.Option, ArrayList<dev.toad.msg.owned.OptionValue>> value;
BinaryOperator<ArrayList<dev.toad.msg.owned.OptionValue>> merge;
key = o -> o.number();
value =
o -> o ->
o o
.values() .values()
.stream() .stream()
.map(v -> v.toOwned()) .map(dev.toad.msg.OptionValue::toOwned)
.collect(Collectors.toCollection(ArrayList::new)); .collect(Collectors.toCollection(ArrayList::new));
BinaryOperator<ArrayList<dev.toad.msg.owned.OptionValue>> merge = ( merge =
a, (a, b) -> {
b
) -> {
a.addAll(b); a.addAll(b);
return a; return a;
}; };
@ -58,7 +76,7 @@ public final class Message
other other
.options() .options()
.stream() .stream()
.collect(Collectors.toMap(key, value, merge, () -> new HashMap<>())); .collect(Collectors.toMap(key, value, merge, HashMap::new));
builder.id = Optional.of(other.id()); builder.id = Optional.of(other.id());
builder.code = Optional.of(other.code()); builder.code = Optional.of(other.code());
builder.token = Optional.of(other.token()); builder.token = Optional.of(other.token());
@ -73,15 +91,17 @@ public final class Message
return new Message(); return new Message();
} }
public MessageNeeds.Type uri(String uriStr) public Message uri(String uriStr)
throws URISyntaxException, UnknownHostException { throws URISyntaxException, UnknownHostException {
var uri = new URI(uriStr); var uri = new URI(uriStr);
var addr = InetAddress.getByName(uri.getHost()); var addr = InetAddress.getByName(uri.getHost());
var port = uri.getPort() > 0 var secure = uri.getScheme() != null && uri.getScheme().equals("coaps");
? uri.getPort()
: uri.getScheme() != null && uri.getScheme().equals("coaps") // prettier-ignore
? 5684 var port = uri.getPort() > 0 ? uri.getPort()
: secure ? 5684
: 5683; : 5683;
this.addr = Optional.of(new InetSocketAddress(addr, port)); this.addr = Optional.of(new InetSocketAddress(addr, port));
this.option(new Host(addr.getHostAddress())); this.option(new Host(addr.getHostAddress()));
@ -97,12 +117,12 @@ public final class Message
return this; return this;
} }
public MessageNeeds.Type addr(InetSocketAddress addr) { public Message addr(InetSocketAddress addr) {
this.addr = Optional.of(addr); this.addr = Optional.of(addr);
return this; return this;
} }
public MessageNeeds.Code type(Type type) { public Message type(Type type) {
this.type = Optional.of(type); this.type = Optional.of(type);
return this; return this;
} }

View File

@ -1,5 +1,6 @@
package dev.toad.msg.option; package dev.toad.msg.option;
import dev.toad.Eq;
import dev.toad.ffi.u16; import dev.toad.ffi.u16;
import dev.toad.msg.Option; import dev.toad.msg.Option;
import dev.toad.msg.OptionValue; import dev.toad.msg.OptionValue;
@ -11,6 +12,7 @@ import java.util.stream.Collectors;
public final class Accept extends ContentFormat implements Option { public final class Accept extends ContentFormat implements Option {
public static final long number = 17; public static final long number = 17;
public static final Eq<Accept> eq = Eq.int_.contramap(Accept::value);
public static final Accept TEXT = new Accept(ContentFormat.TEXT); public static final Accept TEXT = new Accept(ContentFormat.TEXT);
public static final Accept LINK_FORMAT = new Accept( public static final Accept LINK_FORMAT = new Accept(
@ -46,7 +48,7 @@ public final class Accept extends ContentFormat implements Option {
o o
.values() .values()
.stream() .stream()
.map(v -> v.asString()) .map(OptionValue::asString)
.collect(Collectors.toList()) .collect(Collectors.toList())
) )
); );

View File

@ -1,5 +1,6 @@
package dev.toad.msg.option; package dev.toad.msg.option;
import dev.toad.Eq;
import dev.toad.ffi.u16; import dev.toad.ffi.u16;
import dev.toad.msg.Option; import dev.toad.msg.Option;
import dev.toad.msg.OptionValue; import dev.toad.msg.OptionValue;
@ -13,6 +14,9 @@ public sealed class ContentFormat implements Option permits Accept {
protected u16 value; protected u16 value;
public static final Eq<ContentFormat> eq = Eq.int_.contramap(
ContentFormat::value
);
public static final long number = 12; public static final long number = 12;
public ContentFormat(Option o) { public ContentFormat(Option o) {
@ -76,48 +80,37 @@ public sealed class ContentFormat implements Option permits Accept {
public boolean isUtf8Text() { public boolean isUtf8Text() {
return ( return (
this.value() == ContentFormat.TEXT.value() || this.equals(ContentFormat.TEXT) ||
this.value() == ContentFormat.CSS.value() || this.equals(ContentFormat.CSS) ||
this.value() == ContentFormat.JSON.value() || this.equals(ContentFormat.JSON) ||
this.value() == ContentFormat.XML.value() || this.equals(ContentFormat.XML) ||
this.value() == ContentFormat.JAVASCRIPT.value() || this.equals(ContentFormat.JAVASCRIPT) ||
this.value() == ContentFormat.LINK_FORMAT.value() || this.equals(ContentFormat.LINK_FORMAT) ||
this.value() == ContentFormat.IMAGE_SVG.value() this.equals(ContentFormat.IMAGE_SVG)
); );
} }
public String toMimeType() { public String toMimeType() {
// https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats // https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats
return this.value() == ContentFormat.TEXT.value() // prettier-ignore
? "text/plain; charset=utf-8" return this.equals(ContentFormat.TEXT) ? "text/plain; charset=utf-8"
: this.value() == ContentFormat.CSS.value() : this.equals(ContentFormat.CSS) ? "text/css"
? "text/css" : this.equals(ContentFormat.JSON) ? "application/json"
: this.value() == ContentFormat.JSON.value() : this.equals(ContentFormat.XML) ? "application/xml"
? "application/json" : this.equals(ContentFormat.EXI) ? "application/exi"
: this.value() == ContentFormat.XML.value() : this.equals(ContentFormat.CBOR) ? "application/cbor"
? "application/xml" : this.equals(ContentFormat.JAVASCRIPT) ? "application/javascript"
: this.value() == ContentFormat.EXI.value() : this.equals(ContentFormat.OCTET_STREAM) ? "application/octet-stream"
? "application/exi" : this.equals(ContentFormat.LINK_FORMAT) ? "application/link-format"
: this.value() == ContentFormat.CBOR.value() : this.equals(ContentFormat.IMAGE_GIF) ? "image/gif"
? "application/cbor" : this.equals(ContentFormat.IMAGE_JPG) ? "image/jpeg"
: this.value() == ContentFormat.JAVASCRIPT.value() : this.equals(ContentFormat.IMAGE_PNG) ? "image/png"
? "application/javascript" : this.equals(ContentFormat.IMAGE_SVG) ? "image/svg+xml"
: this.value() == ContentFormat.OCTET_STREAM.value() : String.format("ContentFormat(%d)", this.value());
? "application/octet-stream" }
: this.value() == ContentFormat.LINK_FORMAT.value()
? "application/link-format" public boolean equals(ContentFormat cf) {
: this.value() == ContentFormat.IMAGE_GIF.value() return ContentFormat.eq.test(this, cf);
? "image/gif"
: this.value() == ContentFormat.IMAGE_JPG.value()
? "image/jpeg"
: this.value() == ContentFormat.IMAGE_PNG.value()
? "image/png"
: this.value() == ContentFormat.IMAGE_SVG.value()
? "image/svg+xml"
: String.format(
"ContentFormat(%d)",
this.value()
);
} }
@Override @Override
@ -128,10 +121,6 @@ public sealed class ContentFormat implements Option permits Accept {
}; };
} }
public boolean equals(ContentFormat other) {
return this.value() == other.value();
}
public String toString() { public String toString() {
return String.format("ContentFormat(%d)", this.value()); return String.format("ContentFormat(%d)", this.value());
} }

View File

@ -1,5 +1,6 @@
package dev.toad.msg.option; package dev.toad.msg.option;
import dev.toad.Eq;
import dev.toad.msg.Option; import dev.toad.msg.Option;
import dev.toad.msg.OptionValue; import dev.toad.msg.OptionValue;
import java.util.ArrayList; import java.util.ArrayList;
@ -12,6 +13,7 @@ public final class Host implements Option {
final String host; final String host;
public static final long number = 3; public static final long number = 3;
public static final Eq<Host> eq = Eq.string.contramap(Host::toString);
public Host(Option o) { public Host(Option o) {
if (o.number() != Host.number) { if (o.number() != Host.number) {
@ -58,7 +60,7 @@ public final class Host implements Option {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case Host h -> this.toString().equals(h.toString()); case Host h -> Host.eq.test(this, h);
default -> false; default -> false;
}; };
} }

View File

@ -1,5 +1,6 @@
package dev.toad.msg.option; package dev.toad.msg.option;
import dev.toad.Eq;
import dev.toad.msg.Option; import dev.toad.msg.Option;
import dev.toad.msg.OptionValue; import dev.toad.msg.OptionValue;
import java.util.ArrayList; import java.util.ArrayList;
@ -12,6 +13,9 @@ public final class Path implements Option {
final ArrayList<String> segments; final ArrayList<String> segments;
public static final long number = 11; public static final long number = 11;
public static final Eq<Path> eq = Eq
.list(Eq.string)
.contramap(Path::segments);
public Path(Option o) { public Path(Option o) {
if (o.number() != Path.number) { if (o.number() != Path.number) {
@ -40,10 +44,6 @@ public final class Path implements Option {
} }
} }
public boolean equals(Path other) {
return this.segments == other.segments;
}
public List<String> segments() { public List<String> segments() {
return this.segments; return this.segments;
} }
@ -73,4 +73,12 @@ public final class Path implements Option {
.map(s -> new dev.toad.msg.owned.OptionValue(s)) .map(s -> new dev.toad.msg.owned.OptionValue(s))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public boolean equals(Object other) {
return switch (other) {
case Path p -> Path.eq.test(this, p);
default -> false;
};
}
} }

View File

@ -1,5 +1,6 @@
package dev.toad.msg.option; package dev.toad.msg.option;
import dev.toad.Eq;
import dev.toad.msg.Option; import dev.toad.msg.Option;
import dev.toad.msg.OptionValue; import dev.toad.msg.OptionValue;
import java.util.ArrayList; import java.util.ArrayList;
@ -17,6 +18,9 @@ public final class Query implements Option {
final ArrayList<String> query; final ArrayList<String> query;
public static final long number = 15; public static final long number = 15;
public static final Eq<Query> eq = Eq
.<String, List<Value>>map(Eq.list(Value.eq))
.contramap(Query::toMap);
public Query(Option o) { public Query(Option o) {
if (o.number() != Query.number) { if (o.number() != Query.number) {
@ -90,12 +94,24 @@ public final class Query implements Option {
return String.format("Uri-Query: %s", this.toString()); return String.format("Uri-Query: %s", this.toString());
} }
@Override
public boolean equals(Object other) {
return switch (other) {
case Query q -> Query.eq.test(this, q);
default -> false;
};
}
public static final class Value { public static final class Value {
public static final Eq<Value> eq = Eq
.optional(Eq.string)
.contramap(Value::value);
final Optional<String> val; final Optional<String> val;
public Value(String val) { public Value(String val) {
if (val == null || val == "") { if (val == null || val.isEmpty()) {
this.val = Optional.empty(); this.val = Optional.empty();
} else { } else {
this.val = Optional.of(val); this.val = Optional.of(val);
@ -119,14 +135,10 @@ public final class Query implements Option {
} }
} }
public boolean equals(Value other) {
return this.toString().equals(other.toString());
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return switch (other) { return switch (other) {
case Value v -> this.equals(v); case Value v -> Value.eq.test(this, v);
default -> false; default -> false;
}; };
} }

View File

@ -14,36 +14,29 @@ import java.util.concurrent.TimeUnit
import java.net.InetAddress import java.net.InetAddress
class E2E extends munit.FunSuite { class E2E extends munit.FunSuite {
test("minimal client and server") { test("little baby server") {
Toad.loadNativeLib() Toad.loadNativeLib()
val serverThread = Thread((() => { val server = Toad.builder
Toad.builder
.port(10102) .port(10102)
.logLevel(Level.INFO) .logLevel(Level.INFO)
.server .server
.post( .post("exit", _msg => Server.Middleware.exit)
"exit",
_msg => {
Server.Middleware.exit
}
)
.get( .get(
"hello", "hello",
msg => { msg => {
val name = msg.payload.toString val rep = msg.buildResponse
val rep = msg.modify.unsetId
.`type`(Type.NON)
.code(Code.OK_CONTENT) .code(Code.OK_CONTENT)
.payload(Payload.text(s"Hello, $name!")) .`type`(Type.NON)
.payload(Payload.text(s"Hello, ${msg.payload.toString}!"))
.build .build
Server.Middleware.respond(rep) Server.Middleware.respond(rep)
} }
) )
.build .build;
.run
}): java.lang.Runnable)
val serverThread = Thread((() => server.run()): java.lang.Runnable)
serverThread.start() serverThread.start()
val req = Message.builder val req = Message.builder
@ -68,6 +61,6 @@ class E2E extends munit.FunSuite {
client.sendNoResponse(exit) client.sendNoResponse(exit)
} }
serverThread.join serverThread.join()
} }
} }

126
src/test/scala/Eq.scala Normal file
View File

@ -0,0 +1,126 @@
import scala.jdk.CollectionConverters.*
import dev.toad.*
import dev.toad.msg.*
import dev.toad.msg.option.*
class Eq extends munit.FunSuite {
test("byteArray") {
assert(dev.toad.Eq.byteArray.test(Array[Byte](), Array[Byte]()))
assert(dev.toad.Eq.byteArray.test(Array[Byte](1), Array[Byte](1)))
assert(dev.toad.Eq.byteArray.test(Array[Byte](1, 2), Array[Byte](1, 2)))
assert(!dev.toad.Eq.byteArray.test(Array[Byte](1), Array[Byte]()))
}
test("int, short, long") {
assert(dev.toad.Eq.int_.test(0, 0))
assert(dev.toad.Eq.int_.test(1, 1))
assert(dev.toad.Eq.long_.test(1, 1))
assert(dev.toad.Eq.short_.test(1.shortValue, 1.shortValue))
assert(!dev.toad.Eq.int_.test(1, 0))
assert(!dev.toad.Eq.long_.test(1, 0))
assert(!dev.toad.Eq.short_.test(1.shortValue, 0.shortValue))
}
test("String") {
assert(dev.toad.Eq.string.test(null, null))
assert(dev.toad.Eq.string.test("a", "a"))
assert(!dev.toad.Eq.string.test("", null))
assert(!dev.toad.Eq.string.test("", "a"))
}
test("contramap") {
class Foo(val bar: String)
val a = Foo("baz")
val b = Foo("bingo")
val eq = dev.toad.Eq.string.contramap((foo: Foo) => foo.bar)
assert(eq.test(a, a))
assert(!eq.test(a, b))
}
test("contramap + all") {
class Foo(val bar: String, val baz: String)
val a = Foo("a", "b")
val b = Foo("b", "c")
val c = Foo("a", "c")
val eq = dev.toad.Eq.all(
Seq(
dev.toad.Eq.string.contramap((f: Foo) => f.bar),
dev.toad.Eq.string.contramap((f: Foo) => f.baz)
).asJava
)
assert(eq.test(a, a))
assert(eq.test(b, b))
assert(!eq.test(a, b))
assert(!eq.test(a, c))
}
test("Map") {
val map = dev.toad.Eq.map[String, String](dev.toad.Eq.string)
assert(map.test(Map().asJava, Map().asJava))
assert(
map.test(
Map("a" -> "a", "foo" -> "bar").asJava,
Map("a" -> "a", "foo" -> "bar").asJava
)
)
assert(
!map.test(
Map("foo" -> "bar").asJava,
Map("a" -> "a", "foo" -> "bar").asJava
)
)
assert(!map.test(Map().asJava, Map("a" -> "a", "foo" -> "bar").asJava))
}
test("List") {
val list = dev.toad.Eq.list(dev.toad.Eq.string)
assert(list.test(Seq("a", "b").asJava, Seq("a", "b").asJava))
assert(!list.test(Seq("b", "b").asJava, Seq("a", "b").asJava))
}
test("Id") {
assert(Id.eq.test(Id(1), Id(1)))
assert(!Id.eq.test(Id(0), Id(1)))
}
test("Token") {
assert(
Token.eq.test(Token(Array[Byte](1, 2, 3)), Token(Array[Byte](1, 2, 3)))
)
assert(
!Token.eq.test(Token(Array[Byte](2, 2, 3)), Token(Array[Byte](1, 2, 3)))
)
}
test("Code") {
assert(Code.eq.test(Code.OK_CONTENT, Code.OK_CONTENT))
assert(!Code.eq.test(Code.GET, Code.OK_CONTENT))
}
test("Message") {
val a = Message.builder
.uri("coap://localhost:1234/a/b/c")
.`type`(Type.CON)
.code(Code.GET)
.id(Id(0))
.token(Token(Array[Byte](1, 2, 3)))
.payload(Payload.text("foo"))
.build
assert(Message.eq.test(a, a))
assert(!Message.eq.test(a, a.buildCopy.`type`(Type.NON).build))
assert(!Message.eq.test(a, a.buildCopy.option(Path("b/c")).build))
assert(!Message.eq.test(a, a.buildCopy.code(Code.PUT).build))
assert(!Message.eq.test(a, a.buildCopy.id(Id(1)).build))
assert(!Message.eq.test(a, a.buildCopy.token(Token(Array[Byte](2))).build))
assert(
!Message.eq.test(a, a.buildCopy.uri("coap://google.com:1234/a/b/c").build)
)
assert(!Message.eq.test(a, a.buildCopy.payload(Payload.json("foo")).build))
}
}

View File

@ -2,10 +2,10 @@ import sys.process._
class Glue extends munit.FunSuite { class Glue extends munit.FunSuite {
test("cargo test") { test("cargo test") {
// Seq( Seq(
// "sh", "sh",
// "-c", "-c",
// "cd glue; RUST_BACKTRACE=full cargo test --quiet --features e2e" "cd glue; RUST_BACKTRACE=full cargo test --quiet --features e2e"
// ).!! ).!!
} }
} }

View File

@ -15,7 +15,54 @@ class MessageBuilder extends munit.FunSuite {
.payload(Payload.json("[\"fart\"]")) .payload(Payload.json("[\"fart\"]"))
.build .build
assertEquals(msg, dev.toad.msg.build.Message.from(msg).build) assertEquals(msg, dev.toad.msg.build.Message.copyOf(msg).build)
}
test("respondTo(Message) unsets Id") {
val msg = dev.toad.msg.build.Message
.builder()
.uri("coap://localhost")
.`type`(Type.NON)
.code(Code.GET)
.payload(Payload.json("[\"fart\"]"))
.build
val rep =
dev.toad.msg.build.Message.respondTo(msg).code(Code.OK_CONTENT).build
assertEquals(rep.id, Id(0))
}
test("respondTo(Message) copies token") {
val msg = dev.toad.msg.build.Message
.builder()
.uri("coap://localhost")
.`type`(Type.NON)
.code(Code.GET)
.token(Token(Array[Byte](4, 5, 6)))
.payload(Payload.json("[\"fart\"]"))
.build
val rep =
dev.toad.msg.build.Message.respondTo(msg).code(Code.OK_CONTENT).build
assertEquals(rep.token, Token(Array[Byte](4, 5, 6)))
}
test("respondTo(Message) sets type to ACK on CON response") {
val msg = dev.toad.msg.build.Message
.builder()
.uri("coap://localhost")
.`type`(Type.CON)
.code(Code.GET)
.token(Token(Array[Byte](4, 5, 6)))
.payload(Payload.json("[\"fart\"]"))
.build
val rep =
dev.toad.msg.build.Message.respondTo(msg).code(Code.OK_CONTENT).build
assertEquals(rep.`type`(), Type.ACK)
} }
test("payload(Payload) sets content format to ContentFormat.JSON") { test("payload(Payload) sets content format to ContentFormat.JSON") {