Skip to main content

apcore_toolkit/
schema_utils.rs

1// JSON Schema enrichment utilities.
2//
3// Helpers for merging docstring-extracted parameter descriptions into
4// JSON Schema `properties`.
5
6use std::collections::HashMap;
7
8use serde_json::Value;
9
10/// Merge parameter descriptions into JSON Schema properties.
11///
12/// By default, only fills in *missing* descriptions — existing ones are
13/// preserved. Set `overwrite` to `true` to replace existing descriptions.
14///
15/// Returns a **new** Value with descriptions merged in. The original
16/// schema is not mutated.
17pub fn enrich_schema_descriptions(
18    schema: &Value,
19    param_descriptions: &HashMap<String, String>,
20    overwrite: bool,
21) -> Value {
22    if param_descriptions.is_empty() {
23        return schema.clone();
24    }
25
26    match schema.get("properties") {
27        Some(p) if p.is_object() => p,
28        _ => return schema.clone(),
29    };
30
31    let mut result = schema.clone();
32
33    if let Some(result_props) = result.get_mut("properties").and_then(|p| p.as_object_mut()) {
34        for (name, desc) in param_descriptions {
35            if let Some(prop) = result_props.get_mut(name) {
36                if let Some(prop_obj) = prop.as_object_mut() {
37                    if overwrite || !prop_obj.contains_key("description") {
38                        prop_obj.insert("description".to_string(), Value::String(desc.clone()));
39                    }
40                }
41            }
42        }
43    }
44
45    result
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use serde_json::json;
52
53    #[test]
54    fn test_enrich_adds_missing_descriptions() {
55        let schema = json!({
56            "type": "object",
57            "properties": {
58                "name": {"type": "string"},
59                "age": {"type": "integer"}
60            }
61        });
62        let mut descs = HashMap::new();
63        descs.insert("name".into(), "The user's name".into());
64        descs.insert("age".into(), "The user's age".into());
65
66        let result = enrich_schema_descriptions(&schema, &descs, false);
67        assert_eq!(
68            result["properties"]["name"]["description"],
69            "The user's name"
70        );
71        assert_eq!(result["properties"]["age"]["description"], "The user's age");
72    }
73
74    #[test]
75    fn test_enrich_preserves_existing_descriptions() {
76        let schema = json!({
77            "type": "object",
78            "properties": {
79                "name": {"type": "string", "description": "Original"}
80            }
81        });
82        let mut descs = HashMap::new();
83        descs.insert("name".into(), "New description".into());
84
85        let result = enrich_schema_descriptions(&schema, &descs, false);
86        assert_eq!(result["properties"]["name"]["description"], "Original");
87    }
88
89    #[test]
90    fn test_enrich_overwrites_when_flag_set() {
91        let schema = json!({
92            "type": "object",
93            "properties": {
94                "name": {"type": "string", "description": "Original"}
95            }
96        });
97        let mut descs = HashMap::new();
98        descs.insert("name".into(), "Overwritten".into());
99
100        let result = enrich_schema_descriptions(&schema, &descs, true);
101        assert_eq!(result["properties"]["name"]["description"], "Overwritten");
102    }
103
104    #[test]
105    fn test_enrich_empty_descriptions_returns_original() {
106        let schema = json!({"type": "object", "properties": {"a": {"type": "string"}}});
107        let descs = HashMap::new();
108        let result = enrich_schema_descriptions(&schema, &descs, false);
109        assert_eq!(result, schema);
110    }
111
112    #[test]
113    fn test_enrich_no_properties_returns_original() {
114        let schema = json!({"type": "string"});
115        let mut descs = HashMap::new();
116        descs.insert("x".into(), "desc".into());
117        let result = enrich_schema_descriptions(&schema, &descs, false);
118        assert_eq!(result, schema);
119    }
120
121    #[test]
122    fn test_enrich_ignores_unknown_params() {
123        let schema = json!({
124            "type": "object",
125            "properties": {"name": {"type": "string"}}
126        });
127        let mut descs = HashMap::new();
128        descs.insert("unknown_field".into(), "desc".into());
129        let result = enrich_schema_descriptions(&schema, &descs, false);
130        assert!(!result["properties"]
131            .as_object()
132            .unwrap()
133            .contains_key("unknown_field"));
134    }
135
136    #[test]
137    fn test_enrich_does_not_mutate_original() {
138        let schema = json!({
139            "type": "object",
140            "properties": {
141                "name": {"type": "string"},
142                "age": {"type": "integer"}
143            }
144        });
145        let original = schema.clone();
146        let mut descs = HashMap::new();
147        descs.insert("name".into(), "A name".into());
148        descs.insert("age".into(), "An age".into());
149
150        let result = enrich_schema_descriptions(&schema, &descs, false);
151        // Result should have descriptions
152        assert_eq!(result["properties"]["name"]["description"], "A name");
153        // Original must be unchanged
154        assert_eq!(schema, original, "original schema must not be mutated");
155        assert!(
156            schema["properties"]["name"]
157                .as_object()
158                .unwrap()
159                .get("description")
160                .is_none(),
161            "original should not have description added"
162        );
163    }
164
165    #[test]
166    fn test_enrich_partial_match() {
167        // Only some params match schema properties; others are ignored
168        let schema = json!({
169            "type": "object",
170            "properties": {
171                "name": {"type": "string"},
172                "email": {"type": "string"}
173            }
174        });
175        let mut descs = HashMap::new();
176        descs.insert("name".into(), "User name".into());
177        descs.insert("nonexistent".into(), "Should be ignored".into());
178
179        let result = enrich_schema_descriptions(&schema, &descs, false);
180        // Matching property gets description
181        assert_eq!(result["properties"]["name"]["description"], "User name");
182        // Non-matching property stays untouched (no description added)
183        assert!(
184            result["properties"]["email"]
185                .as_object()
186                .unwrap()
187                .get("description")
188                .is_none(),
189            "email should not get a description"
190        );
191        // Nonexistent param should not create a new property
192        assert!(
193            !result["properties"]
194                .as_object()
195                .unwrap()
196                .contains_key("nonexistent"),
197            "nonexistent param should not appear in properties"
198        );
199    }
200}