Discover Converter capabilities and validate shared binding drafts

This commit is contained in:
2026-11-13 12:58:08 +01:00
parent 17b1bce704
commit 5a208d2336
35 changed files with 1096 additions and 34 deletions
@@ -9,7 +9,27 @@ public record BindingMetadata(
String direction,
List<Attribute> attributes,
List<Target> targets,
List<String> diagnostics) {
List<String> diagnostics,
List<Converter> converters,
List<Problem> problems) {
public record Converter(String name, String status, String reason) {
public String toString() {
return name + " — " + status + ": " + reason;
}
}
public record Problem(String control, String property, String severity, String message) {}
public BindingMetadata(
String model,
String path,
String direction,
List<Attribute> attributes,
List<Target> targets,
List<String> diagnostics) {
this(model, path, direction, attributes, targets, diagnostics, List.of(), List.of());
}
public record Attribute(String path, String type, String kind, String elementType) {
public String toString() {
return path + " : " + type + " (" + kind + ")";
@@ -23,13 +43,17 @@ public record BindingMetadata(
attributes = List.copyOf(attributes);
targets = List.copyOf(targets);
diagnostics = List.copyOf(diagnostics);
converters = List.copyOf(converters);
problems = List.copyOf(problems);
if (!Set.of("view", "model", "both").contains(direction))
throw new IllegalArgumentException("Invalid direction");
}
public Map<String, String> encode() {
var m = new TreeMap<String, String>();
m.put("format", "1");
m.put("format", "2");
m.put("converters", "" + converters.size());
m.put("problems", "" + problems.size());
m.put("model", model);
m.put("path", path);
m.put("direction", direction);
@@ -54,6 +78,21 @@ public record BindingMetadata(
m.put(p + "reason", t.reason);
}
for (int i = 0; i < diagnostics.size(); i++) m.put("d." + i, diagnostics.get(i));
for (int i = 0; i < converters.size(); i++) {
var c = converters.get(i);
String p = "c." + i + ".";
m.put(p + "name", c.name());
m.put(p + "status", c.status());
m.put(p + "reason", c.reason());
}
for (int i = 0; i < problems.size(); i++) {
var v = problems.get(i);
String p = "v." + i + ".";
m.put(p + "control", v.control());
m.put(p + "property", v.property());
m.put(p + "severity", v.severity());
m.put(p + "message", v.message());
}
return m;
}
@@ -68,7 +107,7 @@ public record BindingMetadata(
}
public static BindingMetadata decode(Map<String, String> m) {
if (!"1".equals(field(m, "format")))
if (!"2".equals(field(m, "format")))
throw new IllegalArgumentException("Unsupported binding metadata");
var a = new ArrayList<Attribute>();
var t = new ArrayList<Target>();
@@ -93,6 +132,22 @@ public record BindingMetadata(
field(m, p + "reason")));
}
for (int i = 0; i < count(m, "diagnostics"); i++) d.add(field(m, "d." + i));
return new BindingMetadata(field(m, "model"), field(m, "path"), field(m, "direction"), a, t, d);
var c = new ArrayList<Converter>();
var v = new ArrayList<Problem>();
for (int i = 0; i < count(m, "converters"); i++) {
String p = "c." + i + ".";
c.add(new Converter(field(m, p + "name"), field(m, p + "status"), field(m, p + "reason")));
}
for (int i = 0; i < count(m, "problems"); i++) {
String p = "v." + i + ".";
v.add(
new Problem(
field(m, p + "control"),
field(m, p + "property"),
field(m, p + "severity"),
field(m, p + "message")));
}
return new BindingMetadata(
field(m, "model"), field(m, "path"), field(m, "direction"), a, t, d, c, v);
}
}
@@ -82,8 +82,8 @@ public final class ProtocolSession implements AutoCloseable {
&& supported.contains("interactive-v1")
&& hello.capabilities().contains("interactive-v1")) usable.add(Operation.INTERACTIVE);
if (hello.metadata().major() == 1
&& supported.contains("bindings-v1")
&& hello.capabilities().contains("bindings-v1")) usable.add(Operation.BINDINGS);
&& supported.contains("bindings-v2")
&& hello.capabilities().contains("bindings-v2")) usable.add(Operation.BINDINGS);
var requiredByEither = new HashSet<>(required);
requiredByEither.addAll(hello.required());
if ((requiredByEither.contains("discover-v1") && !usable.contains(Operation.DISCOVER))
@@ -92,7 +92,7 @@ public final class ProtocolSession implements AutoCloseable {
if ((requiredByEither.contains("layout-v1") && !usable.contains(Operation.MEASURE))
|| (requiredByEither.contains("interactive-v1") && !usable.contains(Operation.INTERACTIVE)))
throw new IllegalArgumentException("Required capability version incompatible");
if (requiredByEither.contains("bindings-v1") && !usable.contains(Operation.BINDINGS))
if (requiredByEither.contains("bindings-v2") && !usable.contains(Operation.BINDINGS))
throw new IllegalArgumentException("Required capability version incompatible");
operations = Set.copyOf(usable);
state = State.READY;
@@ -19,7 +19,9 @@ class BindingMetadataTest {
List.of(
new BindingMetadata.Target(
"input", "text", "java.lang.String", "COMPATIBLE", "Types match")),
List.of());
List.of(),
List.of(new BindingMetadata.Converter("app.C", "UNVERIFIED", "No metadata")),
List.of(new BindingMetadata.Problem("input", "text", "ERROR", "Missing path")));
assertEquals(
m, BindingMetadata.decode(PreviewMessages.decode(PreviewMessages.encode(m.encode()))));
var id = new Identity("s", "r", "c", "b", Map.of("file", 1L), 0, 0);
@@ -34,14 +36,14 @@ class BindingMetadataTest {
new Identity("s", "r", "c", "b", Map.of(), 0, 0),
new byte[0]));
var invalid = new HashMap<>(m.encode());
invalid.put("targets", "99999");
invalid.put("converters", "99999");
assertThrows(IllegalArgumentException.class, () -> BindingMetadata.decode(invalid));
}
@Test
void negotiationRequiresMetadataVersion() throws Exception {
String token = ProtocolSession.newToken();
var session = new ProtocolSession("s", 1, token, Set.of("bindings-v1"), Set.of("bindings-v1"));
var session = new ProtocolSession("s", 1, token, Set.of("bindings-v2"), Set.of("bindings-v2"));
assertTrue(
session
.authenticate(
@@ -53,11 +55,27 @@ class BindingMetadataTest {
new Version(1, 0),
new Version(1, 0),
new Version(1, 0),
Set.of("bindings-v1"),
Set.of("bindings-v2"),
Set.of()))
.contains(Operation.BINDINGS));
var oldWorker =
new ProtocolSession("s", 1, token, Set.of("bindings-v2"), Set.of("bindings-v2"));
assertThrows(
IllegalArgumentException.class,
() ->
oldWorker.authenticate(
InetAddress.getLoopbackAddress(),
new Hello(
"s",
1,
token,
new Version(1, 0),
new Version(1, 0),
new Version(1, 0),
Set.of("bindings-v1"),
Set.of())));
var incompatible =
new ProtocolSession("s", 1, token, Set.of("bindings-v1"), Set.of("bindings-v1"));
new ProtocolSession("s", 1, token, Set.of("bindings-v2"), Set.of("bindings-v2"));
assertThrows(
IllegalArgumentException.class,
() ->
@@ -70,7 +88,7 @@ class BindingMetadataTest {
new Version(1, 0),
new Version(2, 0),
new Version(1, 0),
Set.of("bindings-v1"),
Set.of("bindings-v2"),
Set.of())));
}
}