Discover Repeat collections and compatible Components in both hosts

This commit is contained in:
2026-11-13 14:47:21 +01:00
parent 0300062fb6
commit 0d14ac14e0
28 changed files with 969 additions and 44 deletions
@@ -14,7 +14,45 @@ public record BindingMetadata(
List<Problem> problems,
String controller,
String viewModel,
List<Computed> computed) {
List<Computed> computed,
List<RepeatOption> collections,
List<RepeatOption> components,
List<RepeatOption> panes) {
public record RepeatOption(String name, String type, String status, String reason) {
public String toString() {
return name + " : " + type + " — " + status + " (" + reason + ")";
}
}
public BindingMetadata(
String model,
String path,
String direction,
List<Attribute> attributes,
List<Target> targets,
List<String> diagnostics,
List<Converter> converters,
List<Problem> problems,
String controller,
String viewModel,
List<Computed> computed) {
this(
model,
path,
direction,
attributes,
targets,
diagnostics,
converters,
problems,
controller,
viewModel,
computed,
List.of(),
List.of(),
List.of());
}
public record Computed(String method, String returnType, String status, String reason) {
public String toString() {
return method + " : " + returnType + " — " + status + " (" + reason + ")";
@@ -78,13 +116,19 @@ public record BindingMetadata(
converters = List.copyOf(converters);
problems = List.copyOf(problems);
computed = List.copyOf(computed);
collections = List.copyOf(collections);
components = List.copyOf(components);
panes = List.copyOf(panes);
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", "3");
m.put("format", "4");
encodeOptions(m, "rc", collections);
encodeOptions(m, "rt", components);
encodeOptions(m, "rp", panes);
m.put("controller", controller);
m.put("viewModel", viewModel);
m.put("computed", "" + computed.size());
@@ -140,6 +184,33 @@ public record BindingMetadata(
return m;
}
private static void encodeOptions(
Map<String, String> m, String prefix, List<RepeatOption> values) {
m.put(prefix, "" + values.size());
for (int i = 0; i < values.size(); i++) {
var v = values.get(i);
String p = prefix + "." + i + ".";
m.put(p + "name", v.name());
m.put(p + "type", v.type());
m.put(p + "status", v.status());
m.put(p + "reason", v.reason());
}
}
private static List<RepeatOption> decodeOptions(Map<String, String> m, String prefix) {
var result = new ArrayList<RepeatOption>();
for (int i = 0; i < count(m, prefix); i++) {
String p = prefix + "." + i + ".";
result.add(
new RepeatOption(
field(m, p + "name"),
field(m, p + "type"),
field(m, p + "status"),
field(m, p + "reason")));
}
return result;
}
private static String field(Map<String, String> m, String key) {
return Objects.requireNonNull(m.get(key), "Missing " + key);
}
@@ -151,7 +222,7 @@ public record BindingMetadata(
}
public static BindingMetadata decode(Map<String, String> m) {
if (!"3".equals(field(m, "format")))
if (!"4".equals(field(m, "format")))
throw new IllegalArgumentException("Unsupported binding metadata");
var a = new ArrayList<Attribute>();
var t = new ArrayList<Target>();
@@ -212,6 +283,9 @@ public record BindingMetadata(
v,
field(m, "controller"),
field(m, "viewModel"),
methods);
methods,
decodeOptions(m, "rc"),
decodeOptions(m, "rt"),
decodeOptions(m, "rp"));
}
}
@@ -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-v3")
&& hello.capabilities().contains("bindings-v3")) usable.add(Operation.BINDINGS);
&& supported.contains("bindings-v4")
&& hello.capabilities().contains("bindings-v4")) 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-v3") && !usable.contains(Operation.BINDINGS))
if (requiredByEither.contains("bindings-v4") && !usable.contains(Operation.BINDINGS))
throw new IllegalArgumentException("Required capability version incompatible");
operations = Set.copyOf(usable);
state = State.READY;
@@ -26,7 +26,10 @@ class BindingMetadataTest {
"app.ViewModel",
List.of(
new BindingMetadata.Computed(
"#caption", "java.lang.String", "COMPATIBLE", "Declared type")));
"#caption", "java.lang.String", "COMPATIBLE", "Declared type")),
List.of(new BindingMetadata.RepeatOption("visits", "Address", "COMPATIBLE", "Element")),
List.of(new BindingMetadata.RepeatOption("app.Card", "Address", "COMPATIBLE", "Model")),
List.of(new BindingMetadata.RepeatOption("rows", "VBox", "COMPATIBLE", "Pane")));
assertEquals(
m, BindingMetadata.decode(PreviewMessages.decode(PreviewMessages.encode(m.encode()))));
var id = new Identity("s", "r", "c", "b", Map.of("file", 1L), 0, 0);
@@ -41,14 +44,14 @@ class BindingMetadataTest {
new Identity("s", "r", "c", "b", Map.of(), 0, 0),
new byte[0]));
var invalid = new HashMap<>(m.encode());
invalid.put("converters", "99999");
invalid.put("rc", "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-v3"), Set.of("bindings-v3"));
var session = new ProtocolSession("s", 1, token, Set.of("bindings-v4"), Set.of("bindings-v4"));
assertTrue(
session
.authenticate(
@@ -60,11 +63,11 @@ class BindingMetadataTest {
new Version(1, 0),
new Version(1, 0),
new Version(1, 0),
Set.of("bindings-v3"),
Set.of("bindings-v4"),
Set.of()))
.contains(Operation.BINDINGS));
var oldWorker =
new ProtocolSession("s", 1, token, Set.of("bindings-v3"), Set.of("bindings-v3"));
new ProtocolSession("s", 1, token, Set.of("bindings-v4"), Set.of("bindings-v4"));
assertThrows(
IllegalArgumentException.class,
() ->
@@ -77,10 +80,10 @@ class BindingMetadataTest {
new Version(1, 0),
new Version(1, 0),
new Version(1, 0),
Set.of("bindings-v2"),
Set.of("bindings-v3"),
Set.of())));
var incompatible =
new ProtocolSession("s", 1, token, Set.of("bindings-v3"), Set.of("bindings-v3"));
new ProtocolSession("s", 1, token, Set.of("bindings-v4"), Set.of("bindings-v4"));
assertThrows(
IllegalArgumentException.class,
() ->
@@ -93,7 +96,7 @@ class BindingMetadataTest {
new Version(1, 0),
new Version(2, 0),
new Version(1, 0),
Set.of("bindings-v3"),
Set.of("bindings-v4"),
Set.of())));
}
}