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(
35 example: &Value,
36 base: &Value,
37) -> Value {
38 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 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 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 let mut definitions = json!({"definitions": {}});
91
92 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 definitions_obj.insert(key.clone(), val.clone());
104 }
105 }
106 }
107 }
108 }
109
110 let j = merge_json(&updated_schema, &json!( { "$schema": SCHEMA_VERSION } ));
112 json!(merge_json(&j, &definitions))
114}
115
116fn 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 merged.insert(key.clone(), value.clone());
127 }
128 }
129
130 Value::Object(merged)
131}
132
133fn 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 segment == path.last().unwrap_or(&String::new()) {
145 return obj.get(segment).map(|v| json!({ segment: v }));
146 }
147 obj.get(segment)?
149 },
150 _ => return None,
151 };
152 }
153
154 None
155}
156
157fn 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}