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 use crate::validation::schema::validate_json_schema_internal;
58 validate_json_schema_internal(&content).map_err(|e| {
59 ExportError::ValidationError(format!("JSON Schema validation failed: {}", e))
60 })?;
61 }
62
63 Ok(ExportResult {
64 content,
65 format: "json_schema".to_string(),
66 })
67 }
68
69 fn export_model_from_tables(tables: &[Table]) -> serde_json::Value {
70 let mut definitions = serde_json::Map::new();
71 for table in tables {
72 let schema = Self::export_table(table);
73 definitions.insert(table.name.clone(), schema);
74 }
75 let mut root = serde_json::Map::new();
76 root.insert(
77 "$schema".to_string(),
78 serde_json::json!("http://json-schema.org/draft-07/schema#"),
79 );
80 root.insert("type".to_string(), serde_json::json!("object"));
81 root.insert("definitions".to_string(), serde_json::json!(definitions));
82 serde_json::json!(root)
83 }
84
85 pub fn export_table(table: &Table) -> Value {
111 let mut properties = serde_json::Map::new();
112
113 for column in &table.columns {
114 let mut property = serde_json::Map::new();
115
116 let (json_type, format) = Self::map_data_type_to_json_schema(&column.data_type);
118 property.insert("type".to_string(), json!(json_type));
119
120 if let Some(fmt) = format {
121 property.insert("format".to_string(), json!(fmt));
122 }
123
124 if !column.nullable {
125 }
127
128 if !column.description.is_empty() {
129 property.insert("description".to_string(), json!(column.description));
130 }
131
132 if let Some(ref_path) = get_ref_path_from_relationships(column) {
134 property.insert("$ref".to_string(), json!(ref_path));
135 }
136
137 if !column.enum_values.is_empty() {
139 let enum_vals: Vec<Value> = column
140 .enum_values
141 .iter()
142 .map(|v| {
143 if let Ok(num) = v.parse::<i64>() {
145 json!(num)
146 } else if let Ok(num) = v.parse::<f64>() {
147 json!(num)
148 } else if let Ok(b) = v.parse::<bool>() {
149 json!(b)
150 } else if v == "null" {
151 json!(null)
152 } else {
153 json!(v)
154 }
155 })
156 .collect();
157 property.insert("enum".to_string(), json!(enum_vals));
158 }
159
160 Self::export_validation_keywords(&mut property, column);
162
163 properties.insert(column.name.clone(), json!(property));
164 }
165
166 let mut schema = serde_json::Map::new();
167 schema.insert(
168 "$schema".to_string(),
169 json!("http://json-schema.org/draft-07/schema#"),
170 );
171 schema.insert("type".to_string(), json!("object"));
172 schema.insert("title".to_string(), json!(table.name));
173 schema.insert("properties".to_string(), json!(properties));
174
175 let required: Vec<String> = table
177 .columns
178 .iter()
179 .filter(|c| !c.nullable)
180 .map(|c| c.name.clone())
181 .collect();
182
183 if !required.is_empty() {
184 schema.insert("required".to_string(), json!(required));
185 }
186
187 if !table.tags.is_empty() {
189 let tags_array: Vec<String> = table.tags.iter().map(|t| t.to_string()).collect();
190 schema.insert("tags".to_string(), json!(tags_array));
191 }
192
193 json!(schema)
194 }
195
196 pub fn export_model(model: &DataModel, table_ids: Option<&[uuid::Uuid]>) -> Value {
198 let mut definitions = serde_json::Map::new();
199
200 let tables_to_export: Vec<&Table> = if let Some(ids) = table_ids {
201 model
202 .tables
203 .iter()
204 .filter(|t| ids.contains(&t.id))
205 .collect()
206 } else {
207 model.tables.iter().collect()
208 };
209
210 for table in tables_to_export {
211 let schema = Self::export_table(table);
212 definitions.insert(table.name.clone(), schema);
213 }
214
215 let mut root = serde_json::Map::new();
216 root.insert(
217 "$schema".to_string(),
218 json!("http://json-schema.org/draft-07/schema#"),
219 );
220 root.insert("title".to_string(), json!(model.name));
221 root.insert("type".to_string(), json!("object"));
222 root.insert("definitions".to_string(), json!(definitions));
223
224 json!(root)
225 }
226
227 fn map_data_type_to_json_schema(data_type: &str) -> (String, Option<String>) {
229 let dt_lower = data_type.to_lowercase();
230
231 match dt_lower.as_str() {
232 "int" | "integer" | "bigint" | "smallint" | "tinyint" => ("integer".to_string(), None),
233 "float" | "double" | "real" | "decimal" | "numeric" => ("number".to_string(), None),
234 "boolean" | "bool" => ("boolean".to_string(), None),
235 "date" => ("string".to_string(), Some("date".to_string())),
236 "time" => ("string".to_string(), Some("time".to_string())),
237 "timestamp" | "datetime" => ("string".to_string(), Some("date-time".to_string())),
238 "uuid" => ("string".to_string(), Some("uuid".to_string())),
239 "uri" | "url" => ("string".to_string(), Some("uri".to_string())),
240 "email" => ("string".to_string(), Some("email".to_string())),
241 _ => {
242 ("string".to_string(), None)
244 }
245 }
246 }
247
248 fn export_validation_keywords(
250 property: &mut serde_json::Map<String, Value>,
251 column: &crate::models::Column,
252 ) {
253 for rule in &column.quality {
254 let source = rule.get("source").and_then(|v| v.as_str());
257 if source.is_some() && source != Some("json_schema") {
258 continue;
259 }
260
261 if let Some(rule_type) = rule.get("type").and_then(|v| v.as_str()) {
262 match rule_type {
263 "pattern" => {
264 if let Some(pattern) = rule.get("pattern").or_else(|| rule.get("value")) {
265 property.insert("pattern".to_string(), pattern.clone());
266 }
267 }
268 "minimum" => {
269 if let Some(value) = rule.get("value") {
270 property.insert("minimum".to_string(), value.clone());
271 if let Some(exclusive) = rule.get("exclusive")
272 && exclusive.as_bool() == Some(true)
273 {
274 property.insert("exclusiveMinimum".to_string(), json!(true));
275 }
276 }
277 }
278 "maximum" => {
279 if let Some(value) = rule.get("value") {
280 property.insert("maximum".to_string(), value.clone());
281 if let Some(exclusive) = rule.get("exclusive")
282 && exclusive.as_bool() == Some(true)
283 {
284 property.insert("exclusiveMaximum".to_string(), json!(true));
285 }
286 }
287 }
288 "minLength" => {
289 if let Some(value) = rule.get("value") {
290 property.insert("minLength".to_string(), value.clone());
291 }
292 }
293 "maxLength" => {
294 if let Some(value) = rule.get("value") {
295 property.insert("maxLength".to_string(), value.clone());
296 }
297 }
298 "multipleOf" => {
299 if let Some(value) = rule.get("value") {
300 property.insert("multipleOf".to_string(), value.clone());
301 }
302 }
303 "const" => {
304 if let Some(value) = rule.get("value") {
305 property.insert("const".to_string(), value.clone());
306 }
307 }
308 "minItems" => {
309 if let Some(value) = rule.get("value") {
310 property.insert("minItems".to_string(), value.clone());
311 }
312 }
313 "maxItems" => {
314 if let Some(value) = rule.get("value") {
315 property.insert("maxItems".to_string(), value.clone());
316 }
317 }
318 "uniqueItems" => {
319 if let Some(value) = rule.get("value")
320 && value.as_bool() == Some(true)
321 {
322 property.insert("uniqueItems".to_string(), json!(true));
323 }
324 }
325 "minProperties" => {
326 if let Some(value) = rule.get("value") {
327 property.insert("minProperties".to_string(), value.clone());
328 }
329 }
330 "maxProperties" => {
331 if let Some(value) = rule.get("value") {
332 property.insert("maxProperties".to_string(), value.clone());
333 }
334 }
335 "additionalProperties" => {
336 if let Some(value) = rule.get("value") {
337 property.insert("additionalProperties".to_string(), value.clone());
338 }
339 }
340 "format" => {
341 if let Some(value) = rule.get("value").and_then(|v| v.as_str()) {
344 if !property.contains_key("format") {
346 property.insert("format".to_string(), json!(value));
347 }
348 }
349 }
350 "allOf" | "anyOf" | "oneOf" | "not" => {
351 if let Some(value) = rule.get("value") {
353 property.insert(rule_type.to_string(), value.clone());
354 }
355 }
356 _ => {
357 }
360 }
361 }
362 }
363
364 for constraint in &column.constraints {
366 let constraint_upper = constraint.to_uppercase();
368 if constraint_upper.contains("UNIQUE") {
369 } else if constraint_upper.starts_with("CHECK") {
372 }
375 }
376 }
377}