feat: known options, uri parsing

This commit is contained in:
Orion Kindel 2023-04-19 20:37:42 -05:00
parent ff7c0b00e4
commit 30e4a235f3
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
11 changed files with 369 additions and 1 deletions

View File

@ -1,5 +1,8 @@
package dev.toad.msg;
import dev.toad.msg.option.Path;
import dev.toad.msg.option.Host;
import dev.toad.msg.option.Query;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Optional;
@ -17,6 +20,22 @@ public interface Message {
public List<Option> options();
default public Optional<Option> getOption(long number) {
return this.options().stream().filter(o -> o.number() == number).findAny();
}
default public Optional<Path> getPath() {
return this.getOption(Path.number).map(o -> new Path(o));
}
default public Optional<Host> getHost() {
return this.getOption(Host.number).map(o -> new Host(o));
}
default public Optional<Query> getQuery() {
return this.getOption(Query.number).map(o -> new Query(o));
}
public byte[] payloadBytes();
public String payloadString();

View File

@ -4,6 +4,13 @@ import dev.toad.msg.Code;
import dev.toad.msg.Id;
import dev.toad.msg.Token;
import dev.toad.msg.Type;
import dev.toad.msg.option.Path;
import dev.toad.msg.option.Query;
import dev.toad.msg.option.Host;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.net.URI;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
@ -30,6 +37,18 @@ public final class Message
return new Message();
}
public MessageNeeds.Type 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().equals("coaps") ? 5684 : 5683;
this.addr = Optional.of(new InetSocketAddress(addr, port));
return this
.option(new Host(uri.getHost()))
.option(new Query(uri.getQuery()))
.option(new Path(uri.getPath()));
}
public MessageNeeds.Type addr(InetSocketAddress addr) {
this.addr = Optional.of(addr);
return this;

View File

@ -1,10 +1,13 @@
package dev.toad.msg.build;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.net.InetSocketAddress;
public final class MessageNeeds {
public interface Destination {
MessageNeeds.Type uri(String uri) throws URISyntaxException, UnknownHostException;
MessageNeeds.Type addr(InetSocketAddress addr);
}

View File

@ -8,6 +8,8 @@ import java.util.List;
public final class Accept extends ContentFormat implements Option {
public static final long number = 17;
public static final Accept TEXT = new Accept(ContentFormat.TEXT);
public static final Accept LINK_FORMAT = new Accept(
ContentFormat.LINK_FORMAT
@ -23,6 +25,10 @@ public final class Accept extends ContentFormat implements Option {
super(value);
}
public long number() {
return Accept.number;
}
public Accept(ContentFormat format) {
this(format.value());
}

View File

@ -10,6 +10,8 @@ public sealed class ContentFormat implements Option permits Accept {
final u16 value;
public static final long number = 12;
ContentFormat(int value) {
this.value = new u16(value);
}
@ -30,7 +32,7 @@ public sealed class ContentFormat implements Option permits Accept {
}
public long number() {
return 12;
return ContentFormat.number;
}
public int value() {

View File

@ -0,0 +1,55 @@
package dev.toad.msg.option;
import dev.toad.msg.Option;
import dev.toad.msg.OptionValue;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
public final class Host implements Option {
final String host;
public static final long number = 3;
public Host(Option o) {
if (o.number() != Host.number) {
throw new IllegalArgumentException(String.format("%d != Host number %d", o.number(), Path.number));
}
if (o.values().size() > 1) {
throw new IllegalArgumentException(String.format("Host is not repeatable, %s", o.values().stream().map(v -> v.asString()).collect(Collectors.toList())));
}
this.host = o.values().get(0).asString();
}
public Host(String host) {
this.host = host;
}
@Override
public long number() {
return Host.number;
}
@Override
public String toString() {
return this.host;
}
@Override
public boolean equals(Object other) {
return switch(other) {
case Host h -> this.toString().equals(h.toString());
default -> false;
};
}
@Override
public List<OptionValue> values() {
var list = new ArrayList<OptionValue>();
list.add(new dev.toad.msg.owned.OptionValue(this.host));
return list;
}
}

View File

@ -0,0 +1,53 @@
package dev.toad.msg.option;
import dev.toad.msg.Option;
import dev.toad.msg.OptionValue;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
public final class Path implements Option {
final ArrayList<String> segments;
public static final long number = 11;
public Path(Option o) {
if (o.number() != Path.number) {
throw new IllegalArgumentException(String.format("%d != Path number %d", o.number(), Path.number));
}
this.segments = o.values().stream().map(v -> v.asString()).collect(Collectors.toCollection(() -> new ArrayList<>()));
}
public Path(String path) {
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;
}
public List<String> segments() {
return this.segments;
}
@Override
public long number() {
return Path.number;
}
@Override
public String toString() {
return String.join("/", this.segments);
}
@Override
public List<OptionValue> values() {
return this.segments.stream().map(s -> new dev.toad.msg.owned.OptionValue(s)).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,117 @@
package dev.toad.msg.option;
import dev.toad.msg.Option;
import dev.toad.msg.OptionValue;
import java.util.Optional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Collectors;
public final class Query implements Option {
final ArrayList<String> query;
public static final long number = 15;
public Query(Option o) {
if (o.number() != Query.number) {
throw new IllegalArgumentException(String.format("%d != Query number %d", o.number(), Query.number));
}
this.query = o.values().stream().map(v -> v.asString()).collect(Collectors.toCollection(() -> new ArrayList<>()));
}
public Query(String query) {
this.query = new ArrayList<>(Arrays.asList(query.split("&")));
}
public Query(List<String> query) {
this.query = new ArrayList<>(query);
}
public Map<String, List<Value>> toMap() {
Function<String[], String> key = segs -> segs[0];
Function<String[], List<Value>> value = segs -> {
var vals = new ArrayList<Value>();
if (segs.length == 2) {
vals.add(new Value(segs[1]));
} else {
vals.add(new Value(null));
}
return vals;
};
BinaryOperator<List<Value>> merge = (a, b) -> {
a.addAll(b);
return a;
};
return this.query.stream()
.map(v -> v.split("=", 2))
.collect(Collectors.toMap(key, value, merge, () -> new HashMap<>()));
}
@Override
public long number() {
return Query.number;
}
@Override
public String toString() {
return String.join("&", this.query);
}
@Override
public List<OptionValue> values() {
return this.query.stream()
.map(q -> new dev.toad.msg.owned.OptionValue(q))
.collect(Collectors.toList());
}
public static final class Value {
final Optional<String> val;
public Value(String val) {
if (val == null || val == "") {
this.val = Optional.empty();
} else {
this.val = Optional.of(val);
}
}
public static Value empty() {
return new Value(null);
}
public Optional<String> value() {
return this.val;
}
@Override
public String toString() {
if (!this.value().isEmpty()) {
return "Query.Value(\""+this.value().get()+"\")";
} else {
return "Query.Value.empty()";
}
}
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);
default -> false;
};
}
}
}

View File

@ -1,9 +1,15 @@
package dev.toad.msg.owned;
import java.nio.charset.StandardCharsets;
public class OptionValue implements dev.toad.msg.OptionValue {
public final byte[] bytes;
public OptionValue(String str) {
this.bytes = str.getBytes(StandardCharsets.UTF_8);
}
public OptionValue(byte[] bytes) {
this.bytes = bytes;
}

View File

@ -0,0 +1,39 @@
import scala.jdk.CollectionConverters.*
import dev.toad.msg.*
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://)") {
val msg = dev.toad.msg.build.Message
.builder()
.uri("coap://google.com/cheese/gruyere?foo=bar&bingus")
.`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(a.getPort, 5683)
assertEquals(h.toString, "google.com")
assertEquals(p.toString, "cheese/gruyere")
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)
}
}

View File

@ -0,0 +1,49 @@
import scala.collection.immutable.HashMap
import scala.jdk.CollectionConverters.*
import dev.toad.msg.option.Host
import dev.toad.msg.option.Path
import dev.toad.msg.option.Query
class Options extends munit.FunSuite {
test("Path.toString") {
val p = Path("foo/bar/baz")
assertEquals(p.toString, "foo/bar/baz")
}
test("Path.segments") {
val p = Path("foo/bar/baz")
assertEquals(Seq.from(p.segments.asScala), Seq("foo", "bar", "baz"))
}
test("Host.toString") {
val h = Host("cheese.com")
assertEquals(h.toString, "cheese.com")
}
test("Query.equals") {
val q = Query(
Seq("foo=test", "bar", "quux=a", "bar", "quux=b", "bar=third").asJava
)
assertEquals(q.toString, "foo=test&bar&quux=a&bar&quux=b&bar=third")
}
test("Query.toMap") {
val q = Query(
Seq("foo=test", "bar", "quux=a", "bar", "quux=b", "bar=third").asJava
)
val actual = q.toMap.asScala
assertEquals(
Map.from(
actual.map { case (k, vs) => (k, Seq.from(vs.asScala)) }
),
Map(
"foo" -> Seq(Query.Value("test")),
"bar" -> Seq(Query.Value.empty, Query.Value.empty, Query.Value("third")),
"quux" -> Seq(Query.Value("a"), Query.Value("b")),
)
)
}
}