cat_gateway/utils/
schema.rs1use std::sync::LazyLock;
4
5use serde_json::{json, Value};
6
7use crate::service::api_spec;
8
9pub(crate) const SCHEMA_VERSION: &str = "https://json-schema.org/draft/2020-12/schema";
11
12pub(crate) static OPENAPI_SPEC: LazyLock<Value> = LazyLock::new(api_spec);
14
15#[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 if schema.is_null() {
28 return json!({});
29 }
30 update_refs(&schema, &OPENAPI_SPEC)
31}
32
33pub(crate) fn update_refs(example: &Value, base: &Value) -> Value {
35 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 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 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 let mut definitions = json!({"definitions": {}});
88
89 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 definitions_obj.insert(key.clone(), val.clone());
101 }
102 }
103 }
104 }
105 }
106
107 let j = merge_json(&updated_schema, &json!( { "$schema": SCHEMA_VERSION } ));
109 json!(merge_json(&j, &definitions))
111}
112
113fn 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 merged.insert(key.clone(), value.clone());
121 }
122 }
123
124 Value::Object(merged)
125}
126
127fn 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 segment == path.last().unwrap_or(&String::new()) {
136 return obj.get(segment).map(|v| json!({ segment: v }));
137 }
138 obj.get(segment)?
140 },
141 _ => return None,
142 };
143 }
144
145 None
146}
147
148fn 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}