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