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_value(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 fields.
27pub fn module_to_value(module: &ScannedModule) -> Value {
28    let examples = serde_json::to_value(&module.examples).unwrap_or_else(|e| {
29        warn!(
30            module_id = %module.module_id,
31            "Failed to serialize examples: {e}"
32        );
33        json!([])
34    });
35
36    json!({
37        "module_id": module.module_id,
38        "description": module.description,
39        "documentation": module.documentation,
40        "tags": module.tags,
41        "version": module.version,
42        "target": module.target,
43        "annotations": annotations_to_value(module.annotations.as_ref()),
44        "examples": examples,
45        "metadata": module.metadata,
46        "input_schema": module.input_schema,
47        "output_schema": module.output_schema,
48        "warnings": module.warnings,
49    })
50}
51
52/// Batch-convert a list of ScannedModules to Values.
53pub fn modules_to_values(modules: &[ScannedModule]) -> Vec<Value> {
54    modules.iter().map(module_to_value).collect()
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use serde_json::json;
61
62    fn sample_module() -> ScannedModule {
63        ScannedModule::new(
64            "users.get".into(),
65            "Get user".into(),
66            json!({"type": "object"}),
67            json!({"type": "object"}),
68            vec!["users".into()],
69            "app:get_user".into(),
70        )
71    }
72
73    #[test]
74    fn test_annotations_to_value_none() {
75        assert_eq!(annotations_to_value(None), Value::Null);
76    }
77
78    #[test]
79    fn test_annotations_to_value_some() {
80        let ann = ModuleAnnotations {
81            readonly: true,
82            ..Default::default()
83        };
84        let val = annotations_to_value(Some(&ann));
85        assert_eq!(val["readonly"], true);
86    }
87
88    #[test]
89    fn test_module_to_value() {
90        let m = sample_module();
91        let val = module_to_value(&m);
92        assert_eq!(val["module_id"], "users.get");
93        assert_eq!(val["description"], "Get user");
94        assert_eq!(val["version"], "1.0.0");
95        assert_eq!(val["target"], "app:get_user");
96        assert!(val["tags"].is_array());
97    }
98
99    #[test]
100    fn test_modules_to_values() {
101        let modules = vec![sample_module(), sample_module()];
102        let values = modules_to_values(&modules);
103        assert_eq!(values.len(), 2);
104    }
105
106    #[test]
107    fn test_module_to_value_with_annotations() {
108        let mut m = sample_module();
109        m.annotations = Some(ModuleAnnotations {
110            destructive: true,
111            ..Default::default()
112        });
113        let val = module_to_value(&m);
114        assert_eq!(val["annotations"]["destructive"], true);
115    }
116
117    #[test]
118    fn test_module_to_value_all_keys() {
119        let val = module_to_value(&sample_module());
120        let obj = val.as_object().unwrap();
121        let expected_keys: std::collections::HashSet<&str> = [
122            "module_id",
123            "description",
124            "documentation",
125            "tags",
126            "version",
127            "target",
128            "annotations",
129            "examples",
130            "metadata",
131            "input_schema",
132            "output_schema",
133            "warnings",
134        ]
135        .into_iter()
136        .collect();
137
138        let actual_keys: std::collections::HashSet<&str> = obj.keys().map(|k| k.as_str()).collect();
139
140        assert_eq!(actual_keys, expected_keys);
141    }
142
143    #[test]
144    fn test_module_to_value_warnings_empty_default() {
145        let val = module_to_value(&sample_module());
146        let warnings = val["warnings"].as_array().unwrap();
147        assert!(warnings.is_empty());
148    }
149
150    #[test]
151    fn test_module_to_value_with_documentation() {
152        let mut m = sample_module();
153        m.documentation = Some("Detailed documentation".into());
154        let val = module_to_value(&m);
155        assert_eq!(val["documentation"], "Detailed documentation");
156    }
157
158    #[test]
159    fn test_module_to_value_examples_empty_default() {
160        let val = module_to_value(&sample_module());
161        let examples = val["examples"].as_array().unwrap();
162        assert!(examples.is_empty());
163    }
164
165    #[test]
166    fn test_modules_to_values_empty() {
167        let values = modules_to_values(&[]);
168        assert!(values.is_empty());
169    }
170}