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) {
this.state = State.OPEN;
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));
}
public CompletableFuture<Void> close() {
return this.client.send(
this.message.modify().option(Observe.DEREGISTER).unsetId().build()
this.message.buildCopy().option(Observe.DEREGISTER).unsetId().build()
)
.thenAccept(m -> {
this.state = State.CLOSED;
@ -29,7 +29,7 @@ public class ClientObserveStream {
}
public CompletableFuture<Message> next() {
if (this.state == State.CLOSED) {
if (State.eq.test(State.CLOSED, this.state)) {
throw new RuntimeException(
"ClientObserveStream.next() invoked after .close()"
);
@ -47,6 +47,8 @@ public class ClientObserveStream {
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 CLOSED = new State(1);
@ -56,14 +58,10 @@ public class ClientObserveStream {
this.state = state;
}
public boolean equals(State other) {
return this.state == other.state;
}
@Override
public boolean equals(Object other) {
return switch (other) {
case State s -> this.equals(s);
case State s -> State.eq.test(this, s);
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());
public static final Function<Message, Result> notFound = m -> {
return Middleware.respond(
m.modify().unsetId().code(Code.NOT_FOUND).build()
);
return Middleware.respond(m.buildResponse().code(Code.NOT_FOUND).build());
};
public static final BiFunction<Message, Throwable, Result> debugExceptionHandler =
@ -126,8 +124,7 @@ public final class Server {
);
var rep = m
.modify()
.unsetId()
.buildResponse()
.code(Code.INTERNAL_SERVER_ERROR)
.payload(Payload.text(e.toString()))
.build();
@ -144,7 +141,7 @@ public final class Server {
String.format("while handling %s", m.toDebugString()),
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);
};
@ -358,8 +355,11 @@ public final class Server {
public Builder put(String path, Function<Message, Middleware.Result> f) {
return this.when(
m ->
m.code().equals(Code.PUT) &&
m.getPath().map(p -> p.matches(path)).orElse(path == ""),
Code.eq.test(m.code(), Code.PUT) &&
m
.getPath()
.map(p -> p.matches(path))
.orElse(path == null || path.isEmpty()),
f
);
}
@ -367,8 +367,11 @@ public final class Server {
public Builder post(String path, Function<Message, Middleware.Result> f) {
return this.when(
m ->
m.code().equals(Code.POST) &&
m.getPath().map(p -> p.matches(path)).orElse(path == ""),
Code.eq.test(m.code(), Code.POST) &&
m
.getPath()
.map(p -> p.matches(path))
.orElse(path == null || path.isEmpty()),
f
);
}
@ -376,8 +379,11 @@ public final class Server {
public Builder delete(String path, Function<Message, Middleware.Result> f) {
return this.when(
m ->
m.code().equals(Code.DELETE) &&
m.getPath().map(p -> p.matches(path)).orElse(path == ""),
Code.eq.test(m.code(), Code.DELETE) &&
m
.getPath()
.map(p -> p.matches(path))
.orElse(path == null || path.isEmpty()),
f
);
}
@ -385,7 +391,7 @@ public final class Server {
public Builder get(String path, Function<Message, Middleware.Result> f) {
return this.when(
m ->
m.code().equals(Code.GET) &&
Code.eq.test(m.code(), Code.GET) &&
m
.getPath()
.map(p -> p.matches(path))

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package dev.toad.msg;
import dev.toad.Debug;
import dev.toad.Eq;
import dev.toad.msg.option.*;
import java.util.List;
import java.util.stream.Collectors;
@ -10,8 +11,17 @@ public interface Option extends Debug {
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) {
return this.number() == o.number() && this.values().equals(o.values());
return Option.eq().test(this, o);
}
@Override

View File

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

View File

@ -1,9 +1,12 @@
package dev.toad.msg;
import dev.toad.Debug;
import dev.toad.Eq;
import dev.toad.msg.option.ContentFormat;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public final class Payload implements Debug {
@ -11,6 +14,19 @@ public final class Payload implements Debug {
final byte[] bytes;
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() {
this.contentFormat = Optional.empty();
this.bytes = new byte[] {};
@ -63,6 +79,14 @@ public final class Payload implements Debug {
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
public String toDebugString() {
if (this.contentFormat.map(ContentFormat::isUtf8Text).orElse(false)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package dev.toad.msg.option;
import dev.toad.Eq;
import dev.toad.msg.Option;
import dev.toad.msg.OptionValue;
import java.util.ArrayList;
@ -12,6 +13,9 @@ public final class Path implements Option {
final ArrayList<String> segments;
public static final long number = 11;
public static final Eq<Path> eq = Eq
.list(Eq.string)
.contramap(Path::segments);
public Path(Option o) {
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() {
return this.segments;
}
@ -73,4 +73,12 @@ public final class Path implements Option {
.map(s -> new dev.toad.msg.owned.OptionValue(s))
.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;
import dev.toad.Eq;
import dev.toad.msg.Option;
import dev.toad.msg.OptionValue;
import java.util.ArrayList;
@ -17,6 +18,9 @@ public final class Query implements Option {
final ArrayList<String> query;
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) {
if (o.number() != Query.number) {
@ -90,12 +94,24 @@ public final class Query implements Option {
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 Eq<Value> eq = Eq
.optional(Eq.string)
.contramap(Value::value);
final Optional<String> val;
public Value(String val) {
if (val == null || val == "") {
if (val == null || val.isEmpty()) {
this.val = Optional.empty();
} else {
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
public boolean equals(Object other) {
return switch (other) {
case Value v -> this.equals(v);
case Value v -> Value.eq.test(this, v);
default -> false;
};
}

View File

@ -14,36 +14,29 @@ import java.util.concurrent.TimeUnit
import java.net.InetAddress
class E2E extends munit.FunSuite {
test("minimal client and server") {
test("little baby server") {
Toad.loadNativeLib()
val serverThread = Thread((() => {
Toad.builder
.port(10102)
.logLevel(Level.INFO)
.server
.post(
"exit",
_msg => {
Server.Middleware.exit
}
)
.get(
"hello",
msg => {
val name = msg.payload.toString
val rep = msg.modify.unsetId
.`type`(Type.NON)
.code(Code.OK_CONTENT)
.payload(Payload.text(s"Hello, $name!"))
.build
Server.Middleware.respond(rep)
}
)
.build
.run
}): java.lang.Runnable)
val server = Toad.builder
.port(10102)
.logLevel(Level.INFO)
.server
.post("exit", _msg => Server.Middleware.exit)
.get(
"hello",
msg => {
val rep = msg.buildResponse
.code(Code.OK_CONTENT)
.`type`(Type.NON)
.payload(Payload.text(s"Hello, ${msg.payload.toString}!"))
.build
Server.Middleware.respond(rep)
}
)
.build;
val serverThread = Thread((() => server.run()): java.lang.Runnable)
serverThread.start()
val req = Message.builder
@ -68,6 +61,6 @@ class E2E extends munit.FunSuite {
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 {
test("cargo test") {
// Seq(
// "sh",
// "-c",
// "cd glue; RUST_BACKTRACE=full cargo test --quiet --features e2e"
// ).!!
Seq(
"sh",
"-c",
"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\"]"))
.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") {