feat: payload, content format changes
This commit is contained in:
parent
30e4a235f3
commit
c4bd8c6273
@ -1,6 +1,10 @@
|
||||
package dev.toad;
|
||||
|
||||
import dev.toad.msg.Code;
|
||||
import dev.toad.msg.Type;
|
||||
import dev.toad.msg.Message;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@ -12,6 +16,14 @@ public final class Client implements AutoCloseable {
|
||||
this.toad = toad;
|
||||
}
|
||||
|
||||
public CompletableFuture<Message> get(String uri) throws URISyntaxException, UnknownHostException {
|
||||
return this.get(Type.CON, uri);
|
||||
}
|
||||
|
||||
public CompletableFuture<Message> get(Type ty, String uri) throws URISyntaxException, UnknownHostException {
|
||||
return this.send(Message.builder().uri(uri).type(ty).code(Code.GET).build());
|
||||
}
|
||||
|
||||
public CompletableFuture<Message> send(Message message) {
|
||||
if (message.addr().isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
|
@ -15,4 +15,16 @@ public final class u16 {
|
||||
public int intValue() {
|
||||
return this.l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return switch (other) {
|
||||
case u16 o -> this.equals(o);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean equals(u16 other) {
|
||||
return this.intValue() == other.intValue();
|
||||
}
|
||||
}
|
||||
|
@ -15,4 +15,16 @@ public final class u32 {
|
||||
public long longValue() {
|
||||
return this.l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return switch (other) {
|
||||
case u32 o -> this.equals(o);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean equals(u32 other) {
|
||||
return this.longValue() == other.longValue();
|
||||
}
|
||||
}
|
||||
|
@ -23,4 +23,16 @@ public final class u64 {
|
||||
public BigInteger bigintValue() {
|
||||
return this.l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return switch (other) {
|
||||
case u64 o -> this.equals(o);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean equals(u64 other) {
|
||||
return this.bigintValue() == other.bigintValue();
|
||||
}
|
||||
}
|
||||
|
@ -13,4 +13,16 @@ public final class u8 {
|
||||
public short shortValue() {
|
||||
return this.l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return switch (other) {
|
||||
case u8 o -> this.equals(o);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean equals(u8 other) {
|
||||
return this.shortValue() == other.shortValue();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package dev.toad.msg;
|
||||
|
||||
import dev.toad.msg.option.Accept;
|
||||
import dev.toad.msg.option.ContentFormat;
|
||||
import dev.toad.msg.option.Path;
|
||||
import dev.toad.msg.option.Host;
|
||||
import dev.toad.msg.option.Query;
|
||||
@ -8,6 +10,10 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface Message {
|
||||
public static dev.toad.msg.build.MessageNeeds.Destination builder() {
|
||||
return dev.toad.msg.build.Message.builder();
|
||||
}
|
||||
|
||||
public Optional<InetSocketAddress> addr();
|
||||
|
||||
public Id id();
|
||||
@ -20,10 +26,26 @@ public interface Message {
|
||||
|
||||
public List<Option> options();
|
||||
|
||||
public byte[] payloadBytes();
|
||||
|
||||
public String payloadString();
|
||||
|
||||
public dev.toad.msg.owned.Message toOwned();
|
||||
|
||||
public byte[] toBytes();
|
||||
|
||||
default public Optional<Option> getOption(long number) {
|
||||
return this.options().stream().filter(o -> o.number() == number).findAny();
|
||||
}
|
||||
|
||||
default public Optional<Accept> getAccept() {
|
||||
return this.getOption(Accept.number).map(o -> new Accept(o));
|
||||
}
|
||||
|
||||
default public Optional<ContentFormat> getContentFormat() {
|
||||
return this.getOption(ContentFormat.number).map(o -> new ContentFormat(o));
|
||||
}
|
||||
|
||||
default public Optional<Path> getPath() {
|
||||
return this.getOption(Path.number).map(o -> new Path(o));
|
||||
}
|
||||
@ -35,12 +57,4 @@ public interface Message {
|
||||
default public Optional<Query> getQuery() {
|
||||
return this.getOption(Query.number).map(o -> new Query(o));
|
||||
}
|
||||
|
||||
public byte[] payloadBytes();
|
||||
|
||||
public String payloadString();
|
||||
|
||||
public dev.toad.msg.owned.Message toOwned();
|
||||
|
||||
public byte[] toBytes();
|
||||
}
|
||||
|
57
src/main/java/dev.toad/msg/Payload.java
Normal file
57
src/main/java/dev.toad/msg/Payload.java
Normal file
@ -0,0 +1,57 @@
|
||||
package dev.toad.msg;
|
||||
|
||||
import java.util.Optional;
|
||||
import dev.toad.msg.option.ContentFormat;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class Payload {
|
||||
final byte[] bytes;
|
||||
final Optional<ContentFormat> contentFormat;
|
||||
|
||||
public Payload(byte[] bytes) {
|
||||
this.contentFormat = Optional.empty();
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
public Payload(ContentFormat contentFormat, byte[] bytes) {
|
||||
this.contentFormat = Optional.of(contentFormat);
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new String(this.bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public Optional<ContentFormat> contentFormat() {
|
||||
return this.contentFormat;
|
||||
}
|
||||
|
||||
public byte[] bytes() {
|
||||
return this.bytes;
|
||||
}
|
||||
|
||||
public static Payload utf8Encoded(ContentFormat contentFormat, String text) {
|
||||
return new Payload(contentFormat, text.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static Payload text(String text) {
|
||||
return Payload.utf8Encoded(ContentFormat.TEXT, text);
|
||||
}
|
||||
|
||||
public static Payload json(String json) {
|
||||
return Payload.utf8Encoded(ContentFormat.JSON, json);
|
||||
}
|
||||
|
||||
public static Payload linkFormat(String linkFormat) {
|
||||
return Payload.utf8Encoded(ContentFormat.LINK_FORMAT, linkFormat);
|
||||
}
|
||||
|
||||
public static Payload exi(byte[] exi) {
|
||||
return new Payload(ContentFormat.EXI, exi);
|
||||
}
|
||||
|
||||
public static Payload octetStream(byte[] bytes) {
|
||||
return new Payload(ContentFormat.OCTET_STREAM, bytes);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package dev.toad.msg.build;
|
||||
|
||||
import dev.toad.msg.Payload;
|
||||
import dev.toad.msg.Code;
|
||||
import dev.toad.msg.Id;
|
||||
import dev.toad.msg.Token;
|
||||
@ -29,7 +30,7 @@ public final class Message
|
||||
Optional<InetSocketAddress> addr = Optional.empty();
|
||||
Optional<Code> code = Optional.empty();
|
||||
Optional<Type> type = Optional.empty();
|
||||
byte[] payload = new byte[] {};
|
||||
Optional<Payload> payload = Optional.empty();
|
||||
|
||||
Message() {}
|
||||
|
||||
@ -74,6 +75,11 @@ public final class Message
|
||||
return this;
|
||||
}
|
||||
|
||||
public Message payload(Payload payload) {
|
||||
this.payload = Optional.of(payload);
|
||||
return this.option(payload.contentFormat().get());
|
||||
}
|
||||
|
||||
public Message option(
|
||||
Function<OptionNeeds.Number, dev.toad.msg.owned.Option> fun
|
||||
) {
|
||||
@ -95,16 +101,6 @@ public final class Message
|
||||
return this;
|
||||
}
|
||||
|
||||
public Message payload(String payload) {
|
||||
this.payload = payload.getBytes();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Message payload(byte[] payload) {
|
||||
this.payload = payload;
|
||||
return this;
|
||||
}
|
||||
|
||||
public dev.toad.msg.Message build() {
|
||||
return new dev.toad.msg.owned.Message(
|
||||
this.addr,
|
||||
@ -112,7 +108,7 @@ public final class Message
|
||||
this.code.get(),
|
||||
this.id.orElse(Id.defaultId()),
|
||||
this.token.orElse(Token.defaultToken()),
|
||||
this.payload,
|
||||
this.payload.map(p -> p.bytes()).orElse(new byte[]{}),
|
||||
this.options.entrySet()
|
||||
.stream()
|
||||
.map(ent -> new dev.toad.msg.owned.Option(ent.getKey(), ent.getValue()))
|
||||
|
@ -29,7 +29,23 @@ public final class Accept extends ContentFormat implements Option {
|
||||
return Accept.number;
|
||||
}
|
||||
|
||||
public Accept(Option o) {
|
||||
super(new ContentFormat(o).value());
|
||||
}
|
||||
|
||||
public Accept(ContentFormat format) {
|
||||
this(format.value());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return switch (other) {
|
||||
case Accept cf -> this.equals(cf);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean equals(Accept other) {
|
||||
return this.value.equals(other.value);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
package dev.toad.msg.option;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
import dev.toad.ffi.u16;
|
||||
import dev.toad.msg.Option;
|
||||
import dev.toad.msg.OptionValue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public sealed class ContentFormat implements Option permits Accept {
|
||||
|
||||
@ -12,6 +15,30 @@ public sealed class ContentFormat implements Option permits Accept {
|
||||
|
||||
public static final long number = 12;
|
||||
|
||||
public ContentFormat(Option o) {
|
||||
if (o.number() != ContentFormat.number) {
|
||||
throw new IllegalArgumentException(String.format("%d != ContentFormat number %d", o.number(), Path.number));
|
||||
}
|
||||
|
||||
if (o.values().size() > 1) {
|
||||
throw new IllegalArgumentException(String.format("ContentFormat is not repeatable, %s", o.values().stream().map(v -> v.asString()).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
var bytes = o.values().get(0).asBytes();
|
||||
|
||||
var buf = ByteBuffer.wrap(bytes);
|
||||
if (bytes.length == 1) {
|
||||
this.value = new u16(buf.get());
|
||||
} else if (bytes.length == 2) {
|
||||
this.value = new u16(buf.getShort());
|
||||
} else if (bytes.length == 3) {
|
||||
buf.put(0, (byte)0);
|
||||
this.value = new u16(buf.getInt());
|
||||
} else {
|
||||
this.value = new u16(buf.getInt());
|
||||
}
|
||||
}
|
||||
|
||||
ContentFormat(int value) {
|
||||
this.value = new u16(value);
|
||||
}
|
||||
@ -22,13 +49,34 @@ public sealed class ContentFormat implements Option permits Accept {
|
||||
public static final ContentFormat OCTET_STREAM = ContentFormat.custom(42);
|
||||
public static final ContentFormat EXI = ContentFormat.custom(47);
|
||||
public static final ContentFormat JSON = ContentFormat.custom(50);
|
||||
public static final ContentFormat CBOR = ContentFormat.custom(60);
|
||||
|
||||
public static final ContentFormat IMAGE_GIF = ContentFormat.custom(21);
|
||||
public static final ContentFormat IMAGE_JPG = ContentFormat.custom(22);
|
||||
public static final ContentFormat IMAGE_PNG = ContentFormat.custom(23);
|
||||
public static final ContentFormat IMAGE_SVG = ContentFormat.custom(30000);
|
||||
|
||||
public static final ContentFormat JAVASCRIPT = ContentFormat.custom(10002);
|
||||
public static final ContentFormat CSS = ContentFormat.custom(20000);
|
||||
|
||||
public static ContentFormat custom(int value) {
|
||||
return new ContentFormat(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return switch (other) {
|
||||
case ContentFormat cf -> this.equals(cf);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean equals(ContentFormat other) {
|
||||
return this.value == other.value;
|
||||
return this.value() == other.value();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("ContentFormat(%d)", this.value());
|
||||
}
|
||||
|
||||
public long number() {
|
||||
|
@ -21,11 +21,15 @@ public final class Path implements Option {
|
||||
}
|
||||
|
||||
public Path(String path) {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
if (path == null || path.isEmpty()) {
|
||||
this.segments = new ArrayList<>();
|
||||
} else {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
this.segments = new ArrayList<>(Arrays.asList(path.trim().split("/")));
|
||||
this.segments = new ArrayList<>(Arrays.asList(path.trim().split("/")));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean equals(Path other) {
|
||||
|
@ -27,7 +27,11 @@ public final class Query implements Option {
|
||||
}
|
||||
|
||||
public Query(String query) {
|
||||
this.query = new ArrayList<>(Arrays.asList(query.split("&")));
|
||||
if (query == null || query.isEmpty()) {
|
||||
this.query = new ArrayList<>();
|
||||
} else {
|
||||
this.query = new ArrayList<>(Arrays.asList(query.split("&")));
|
||||
}
|
||||
}
|
||||
|
||||
public Query(List<String> query) {
|
||||
|
@ -23,8 +23,7 @@ class E2E extends munit.FunSuite {
|
||||
.code(Code.OK_CONTENT)
|
||||
.id(Id(2))
|
||||
.token(Token(Array(1)))
|
||||
.option(ContentFormat.TEXT)
|
||||
.payload("foobar")
|
||||
.payload(Payload.text("foobar"))
|
||||
.build
|
||||
|
||||
val req = dev.toad.msg.build.Message
|
||||
|
@ -1,39 +1,99 @@
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import dev.toad.msg.*
|
||||
import dev.toad.msg.option.ContentFormat
|
||||
import dev.toad.msg.option.Host
|
||||
import dev.toad.msg.option.Path
|
||||
import dev.toad.msg.option.Query
|
||||
|
||||
class Message extends munit.FunSuite {
|
||||
test("Message.uri (coap://)") {
|
||||
class MessageBuilder extends munit.FunSuite {
|
||||
test("payload sets content format") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coap://google.com/cheese/gruyere?foo=bar&bingus")
|
||||
.uri("coap://localhost")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.payload(Payload.json("[\"fart\"]"))
|
||||
.build
|
||||
|
||||
assertEquals(msg.getContentFormat.get, ContentFormat.JSON)
|
||||
}
|
||||
|
||||
test("uri uses system DNS to resolve host address") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coap://localhost")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.build
|
||||
|
||||
val a = msg.addr.get
|
||||
val h = msg.getHost.get
|
||||
val p = msg.getPath.get
|
||||
val q = msg.getQuery.get.toMap.asScala
|
||||
assertEquals(msg.addr.get.getAddress.getHostAddress, "127.0.0.1");
|
||||
}
|
||||
|
||||
assertEquals(a.getPort, 5683)
|
||||
assertEquals(h.toString, "google.com")
|
||||
assertEquals(p.toString, "cheese/gruyere")
|
||||
test("uri gets port from URI") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coap://localhost:1234")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.build
|
||||
|
||||
assertEquals(msg.addr.get.getPort, 1234)
|
||||
}
|
||||
|
||||
test("uri gets port 5683 from scheme coap://") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coap://localhost")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.build
|
||||
|
||||
assertEquals(msg.addr.get.getPort, 5683)
|
||||
}
|
||||
|
||||
test("uri gets port 5684 from scheme coaps://") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coaps://localhost/cheese/gruyere?foo=bar&bingus")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.build
|
||||
|
||||
assertEquals(msg.addr.get.getPort, 5684)
|
||||
}
|
||||
|
||||
test("uri sets host to host section of uri") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coap://localhost/cheese/gruyere?foo=bar&bingus")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.build
|
||||
|
||||
assertEquals(msg.getHost.get.toString, "localhost")
|
||||
}
|
||||
|
||||
test("uri sets path to path section of uri") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coap://localhost/cheese/gruyere?foo=bar&bingus")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.build
|
||||
|
||||
assertEquals(Seq.from(msg.getPath.get.segments.asScala), Seq("cheese", "gruyere"))
|
||||
}
|
||||
|
||||
test("uri sets query to query section of uri") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coap://localhost/cheese/gruyere?foo=bar&bingus")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.build
|
||||
|
||||
val q = msg.getQuery.get.toMap.asScala
|
||||
assertEquals(Seq.from(q.get("foo").get.asScala), Seq(Query.Value("bar")))
|
||||
assertEquals(Seq.from(q.get("bingus").get.asScala), Seq(Query.Value.empty))
|
||||
}
|
||||
|
||||
test("Message.uri (coaps://)") {
|
||||
val msg = dev.toad.msg.build.Message
|
||||
.builder()
|
||||
.uri("coaps://google.com/cheese/gruyere?foo=bar&bingus")
|
||||
.`type`(Type.NON)
|
||||
.code(Code.GET)
|
||||
.build
|
||||
|
||||
val a = msg.addr.get
|
||||
assertEquals(a.getPort, 5684)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user