feat: payload, content format changes

This commit is contained in:
Orion Kindel 2023-04-20 01:58:11 -05:00
parent 30e4a235f3
commit c4bd8c6273
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
14 changed files with 308 additions and 50 deletions

View File

@ -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(

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}

View 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);
}
}

View File

@ -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()))

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -21,12 +21,16 @@ public final class Path implements Option {
}
public Path(String path) {
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("/")));
}
}
public boolean equals(Path other) {
return this.segments == other.segments;

View File

@ -27,8 +27,12 @@ public final class Query implements Option {
}
public Query(String query) {
if (query == null || query.isEmpty()) {
this.query = new ArrayList<>();
} else {
this.query = new ArrayList<>(Arrays.asList(query.split("&")));
}
}
public Query(List<String> query) {
this.query = new ArrayList<>(query);

View File

@ -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

View File

@ -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)
}
}