feat: known options, uri parsing
This commit is contained in:
parent
ff7c0b00e4
commit
30e4a235f3
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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() {
|
||||
|
55
src/main/java/dev.toad/msg/option/Host.java
Normal file
55
src/main/java/dev.toad/msg/option/Host.java
Normal 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;
|
||||
}
|
||||
}
|
53
src/main/java/dev.toad/msg/option/Path.java
Normal file
53
src/main/java/dev.toad/msg/option/Path.java
Normal 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());
|
||||
}
|
||||
}
|
117
src/main/java/dev.toad/msg/option/Query.java
Normal file
117
src/main/java/dev.toad/msg/option/Query.java
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
39
src/test/scala/Message.scala
Normal file
39
src/test/scala/Message.scala
Normal 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)
|
||||
}
|
||||
}
|
49
src/test/scala/Options.scala
Normal file
49
src/test/scala/Options.scala
Normal 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")),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user