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