feat: add structural equality class because java object equality stinks
This commit is contained in:
parent
0d2d7505f8
commit
f718d9b482
@ -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;
|
||||
};
|
||||
}
|
||||
|
87
src/main/java/dev.toad/Eq.java
Normal file
87
src/main/java/dev.toad/Eq.java
Normal 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)));
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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())
|
||||
)
|
||||
);
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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
126
src/test/scala/Eq.scala
Normal 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))
|
||||
}
|
||||
}
|
@ -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"
|
||||
).!!
|
||||
}
|
||||
}
|
||||
|
@ -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") {
|
||||
|
Loading…
Reference in New Issue
Block a user