1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Derive a JSON Schema for a verb's parameters from its own [`ParamDef`] list.
//!
//! `help` publishes `params[].type` as a documentation string. Every machine
//! bridge parses it as a JSON Schema type, and several of the spellings in use
//! are not schema types: `uuid` names a format, `array` names no element type,
//! and `object or array of object` is a union. A driver that cannot read a
//! verb's parameter types cannot call it, and only the git pack publishes a
//! hand-authored `input_schema` to read instead.
//!
//! The duplicate spellings this module used to absorb are gone: one type is
//! written one way, and the registry-wide test in `kkernel` asserts the exact
//! set. The map below therefore has one arm per spelling, and adding an
//! alternation to an arm is the shape to refuse, because it lets a second
//! spelling of an existing type back in without anything failing.
//!
//! This module closes that by deriving a schema from the declarations the
//! runtime already holds. A pack-supplied `input_schema` still wins, so the
//! hand-authored ones are untouched; every other verb gains a derived one.
//! `params[].type` is left exactly as it is: it is documentation, and `uuid`
//! carries information a bare schema type does not.
use serde_json::{json, Value};
use khive_types::ParamDef;
/// Parameters every verb accepts without declaring them, and which therefore
/// must not be rejected by a derived schema.
///
/// `help` short-circuits any dispatch to the verb's own description, and
/// `namespace` is resolved for every verb by the dispatch path whether or not
/// the verb lists it. A schema stricter than the dispatcher turns a working
/// call into a driver-side refusal, which is the same defect this module exists
/// to fix pointed the other way, so `additionalProperties` stays open and these
/// two are declared rather than merely tolerated.
const UNIVERSAL_PARAMS: &[(&str, &str, &str)] = &[
(
"help",
"boolean",
"Return this verb's description instead of dispatching it. No side effects.",
),
(
"namespace",
"string",
"Attribution namespace for this call. Defaults to the caller's resolved namespace.",
),
];
/// Map one declared `param_type` spelling onto a JSON Schema fragment.
///
/// Total over the spellings in use, and deliberately without a wildcard arm: an
/// unknown spelling returns `None` so the registry-wide test can name it and
/// fail. A default arm here would silently give every future spelling whatever
/// the fallback happened to be, which recreates the defect one verb at a time.
pub fn json_schema_type(param_type: &str) -> Option<Value> {
let schema = match param_type {
"string" => json!({"type": "string"}),
"integer" => json!({"type": "integer"}),
"number" => json!({"type": "number"}),
"boolean" => json!({"type": "boolean"}),
"object" => json!({"type": "object"}),
"array" => json!({"type": "array"}),
"uuid" => json!({"type": "string", "format": "uuid"}),
"array of string" => {
json!({"type": "array", "items": {"type": "string"}})
}
"array of object" => {
json!({"type": "array", "items": {"type": "object"}})
}
"array of uuid" => {
json!({"type": "array", "items": {"type": "string", "format": "uuid"}})
}
"object or array of object" => json!({
"oneOf": [{"type": "object"}, {"type": "array", "items": {"type": "object"}}]
}),
"string | array<string>" => json!({
"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]
}),
"string|null" => json!({"type": ["string", "null"]}),
// A parameter documented as accepting any JSON. The empty schema says
// exactly that, rather than picking one type and being wrong.
"JSON value" => json!({}),
_ => return None,
};
Some(schema)
}
/// Build the `input_schema` object for one verb's parameter list.
///
/// Returns `None` when any declared spelling has no mapping, so a verb is never
/// published with a schema that quietly omits one of its parameters. A partial
/// schema is worse than none: a driver would read it as complete.
pub fn derive_input_schema(params: &[ParamDef], described: &[(String, String)]) -> Option<Value> {
let mut properties = serde_json::Map::new();
let mut required: Vec<Value> = Vec::new();
for (index, param) in params.iter().enumerate() {
let mut schema = json_schema_type(param.param_type)?;
let description = described
.get(index)
.map(|(_, d)| d.clone())
.unwrap_or_else(|| param.description.to_string());
if let Value::Object(ref mut map) = schema {
map.insert("description".to_string(), json!(description));
}
properties.insert(param.name.to_string(), schema);
if param.required {
required.push(json!(param.name));
}
}
for (name, spelling, description) in UNIVERSAL_PARAMS {
if properties.contains_key(*name) {
continue;
}
let mut schema = json_schema_type(spelling)?;
if let Value::Object(ref mut map) = schema {
map.insert("description".to_string(), json!(description));
}
properties.insert((*name).to_string(), schema);
}
Some(json!({
"type": "object",
"properties": Value::Object(properties),
"required": Value::Array(required),
"additionalProperties": true,
}))
}