Let root-free take its machines as a list, as the router names them

The router's contract names the machines as a JSON array. A verb's argument declared a list now takes
an array of names (or one text separated by commas), and refuses anything else in it.
This commit is contained in:
2026-10-09 13:48:56 +02:00
parent 4c375dafed
commit 0e46b8302d
3 changed files with 52 additions and 5 deletions
+13 -2
View File
@@ -181,7 +181,18 @@ func TestRootFreeIsAnsweredOnlyByTheServingController(t *testing.T) {
if _, err := argvFor("root-free", map[string]any{"machines": "anchor"}); err == nil {
t.Error("root-free ran as a command")
}
if _, err := readArguments("root-free", map[string]any{"machines": []any{"anchor"}}); err == nil {
t.Error("root-free took its machines as a list; they are named in one text, separated by commas")
// The router names its machines as a list (its contract with this verb); one text separated by commas is
// the same; anything else in the list is refused.
for _, given := range []any{[]any{"anchor", "relay"}, "anchor, relay"} {
a, err := readArguments("root-free", map[string]any{"machines": given})
if err != nil || a.given["machines"] != "anchor,relay" && a.given["machines"] != "anchor, relay" {
t.Errorf("root-free given %v read %v (%v)", given, a, err)
}
}
if _, err := readArguments("root-free", map[string]any{"machines": []any{"anchor", 7}}); err == nil {
t.Error("root-free took a number for a machine")
}
if _, err := readArguments("status", map[string]any{"machines": []any{"anchor"}}); err == nil {
t.Error("a verb that takes no list took one")
}
}
+21
View File
@@ -114,6 +114,14 @@ func declaredArguments(v catalogue.Verb) (names []string, switches map[string]bo
return names, switches
}
// isList says a verb's argument is declared a list of text (catalogue's listed): given as a JSON array, it is
// read as its items joined by commas, as the same argument given as one text would be.
func isList(v catalogue.Verb, name string) bool {
props, _ := v.Input["properties"].(map[string]any)
p, _ := props[name].(map[string]any)
return p != nil && p["type"] == "array"
}
// readArguments refuses what the verb does not take, before anything is composed.
func readArguments(verb string, args map[string]any) (*verbArguments, error) {
v, known := controllerVerb(verb)
@@ -150,6 +158,19 @@ func readArguments(verb string, args map[string]any) (*verbArguments, error) {
return nil, fmt.Errorf("%s: %q is text, not true or false", verb, k)
}
value = fmt.Sprint(x)
case []any:
if !isList(v, k) {
return nil, fmt.Errorf("%s: %q is text, and was given a list", verb, k)
}
items := make([]string, 0, len(x))
for _, item := range x {
text, ok := item.(string)
if !ok || strings.TrimSpace(text) == "" || strings.Contains(text, ",") {
return nil, fmt.Errorf("%s: %q is a list of names, and holds %v", verb, k, item)
}
items = append(items, strings.TrimSpace(text))
}
value = strings.Join(items, ",")
default:
return nil, fmt.Errorf("%s: %q is text, and was given %T", verb, k, x)
}
+18 -3
View File
@@ -435,9 +435,9 @@ var ControllerVerbs = []Verb{
"last 15 minutes, and the login shell's execute is not served there. Anything else, a read that failed " +
"included, is not free and says why. The router asks it before an answer from a channel proving its sender " +
"may approve. Only reads.",
Input: schema(map[string]string{
"machines": "the machines to judge, separated by commas",
}, []string{"machines"})},
Input: listed(schema(map[string]string{
"machines": "the machines to judge, by name: a list, or one text separated by commas",
}, []string{"machines"}), "machines")},
{Name: "dead-letters", Description: "Every message a consumer on the bus gave up on after handing it over " +
"as often as it may, kept in DEAD_LETTERS: whose consumer, the subject, how often it was handed over and " +
"when it was given up, newest first. With id: that one whole, with what it said. With deliver: hand it " +
@@ -554,6 +554,21 @@ func schema(properties map[string]string, required []string, switches ...string)
return out
}
// listed makes the named properties of a schema lists of text: a caller gives them as a JSON array (or, as
// any argument, one text separated by commas).
func listed(in map[string]any, names ...string) map[string]any {
props, _ := in["properties"].(map[string]any)
for _, n := range names {
p, _ := props[n].(map[string]any)
if p == nil {
panic("a list that is not a property: " + n)
}
props[n] = map[string]any{"type": "array", "items": map[string]any{"type": "string"},
"description": p["description"]}
}
return in
}
// unpromised is what a claim says it serves and the seat's protocol never promised.
func unpromised(serves []string, promised []Verb) []string {
has := map[string]bool{}