Skip to main content

apcore_toolkit/
serializers.rs

1// ScannedModule serialization utilities.
2//
3// Pure functions with no framework dependency. Convert ScannedModule instances
4// to serde_json::Value suitable for JSON/YAML output or API responses.
5
6use serde_json::{json, Value};
7use tracing::warn;
8
9use apcore::module::ModuleAnnotations;
10
11use crate::types::ScannedModule;
12
13/// Convert annotations to a JSON Value, handling both present and absent forms.
14///
15/// Returns `Value::Null` if annotations is `None` or serialization fails.
16pub fn annotations_to_dict(annotations: Option<&ModuleAnnotations>) -> Value {
17    match annotations {
18        Some(ann) => serde_json::to_value(ann).unwrap_or_else(|e| {
19            warn!("Failed to serialize ModuleAnnotations: {e}");
20            Value::Null
21        }),
22        None => Value::Null,
23    }
24}
25
26/// Convert a ScannedModule to a JSON Value with all 14 fields.
27///
28/// Unlike `serde_json::to_value(&module)` (which honours
29/// `#[serde(skip_serializing_if = "Option::is_none")]` on the struct and
30/// omits unset optionals), this function emits every field — using
31/// `Value::Null` for absent optionals — to match the `to_dict` wire format
32/// of the Python and TypeScript SDKs. Downstream tools that rely on a
33/// stable key set across languages should use this rather than the
34/// derived `Serialize` impl.
35pub fn module_to_dict(module: &ScannedModule) -> Value {
36    let examples = serde_json::to_value(&module.examples).unwrap_or_else(|e| {
37        warn!(
38            module_id = %module.module_id,
39            "Failed to serialize examples: {e}"
40        );
41        json!([])
42    });
43
44    json!({
45        "module_id": module.module_id,
46        "description": module.description,
47        "documentation": module.documentation,
48        "tags": module.tags,
49        "version": module.version,
50        "target": module.target,
51        "annotations": annotations_to_dict(module.annotations.as_ref()),
52        "suggested_alias": module.suggested_alias,
53        "examples": examples,
54        "metadata": module.metadata,
55        "input_schema": module.input_schema,
56        "output_schema": module.output_schema,
57        "display": module.display,
58        "warnings": module.warnings,
59    })
60}
61
62/// Batch-convert a list of ScannedModules to Values.
63pub fn modules_to_dicts(modules: &[ScannedModule]) -> Vec<Value> {
64    modules.iter().map(module_to_dict).collect()
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use serde_json::json;
71
72    fn sample_module() -> ScannedModule {
73        ScannedModule::new(
74            "users.get".into(),
75            "Get user".into(),
76            json!({"type": "object"}),
77            json!({"type": "object"}),
78            vec!["users".into()],
79            "app:get_user".into(),
80        )
81    }
82
83    #[test]
84    fn test_annotations_to_dict_none() {
85        assert_eq!(annotations_to_dict(None), Value::Null);
86    }
87
88    #[test]
89    fn test_annotations_to_dict_some() {
90        let ann = ModuleAnnotations {
91            readonly: true,
92            ..Default::default()
93        };
94        let val = annotations_to_dict(Some(&ann));
95        assert_eq!(val["readonly"], true);
96    }
97
98    #[test]
99    fn test_module_to_dict() {
100        let m = sample_module();
101        let val = module_to_dict(&m);
102        assert_eq!(val["module_id"], "users.get");
103        assert_eq!(val["description"], "Get user");
104        assert_eq!(val["version"], "1.0.0");
105        assert_eq!(val["target"], "app:get_user");
106        assert!(val["tags"].is_array());
107    }
108
109    #[test]
110    fn test_modules_to_dicts() {
111        let modules = vec![sample_module(), sample_module()];
112        let values = modules_to_dicts(&modules);
113        assert_eq!(values.len(), 2);
114    }
115
116    #[test]
117    fn test_module_to_dict_with_annotations() {
118        let mut m = sample_module();
119        m.annotations = Some(ModuleAnnotations {
120            destructive: true,
121            ..Default::default()
122        });
123        let val = module_to_dict(&m);
124        assert_eq!(val["annotations"]["destructive"], true);
125    }
126
127    #[test]
128    fn test_module_to_dict_all_keys() {
129        let val = module_to_dict(&sample_module());
130        let obj = val.as_object().unwrap();
131        let expected_keys: std::collections::HashSet<&str> = [
132            "module_id",
133            "description",
134            "documentation",
135            "tags",
136            "version",
137            "target",
138            "annotations",
139            "suggested_alias",
140            "examples",
141            "metadata",
142            "input_schema",
143            "output_schema",
144            "display",
145            "warnings",
146        ]
147        .into_iter()
148        .collect();
149
150        let actual_keys: std::collections::HashSet<&str> = obj.keys().map(|k| k.as_str()).collect();
151
152        assert_eq!(actual_keys, expected_keys);
153    }
154
155    #[test]
156    fn test_module_to_dict_includes_suggested_alias() {
157        let mut m = sample_module();
158        m.suggested_alias = Some("users.get_alias".into());
159        let val = module_to_dict(&m);
160        assert_eq!(val["suggested_alias"], "users.get_alias");
161    }
162
163    #[test]
164    fn test_module_to_dict_suggested_alias_null_when_absent() {
165        let val = module_to_dict(&sample_module());
166        assert!(val.as_object().unwrap().contains_key("suggested_alias"));
167        assert_eq!(val["suggested_alias"], Value::Null);
168    }
169
170    #[test]
171    fn test_module_to_dict_warnings_empty_default() {
172        let val = module_to_dict(&sample_module());
173        let warnings = val["warnings"].as_array().unwrap();
174        assert!(warnings.is_empty());
175    }
176
177    #[test]
178    fn test_module_to_dict_with_documentation() {
179        let mut m = sample_module();
180        m.documentation = Some("Detailed documentation".into());
181        let val = module_to_dict(&m);
182        assert_eq!(val["documentation"], "Detailed documentation");
183    }
184
185    #[test]
186    fn test_module_to_dict_examples_empty_default() {
187        let val = module_to_dict(&sample_module());
188        let examples = val["examples"].as_array().unwrap();
189        assert!(examples.is_empty());
190    }
191
192    #[test]
193    fn test_modules_to_dicts_empty() {
194        let values = modules_to_dicts(&[]);
195        assert!(values.is_empty());
196    }
197}