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
15pub(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 if schema.is_null() {
26 return json!({});
27 }
28 update_refs(&schema, &OPENAPI_SPEC)
29}
30
31pub(crate) fn update_refs(example: &Value, base: &Value) -> Value {
33 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 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 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 let mut definitions = json!({"definitions": {}});
86
87 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 definitions_obj.insert(key.clone(), val.clone());
99 }
100 }
101 }
102 }
103 }
104
105 let j = merge_json(&updated_schema, &json!( { "$schema": SCHEMA_VERSION } ));
107 json!(merge_json(&j, &definitions))
109}
110
111fn 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 merged.insert(key.clone(), value.clone());
119 }
120 }
121
122 Value::Object(merged)
123}
124
125fn 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 segment == path.last().unwrap_or(&String::new()) {
134 return obj.get(segment).map(|v| json!({ segment: v }));
135 }
136 obj.get(segment)?
138 },
139 _ => return None,
140 };
141 }
142
143 None
144}
145
146fn 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}