data_modelling_sdk/export/
json_schema.rs1use super::{ExportError, ExportResult};
4use crate::models::{Column, DataModel, Table};
5use serde_json::{Value, json};
6
7fn get_ref_path_from_relationships(column: &Column) -> Option<String> {
10 column.relationships.iter().find_map(|rel| {
11 if rel.relationship_type == "foreignKey" {
12 Some(format!("#/{}", rel.to))
14 } else {
15 None
16 }
17 })
18}
19
20pub struct JSONSchemaExporter;
22
23impl JSONSchemaExporter {
24 pub fn export(&self, tables: &[Table]) -> Result<ExportResult, ExportError> {
50 let schema = Self::export_model_from_tables(tables);
51 let content = serde_json::to_string_pretty(&schema)
52 .map_err(|e| ExportError::SerializationError(e.to_string()))?;
53
54 #[cfg(feature = "schema-validation")]
56 {
57 #[cfg(feature = "cli")]
58 {
59 use crate::cli::validation::validate_json_schema;
60 validate_json_schema(&content).map_err(|e| {
61 ExportError::ValidationError(format!("JSON Schema validation failed: {}", e))
62 })?;
63 }
64 #[cfg(not(feature = "cli"))]
65 {
66 use jsonschema::Validator;
68 use serde_json::Value;
69
70 let schema_value: Value = serde_json::from_str(&content).map_err(|e| {
71 ExportError::ValidationError(format!("Failed to parse JSON Schema: {}", e))
72 })?;
73
74 Validator::new(&schema_value).map_err(|e| {
76 ExportError::ValidationError(format!("Invalid JSON Schema: {}", e))
77 })?;
78 }
79 }
80
81 Ok(ExportResult {
82 content,
83 format: "json_schema".to_string(),
84 })
85 }
86
87 fn export_model_from_tables(tables: &[Table]) -> serde_json::Value {
88 let mut definitions = serde_json::Map::new();
89 for table in tables {
90 let schema = Self::export_table(table);
91 definitions.insert(table.name.clone(), schema);
92 }
93 let mut root = serde_json::Map::new();
94 root.insert(
95 "$schema".to_string(),
96 serde_json::json!("http://json-schema.org/draft-07/schema#"),
97 );
98 root.insert("type".to_string(), serde_json::json!("object"));
99 root.insert("definitions".to_string(), serde_json::json!(definitions));
100 serde_json::json!(root)
101 }
102
103 pub fn export_table(table: &Table) -> Value {
129 let mut properties = serde_json::Map::new();
130
131 for column in &table.columns {
132 let mut property = serde_json::Map::new();
133
134 let (json_type, format) = Self::map_data_type_to_json_schema(&column.data_type);
136 property.insert("type".to_string(), json!(json_type));
137
138 if let Some(fmt) = format {
139 property.insert("format".to_string(), json!(fmt));
140 }
141
142 if !column.nullable {
143 }
145
146 if !column.description.is_empty() {
147 property.insert("description".to_string(), json!(column.description));
148 }
149
150 if let Some(ref_path) = get_ref_path_from_relationships(column) {
152 property.insert("$ref".to_string(), json!(ref_path));
153 }
154
155 if !column.enum_values.is_empty() {
157 let enum_vals: Vec<Value> = column
158 .enum_values
159 .iter()
160 .map(|v| {
161 if let Ok(num) = v.parse::<i64>() {
163 json!(num)
164 } else if let Ok(num) = v.parse::<f64>() {
165 json!(num)
166 } else if let Ok(b) = v.parse::<bool>() {
167 json!(b)
168 } else if v == "null" {
169 json!(null)
170 } else {
171 json!(v)
172 }
173 })
174 .collect();
175 property.insert("enum".to_string(), json!(enum_vals));
176 }
177
178 Self::export_validation_keywords(&mut property, column);
180
181 properties.insert(column.name.clone(), json!(property));
182 }
183
184 let mut schema = serde_json::Map::new();
185 schema.insert(
186 "$schema".to_string(),
187 json!("http://json-schema.org/draft-07/schema#"),
188 );
189 schema.insert("type".to_string(), json!("object"));
190 schema.insert("title".to_string(), json!(table.name));
191 schema.insert("properties".to_string(), json!(properties));
192
193 let required: Vec<String> = table
195 .columns
196 .iter()
197 .filter(|c| !c.nullable)
198 .map(|c| c.name.clone())
199 .collect();
200
201 if !required.is_empty() {
202 schema.insert("required".to_string(), json!(required));
203 }
204
205 if !table.tags.is_empty() {
207 let tags_array: Vec<String> = table.tags.iter().map(|t| t.to_string()).collect();
208 schema.insert("tags".to_string(), json!(tags_array));
209 }
210
211 json!(schema)
212 }
213
214 pub fn export_model(model: &DataModel, table_ids: Option<&[uuid::Uuid]>) -> Value {
216 let mut definitions = serde_json::Map::new();
217
218 let tables_to_export: Vec<&Table> = if let Some(ids) = table_ids {
219 model
220 .tables
221 .iter()
222 .filter(|t| ids.contains(&t.id))
223 .collect()
224 } else {
225 model.tables.iter().collect()
226 };
227
228 for table in tables_to_export {
229 let schema = Self::export_table(table);
230 definitions.insert(table.name.clone(), schema);
231 }
232
233 let mut root = serde_json::Map::new();
234 root.insert(
235 "$schema".to_string(),
236 json!("http://json-schema.org/draft-07/schema#"),
237 );
238 root.insert("title".to_string(), json!(model.name));
239 root.insert("type".to_string(), json!("object"));
240 root.insert("definitions".to_string(), json!(definitions));
241
242 json!(root)
243 }
244
245 fn map_data_type_to_json_schema(data_type: &str) -> (String, Option<String>) {
247 let dt_lower = data_type.to_lowercase();
248
249 match dt_lower.as_str() {
250 "int" | "integer" | "bigint" | "smallint" | "tinyint" => ("integer".to_string(), None),
251 "float" | "double" | "real" | "decimal" | "numeric" => ("number".to_string(), None),
252 "boolean" | "bool" => ("boolean".to_string(), None),
253 "date" => ("string".to_string(), Some("date".to_string())),
254 "time" => ("string".to_string(), Some("time".to_string())),
255 "timestamp" | "datetime" => ("string".to_string(), Some("date-time".to_string())),
256 "uuid" => ("string".to_string(), Some("uuid".to_string())),
257 "uri" | "url" => ("string".to_string(), Some("uri".to_string())),
258 "email" => ("string".to_string(), Some("email".to_string())),
259 _ => {
260 ("string".to_string(), None)
262 }
263 }
264 }
265
266 fn export_validation_keywords(
268 property: &mut serde_json::Map<String, Value>,
269 column: &crate::models::Column,
270 ) {
271 for rule in &column.quality {
272 let source = rule.get("source").and_then(|v| v.as_str());
275 if source.is_some() && source != Some("json_schema") {
276 continue;
277 }
278
279 if let Some(rule_type) = rule.get("type").and_then(|v| v.as_str()) {
280 match rule_type {
281 "pattern" => {
282 if let Some(pattern) = rule.get("pattern").or_else(|| rule.get("value")) {
283 property.insert("pattern".to_string(), pattern.clone());
284 }
285 }
286 "minimum" => {
287 if let Some(value) = rule.get("value") {
288 property.insert("minimum".to_string(), value.clone());
289 if let Some(exclusive) = rule.get("exclusive")
290 && exclusive.as_bool() == Some(true)
291 {
292 property.insert("exclusiveMinimum".to_string(), json!(true));
293 }
294 }
295 }
296 "maximum" => {
297 if let Some(value) = rule.get("value") {
298 property.insert("maximum".to_string(), value.clone());
299 if let Some(exclusive) = rule.get("exclusive")
300 && exclusive.as_bool() == Some(true)
301 {
302 property.insert("exclusiveMaximum".to_string(), json!(true));
303 }
304 }
305 }
306 "minLength" => {
307 if let Some(value) = rule.get("value") {
308 property.insert("minLength".to_string(), value.clone());
309 }
310 }
311 "maxLength" => {
312 if let Some(value) = rule.get("value") {
313 property.insert("maxLength".to_string(), value.clone());
314 }
315 }
316 "multipleOf" => {
317 if let Some(value) = rule.get("value") {
318 property.insert("multipleOf".to_string(), value.clone());
319 }
320 }
321 "const" => {
322 if let Some(value) = rule.get("value") {
323 property.insert("const".to_string(), value.clone());
324 }
325 }
326 "minItems" => {
327 if let Some(value) = rule.get("value") {
328 property.insert("minItems".to_string(), value.clone());
329 }
330 }
331 "maxItems" => {
332 if let Some(value) = rule.get("value") {
333 property.insert("maxItems".to_string(), value.clone());
334 }
335 }
336 "uniqueItems" => {
337 if let Some(value) = rule.get("value")
338 && value.as_bool() == Some(true)
339 {
340 property.insert("uniqueItems".to_string(), json!(true));
341 }
342 }
343 "minProperties" => {
344 if let Some(value) = rule.get("value") {
345 property.insert("minProperties".to_string(), value.clone());
346 }
347 }
348 "maxProperties" => {
349 if let Some(value) = rule.get("value") {
350 property.insert("maxProperties".to_string(), value.clone());
351 }
352 }
353 "additionalProperties" => {
354 if let Some(value) = rule.get("value") {
355 property.insert("additionalProperties".to_string(), value.clone());
356 }
357 }
358 "format" => {
359 if let Some(value) = rule.get("value").and_then(|v| v.as_str()) {
362 if !property.contains_key("format") {
364 property.insert("format".to_string(), json!(value));
365 }
366 }
367 }
368 "allOf" | "anyOf" | "oneOf" | "not" => {
369 if let Some(value) = rule.get("value") {
371 property.insert(rule_type.to_string(), value.clone());
372 }
373 }
374 _ => {
375 }
378 }
379 }
380 }
381
382 for constraint in &column.constraints {
384 let constraint_upper = constraint.to_uppercase();
386 if constraint_upper.contains("UNIQUE") {
387 } else if constraint_upper.starts_with("CHECK") {
390 }
393 }
394 }
395}