cat_gateway/utils/
schema.rs

1//! Utility functions for JSON schema processing
2
3use std::sync::LazyLock;
4
5use serde_json::{json, Value};
6
7use crate::service::api_spec;
8
9/// JSON schema version
10pub(crate) const SCHEMA_VERSION: &str = "https://json-schema.org/draft/2020-12/schema";
11
12/// Get the `OpenAPI` specification
13pub(crate) static OPENAPI_SPEC: LazyLock<Value> = LazyLock::new(api_spec);
14
15/// Extract a JSON schema from `schema_name`
16pub(crate) fn extract_json_schema_for(schema_name: &str) -> Value {
17    let schema = OPENAPI_SPEC
18        .get("components")
19        .and_then(|components| components.get("schemas"))
20        .and_then(|schemas| schemas.get(schema_name))
21        .cloned()
22        .unwrap_or_default();
23
24    // JSON schema not found, return an empty JSON object
25    if schema.is_null() {
26        return json!({});
27    }
28    update_refs(&schema, &OPENAPI_SPEC)
29}
30
31/// Function to resolve a `$ref` in the JSON schema
32pub(crate) fn update_refs(example: &Value, base: &Value) -> Value {
33    /// Return the new JSON with modified $refs.
34    /// and the original values of the $refs
35    fn traverse_and_update(example: &Value) -> (Value, Vec<String>) {
36        if let Value::Object(map) = example {
37            let mut new_map = serde_json::Map::new();
38            let mut original_refs = Vec::new();
39
40            for (key, value) in map {
41                match key.as_str() {
42                    "allOf" | "anyOf" | "oneOf" => {
43                        // Iterate over the array and update each item
44                        if let Value::Array(arr) = value {
45                            let new_array: Vec<Value> = arr
46                                .iter()
47                                .map(|item| {
48                                    let (updated_item, refs) = traverse_and_update(item);
49                                    original_refs.extend(refs);
50                                    updated_item
51                                })
52                                .collect();
53                            new_map.insert(key.to_string(), Value::Array(new_array));
54                        }
55                    },
56                    "$ref" => {
57                        // Modify the ref value to a new path, which is
58                        // "#/definitions/{schema_name}"
59                        if let Value::String(ref ref_str) = value {
60                            let original_ref = ref_str.clone();
61                            let parts: Vec<&str> = ref_str.split('/').collect();
62                            if let Some(schema_name) = parts.last() {
63                                let new_ref = format!("#/definitions/{schema_name}");
64                                new_map.insert(key.to_string(), json!(new_ref));
65                                original_refs.push(original_ref);
66                            }
67                        }
68                    },
69                    _ => {
70                        let (updated_value, refs) = traverse_and_update(value);
71                        new_map.insert(key.to_string(), updated_value);
72                        original_refs.extend(refs);
73                    },
74                }
75            }
76
77            (Value::Object(new_map), original_refs)
78        } else {
79            (example.clone(), Vec::new())
80        }
81    }
82
83    let (updated_schema, references) = traverse_and_update(example);
84    // Create new JSON to hold the definitions
85    let mut definitions = json!({"definitions": {}});
86
87    // Traverse the references and retrieve the values
88    for r in references {
89        let path = extract_ref(&r);
90        if let Some(value) = get_nested_value(base, &path) {
91            if let Some(obj) = value.as_object() {
92                for (key, val) in obj {
93                    if let Some(definitions_obj) = definitions
94                        .get_mut("definitions")
95                        .and_then(|v| v.as_object_mut())
96                    {
97                        // Insert the key-value pair into the definitions object
98                        definitions_obj.insert(key.clone(), val.clone());
99                    }
100                }
101            }
102        }
103    }
104
105    // Add schema version
106    let j = merge_json(&updated_schema, &json!( { "$schema": SCHEMA_VERSION } ));
107    // Merge the definitions with the updated schema
108    json!(merge_json(&j, &definitions))
109}
110
111/// Merge 2 JSON objects.
112fn merge_json(json1: &Value, json2: &Value) -> Value {
113    let mut merged = json1.as_object().cloned().unwrap_or_default();
114
115    if let Some(obj2) = json2.as_object() {
116        for (key, value) in obj2 {
117            // Insert or overwrite the definitions
118            merged.insert(key.clone(), value.clone());
119        }
120    }
121
122    Value::Object(merged)
123}
124
125/// Get the nested value from a JSON object.
126fn get_nested_value(base: &Value, path: &[String]) -> Option<Value> {
127    let mut current_value = base;
128
129    for segment in path {
130        current_value = match current_value {
131            Value::Object(ref obj) => {
132                // If this is the last segment, return the key-value as a JSON object
133                if segment == path.last().unwrap_or(&String::new()) {
134                    return obj.get(segment).map(|v| json!({ segment: v }));
135                }
136                // Move to the next nested value
137                obj.get(segment)?
138            },
139            _ => return None,
140        };
141    }
142
143    None
144}
145
146/// Extract the reference parts from a $ref string
147fn extract_ref(ref_str: &str) -> Vec<String> {
148    ref_str
149        .split('/')
150        .filter_map(|part| {
151            match part.trim() {
152                "" | "#" => None,
153                trimmed => Some(trimmed.to_string()),
154            }
155        })
156        .collect()
157}
158
159#[cfg(test)]
160mod test {
161    use serde_json::{json, Value};
162
163    use crate::utils::schema::{extract_json_schema_for, update_refs};
164
165    #[test]
166    fn test_update_refs() {
167        let base_json: Value = json!({
168            "components": {
169                "schemas": {
170                    "Example": {
171                        "type": "object",
172                        "properties": {
173                            "data": {
174                                "allOf": [
175                                    {
176                                        "$ref": "#/components/schemas/Props"
177                                    }
178                                ]
179                            }
180                        },
181                        "required": ["data"],
182                        "description": "Example schema"
183                    },
184                    "Props": {
185                        "type": "object",
186                        "properties": {
187                            "prop1": {
188                                "type": "string",
189                                "description": "Property 1"
190                            },
191                            "prop2": {
192                                "type": "string",
193                                "description": "Property 2"
194                            },
195                            "prop3": {
196                                "type": "string",
197                                "description": "Property 3"
198                            }
199                        },
200                        "required": ["prop1"]
201                    }
202                }
203            }
204        });
205
206        let example_json: Value = json!({
207            "type": "object",
208            "properties": {
209                "data": {
210                    "allOf": [
211                        {
212                            "$ref": "#/components/schemas/Props"
213                        }
214                    ]
215                }
216            },
217            "required": ["data"],
218            "description": "Example schema"
219
220        });
221
222        let schema = update_refs(&example_json, &base_json);
223        assert!(schema.get("definitions").unwrap().get("Props").is_some());
224    }
225
226    #[test]
227    fn test_extract_json_schema_for_frontend_config() {
228        let schema = extract_json_schema_for("FrontendConfig");
229        println!("{schema}");
230        assert!(schema.get("type").is_some());
231        assert!(schema.get("properties").is_some());
232        assert!(schema.get("description").is_some());
233        assert!(schema.get("definitions").is_some());
234        assert!(schema.get("$schema").is_some());
235    }
236
237    #[test]
238    fn test_extract_json_schema_for_frontend_config_no_data() {
239        let schema = extract_json_schema_for("test");
240        assert!(schema.is_object());
241    }
242}