1use super::fields::FieldInfo;
4use super::types::FieldType;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7#[cfg(test)]
8use serde_json::json;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
13pub struct FieldSchema {
14 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
16 pub schema_type: Option<String>,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub format: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub description: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub minimum: Option<f64>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub maximum: Option<f64>,
29 #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
31 pub min_length: Option<usize>,
32 #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
34 pub max_length: Option<usize>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub pattern: Option<String>,
38 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
40 pub enum_values: Option<Vec<String>>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub items: Option<Box<FieldSchema>>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub properties: Option<HashMap<String, FieldSchema>>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub required: Option<Vec<String>>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub default: Option<Value>,
53 #[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
55 pub read_only: Option<bool>,
56 #[serde(rename = "writeOnly", skip_serializing_if = "Option::is_none")]
58 pub write_only: Option<bool>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub nullable: Option<bool>,
62}
63
64pub fn generate_field_schema(field: &FieldInfo) -> FieldSchema {
83 let mut schema = FieldSchema::default();
84
85 match &field.field_type {
87 FieldType::Boolean => {
88 schema.schema_type = Some("boolean".to_string());
89 }
90 FieldType::String => {
91 schema.schema_type = Some("string".to_string());
92 }
93 FieldType::Integer => {
94 schema.schema_type = Some("integer".to_string());
95 schema.format = Some("int64".to_string());
96 }
97 FieldType::Float => {
98 schema.schema_type = Some("number".to_string());
99 schema.format = Some("float".to_string());
100 }
101 FieldType::Decimal => {
102 schema.schema_type = Some("number".to_string());
103 schema.format = Some("double".to_string());
104 }
105 FieldType::Date => {
106 schema.schema_type = Some("string".to_string());
107 schema.format = Some("date".to_string());
108 }
109 FieldType::DateTime => {
110 schema.schema_type = Some("string".to_string());
111 schema.format = Some("date-time".to_string());
112 }
113 FieldType::Time => {
114 schema.schema_type = Some("string".to_string());
115 schema.format = Some("time".to_string());
116 }
117 FieldType::Duration => {
118 schema.schema_type = Some("string".to_string());
119 schema.format = Some("duration".to_string());
120 }
121 FieldType::Email => {
122 schema.schema_type = Some("string".to_string());
123 schema.format = Some("email".to_string());
124 }
125 FieldType::Url => {
126 schema.schema_type = Some("string".to_string());
127 schema.format = Some("uri".to_string());
128 }
129 FieldType::Uuid => {
130 schema.schema_type = Some("string".to_string());
131 schema.format = Some("uuid".to_string());
132 }
133 FieldType::Choice => {
134 schema.schema_type = Some("string".to_string());
135 if let Some(choices) = &field.choices {
136 schema.enum_values = Some(choices.iter().map(|c| c.value.clone()).collect());
137 }
138 }
139 FieldType::MultipleChoice => {
140 schema.schema_type = Some("array".to_string());
141 if let Some(choices) = &field.choices {
142 let item_schema = FieldSchema {
143 schema_type: Some("string".to_string()),
144 enum_values: Some(choices.iter().map(|c| c.value.clone()).collect()),
145 ..Default::default()
146 };
147 schema.items = Some(Box::new(item_schema));
148 }
149 }
150 FieldType::File => {
151 schema.schema_type = Some("string".to_string());
152 schema.format = Some("binary".to_string());
153 }
154 FieldType::Image => {
155 schema.schema_type = Some("string".to_string());
156 schema.format = Some("binary".to_string());
157 }
158 FieldType::List => {
159 schema.schema_type = Some("array".to_string());
160 if let Some(child) = &field.child {
161 schema.items = Some(Box::new(generate_field_schema(child)));
162 }
163 }
164 FieldType::NestedObject => {
165 schema.schema_type = Some("object".to_string());
166 if let Some(children) = &field.children {
167 let mut properties = HashMap::new();
168 let mut required_fields = Vec::new();
169
170 for (name, child_field) in children {
171 properties.insert(name.clone(), generate_field_schema(child_field));
172 if child_field.required {
173 required_fields.push(name.clone());
174 }
175 }
176
177 schema.properties = Some(properties);
178 if !required_fields.is_empty() {
179 schema.required = Some(required_fields);
180 }
181 }
182 }
183 FieldType::Field => {
184 schema.schema_type = Some("string".to_string());
186 }
187 }
188
189 if let Some(min_length) = field.min_length {
191 schema.min_length = Some(min_length);
192 }
193 if let Some(max_length) = field.max_length {
194 schema.max_length = Some(max_length);
195 }
196 if let Some(min_value) = field.min_value {
197 schema.minimum = Some(min_value);
198 }
199 if let Some(max_value) = field.max_value {
200 schema.maximum = Some(max_value);
201 }
202
203 if let Some(help_text) = &field.help_text {
205 schema.description = Some(help_text.clone());
206 } else if let Some(label) = &field.label {
207 schema.description = Some(label.clone());
208 }
209
210 if let Some(default_value) = &field.default_value {
212 schema.default = Some(default_value.clone());
213 }
214
215 if let Some(true) = field.read_only {
217 schema.read_only = Some(true);
218 }
219
220 if let Some(validators) = &field.validators {
222 for validator in validators {
223 if let Some(pattern) = validator.extract_pattern() {
224 schema.pattern = Some(pattern);
225 break;
226 }
227 }
228 }
229
230 schema
231}
232
233pub fn generate_object_schema(fields: &HashMap<String, FieldInfo>) -> FieldSchema {
260 let mut schema = FieldSchema {
261 schema_type: Some("object".to_string()),
262 ..Default::default()
263 };
264
265 let mut properties = HashMap::new();
266 let mut required_fields = Vec::new();
267
268 for (name, field) in fields {
269 properties.insert(name.clone(), generate_field_schema(field));
270 if field.required {
271 required_fields.push(name.clone());
272 }
273 }
274
275 schema.properties = Some(properties);
276 if !required_fields.is_empty() {
277 required_fields.sort(); schema.required = Some(required_fields);
279 }
280
281 schema
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::metadata::fields::FieldInfoBuilder;
288 use crate::metadata::types::ChoiceInfo;
289 use crate::metadata::validators::FieldValidator;
290
291 #[test]
292 fn test_generate_string_schema() {
293 let field = FieldInfoBuilder::new(FieldType::String)
294 .min_length(3)
295 .max_length(50)
296 .build();
297
298 let schema = generate_field_schema(&field);
299 assert_eq!(schema.schema_type, Some("string".to_string()));
300 assert_eq!(schema.min_length, Some(3));
301 assert_eq!(schema.max_length, Some(50));
302 }
303
304 #[test]
305 fn test_generate_integer_schema() {
306 let field = FieldInfoBuilder::new(FieldType::Integer)
307 .min_value(1.0)
308 .max_value(100.0)
309 .build();
310
311 let schema = generate_field_schema(&field);
312 assert_eq!(schema.schema_type, Some("integer".to_string()));
313 assert_eq!(schema.format, Some("int64".to_string()));
314 assert_eq!(schema.minimum, Some(1.0));
315 assert_eq!(schema.maximum, Some(100.0));
316 }
317
318 #[test]
319 fn test_generate_email_schema() {
320 let field = FieldInfoBuilder::new(FieldType::Email).build();
321
322 let schema = generate_field_schema(&field);
323 assert_eq!(schema.schema_type, Some("string".to_string()));
324 assert_eq!(schema.format, Some("email".to_string()));
325 }
326
327 #[test]
328 fn test_generate_datetime_schema() {
329 let field = FieldInfoBuilder::new(FieldType::DateTime).build();
330
331 let schema = generate_field_schema(&field);
332 assert_eq!(schema.schema_type, Some("string".to_string()));
333 assert_eq!(schema.format, Some("date-time".to_string()));
334 }
335
336 #[test]
337 fn test_generate_choice_schema() {
338 let choices = vec![
339 ChoiceInfo {
340 value: "active".to_string(),
341 display_name: "Active".to_string(),
342 },
343 ChoiceInfo {
344 value: "inactive".to_string(),
345 display_name: "Inactive".to_string(),
346 },
347 ];
348
349 let field = FieldInfoBuilder::new(FieldType::Choice)
350 .choices(choices)
351 .build();
352
353 let schema = generate_field_schema(&field);
354 assert_eq!(schema.schema_type, Some("string".to_string()));
355 assert_eq!(
356 schema.enum_values,
357 Some(vec!["active".to_string(), "inactive".to_string()])
358 );
359 }
360
361 #[test]
362 fn test_generate_list_schema() {
363 let child = FieldInfoBuilder::new(FieldType::String)
364 .min_length(1)
365 .build();
366
367 let field = FieldInfoBuilder::new(FieldType::List).child(child).build();
368
369 let schema = generate_field_schema(&field);
370 assert_eq!(schema.schema_type, Some("array".to_string()));
371 assert!(schema.items.is_some());
372
373 let items = schema.items.unwrap();
374 assert_eq!(items.schema_type, Some("string".to_string()));
375 assert_eq!(items.min_length, Some(1));
376 }
377
378 #[test]
379 fn test_generate_nested_object_schema() {
380 let mut children = HashMap::new();
381 children.insert(
382 "name".to_string(),
383 FieldInfoBuilder::new(FieldType::String)
384 .required(true)
385 .build(),
386 );
387 children.insert(
388 "age".to_string(),
389 FieldInfoBuilder::new(FieldType::Integer).build(),
390 );
391
392 let field = FieldInfoBuilder::new(FieldType::NestedObject)
393 .children(children)
394 .build();
395
396 let schema = generate_field_schema(&field);
397 assert_eq!(schema.schema_type, Some("object".to_string()));
398 assert!(schema.properties.is_some());
399
400 let properties = schema.properties.unwrap();
401 assert_eq!(properties.len(), 2);
402 assert!(properties.contains_key("name"));
403 assert!(properties.contains_key("age"));
404
405 assert_eq!(schema.required, Some(vec!["name".to_string()]));
406 }
407
408 #[test]
409 fn test_generate_schema_with_description() {
410 let field = FieldInfoBuilder::new(FieldType::String)
411 .help_text("Enter your username")
412 .build();
413
414 let schema = generate_field_schema(&field);
415 assert_eq!(schema.description, Some("Enter your username".to_string()));
416 }
417
418 #[test]
419 fn test_generate_schema_with_label_fallback() {
420 let field = FieldInfoBuilder::new(FieldType::String)
421 .label("Username")
422 .build();
423
424 let schema = generate_field_schema(&field);
425 assert_eq!(schema.description, Some("Username".to_string()));
426 }
427
428 #[test]
429 fn test_generate_schema_with_default_value() {
430 let field = FieldInfoBuilder::new(FieldType::String)
431 .default_value(json!("default_text"))
432 .build();
433
434 let schema = generate_field_schema(&field);
435 assert_eq!(schema.default, Some(json!("default_text")));
436 }
437
438 #[test]
439 fn test_generate_schema_with_read_only() {
440 let field = FieldInfoBuilder::new(FieldType::Integer)
441 .read_only(true)
442 .build();
443
444 let schema = generate_field_schema(&field);
445 assert_eq!(schema.read_only, Some(true));
446 }
447
448 #[test]
449 fn test_generate_schema_with_regex_pattern() {
450 let validator = FieldValidator {
451 validator_type: "regex".to_string(),
452 options: Some(json!({"pattern": "^[a-zA-Z0-9_]+$"})),
453 message: Some("Invalid format".to_string()),
454 };
455
456 let field = FieldInfoBuilder::new(FieldType::String)
457 .add_validator(validator)
458 .build();
459
460 let schema = generate_field_schema(&field);
461 assert_eq!(schema.pattern, Some("^[a-zA-Z0-9_]+$".to_string()));
462 }
463
464 #[test]
465 fn test_generate_object_schema_basic() {
466 let mut fields = HashMap::new();
467 fields.insert(
468 "name".to_string(),
469 FieldInfoBuilder::new(FieldType::String)
470 .required(true)
471 .build(),
472 );
473 fields.insert(
474 "email".to_string(),
475 FieldInfoBuilder::new(FieldType::Email)
476 .required(true)
477 .build(),
478 );
479 fields.insert(
480 "age".to_string(),
481 FieldInfoBuilder::new(FieldType::Integer).build(),
482 );
483
484 let schema = generate_object_schema(&fields);
485 assert_eq!(schema.schema_type, Some("object".to_string()));
486 assert!(schema.properties.is_some());
487
488 let properties = schema.properties.unwrap();
489 assert_eq!(properties.len(), 3);
490
491 let required = schema.required.unwrap();
492 assert_eq!(required.len(), 2);
493 assert!(required.contains(&"name".to_string()));
494 assert!(required.contains(&"email".to_string()));
495 }
496
497 #[test]
498 fn test_generate_object_schema_empty() {
499 let fields = HashMap::new();
500 let schema = generate_object_schema(&fields);
501
502 assert_eq!(schema.schema_type, Some("object".to_string()));
503 assert!(schema.properties.is_some());
504 assert_eq!(schema.properties.unwrap().len(), 0);
505 assert!(schema.required.is_none());
506 }
507
508 #[test]
509 fn test_schema_serialization() {
510 let field = FieldInfoBuilder::new(FieldType::String)
511 .min_length(3)
512 .max_length(50)
513 .build();
514
515 let schema = generate_field_schema(&field);
516 let json = serde_json::to_string(&schema).unwrap();
517
518 assert!(json.contains("\"type\":\"string\""));
519 assert!(json.contains("\"minLength\":3"));
520 assert!(json.contains("\"maxLength\":50"));
521 }
522}