Skip to main content

khive_runtime/
input_schema.rs

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