1use serde::{Deserialize, Serialize};
2use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
3use std::path::PathBuf;
4use std::rc::Rc;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub enum Schema {
9 String,
10 Number,
11 Integer,
12 Boolean,
13 Null,
14 Any,
15 Array(Box<Schema>),
16 Object(Vec<Field>),
17 Enum(Vec<String>),
18 Union(Vec<Schema>),
19 Tuple(Vec<Schema>),
20 Ref(String),
21 Record {
22 key: Box<Schema>,
23 value: Box<Schema>,
24 },
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28pub struct Field {
29 pub name: String,
30 pub schema: Schema,
31 pub required: bool,
32 pub constraints: Constraints,
33}
34
35#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
36pub struct Constraints {
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub min: Option<f64>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub max: Option<f64>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub min_len: Option<usize>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub max_len: Option<usize>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub one_of: Option<Vec<String>>,
47}
48
49impl Field {
50 pub fn new(name: impl Into<String>, schema: Schema) -> Self {
51 Self {
52 name: name.into(),
53 schema,
54 required: true,
55 constraints: Constraints::default(),
56 }
57 }
58
59 pub fn optional(name: impl Into<String>, schema: Schema) -> Self {
60 Self {
61 name: name.into(),
62 schema,
63 required: false,
64 constraints: Constraints::default(),
65 }
66 }
67}
68
69impl Schema {
70 pub fn type_name(&self) -> &'static str {
71 match self {
72 Schema::String => "string",
73 Schema::Number => "number",
74 Schema::Integer => "integer",
75 Schema::Boolean => "boolean",
76 Schema::Null => "nil",
77 Schema::Any => "any",
78 Schema::Array(_) => "table",
79 Schema::Object(_) => "table",
80 Schema::Enum(_) => "string",
81 Schema::Union(_) => "any",
82 Schema::Tuple(_) => "table",
83 Schema::Ref(_) => "table",
84 Schema::Record { .. } => "table",
85 }
86 }
87}
88
89pub trait SchemaBridge {
90 fn to_ts() -> String;
91 fn to_schema() -> Schema;
92}
93
94impl SchemaBridge for String {
96 fn to_ts() -> String {
97 "string".to_string()
98 }
99 fn to_schema() -> Schema {
100 Schema::String
101 }
102}
103
104impl SchemaBridge for i32 {
105 fn to_ts() -> String {
106 "number".to_string()
107 }
108 fn to_schema() -> Schema {
109 Schema::Integer
110 }
111}
112
113impl SchemaBridge for f64 {
114 fn to_ts() -> String {
115 "number".to_string()
116 }
117 fn to_schema() -> Schema {
118 Schema::Number
119 }
120}
121
122impl SchemaBridge for bool {
123 fn to_ts() -> String {
124 "boolean".to_string()
125 }
126 fn to_schema() -> Schema {
127 Schema::Boolean
128 }
129}
130
131impl SchemaBridge for i8 {
132 fn to_ts() -> String {
133 "number".to_string()
134 }
135 fn to_schema() -> Schema {
136 Schema::Integer
137 }
138}
139
140impl SchemaBridge for i16 {
141 fn to_ts() -> String {
142 "number".to_string()
143 }
144 fn to_schema() -> Schema {
145 Schema::Integer
146 }
147}
148
149impl SchemaBridge for i64 {
150 fn to_ts() -> String {
151 "number".to_string()
152 }
153 fn to_schema() -> Schema {
154 Schema::Integer
155 }
156}
157
158impl SchemaBridge for i128 {
159 fn to_ts() -> String {
160 "number".to_string()
161 }
162 fn to_schema() -> Schema {
163 Schema::Integer
164 }
165}
166
167impl SchemaBridge for isize {
168 fn to_ts() -> String {
169 "number".to_string()
170 }
171 fn to_schema() -> Schema {
172 Schema::Integer
173 }
174}
175
176impl SchemaBridge for u8 {
177 fn to_ts() -> String {
178 "number".to_string()
179 }
180 fn to_schema() -> Schema {
181 Schema::Integer
182 }
183}
184
185impl SchemaBridge for u16 {
186 fn to_ts() -> String {
187 "number".to_string()
188 }
189 fn to_schema() -> Schema {
190 Schema::Integer
191 }
192}
193
194impl SchemaBridge for u32 {
195 fn to_ts() -> String {
196 "number".to_string()
197 }
198 fn to_schema() -> Schema {
199 Schema::Integer
200 }
201}
202
203impl SchemaBridge for u64 {
204 fn to_ts() -> String {
205 "number".to_string()
206 }
207 fn to_schema() -> Schema {
208 Schema::Integer
209 }
210}
211
212impl SchemaBridge for u128 {
213 fn to_ts() -> String {
214 "number".to_string()
215 }
216 fn to_schema() -> Schema {
217 Schema::Integer
218 }
219}
220
221impl SchemaBridge for usize {
222 fn to_ts() -> String {
223 "number".to_string()
224 }
225 fn to_schema() -> Schema {
226 Schema::Integer
227 }
228}
229
230impl SchemaBridge for f32 {
231 fn to_ts() -> String {
232 "number".to_string()
233 }
234 fn to_schema() -> Schema {
235 Schema::Number
236 }
237}
238
239impl SchemaBridge for char {
241 fn to_ts() -> String {
242 "string".to_string()
243 }
244 fn to_schema() -> Schema {
245 Schema::String
246 }
247}
248
249impl SchemaBridge for () {
251 fn to_ts() -> String {
252 "null".to_string()
253 }
254 fn to_schema() -> Schema {
255 Schema::Null
256 }
257}
258
259impl<T: SchemaBridge> SchemaBridge for Option<T> {
260 fn to_ts() -> String {
261 format!("{} | null", T::to_ts())
262 }
263 fn to_schema() -> Schema {
264 Schema::Union(vec![T::to_schema(), Schema::Null])
265 }
266}
267
268impl<T: SchemaBridge> SchemaBridge for Vec<T> {
269 fn to_ts() -> String {
270 format!("{}[]", T::to_ts())
271 }
272 fn to_schema() -> Schema {
273 Schema::Array(Box::new(T::to_schema()))
274 }
275}
276
277impl SchemaBridge for PathBuf {
278 fn to_ts() -> String {
279 "string".to_string()
280 }
281 fn to_schema() -> Schema {
282 Schema::String
283 }
284}
285
286impl<K, V> SchemaBridge for HashMap<K, V>
287where
288 K: SchemaBridge,
289 V: SchemaBridge,
290{
291 fn to_ts() -> String {
292 format!("Record<{}, {}>", K::to_ts(), V::to_ts())
293 }
294 fn to_schema() -> Schema {
295 Schema::Record {
296 key: Box::new(K::to_schema()),
297 value: Box::new(V::to_schema()),
298 }
299 }
300}
301
302impl<K, V> SchemaBridge for BTreeMap<K, V>
303where
304 K: SchemaBridge,
305 V: SchemaBridge,
306{
307 fn to_ts() -> String {
308 format!("Record<{}, {}>", K::to_ts(), V::to_ts())
309 }
310 fn to_schema() -> Schema {
311 Schema::Record {
312 key: Box::new(K::to_schema()),
313 value: Box::new(V::to_schema()),
314 }
315 }
316}
317
318impl<T: SchemaBridge> SchemaBridge for HashSet<T> {
319 fn to_ts() -> String {
320 format!("{}[]", T::to_ts())
321 }
322 fn to_schema() -> Schema {
323 Schema::Array(Box::new(T::to_schema()))
324 }
325}
326
327impl<T: SchemaBridge> SchemaBridge for BTreeSet<T> {
328 fn to_ts() -> String {
329 format!("{}[]", T::to_ts())
330 }
331 fn to_schema() -> Schema {
332 Schema::Array(Box::new(T::to_schema()))
333 }
334}
335
336impl<T: SchemaBridge> SchemaBridge for Box<T> {
337 fn to_ts() -> String {
338 T::to_ts()
339 }
340 fn to_schema() -> Schema {
341 T::to_schema()
342 }
343}
344
345impl<T: SchemaBridge> SchemaBridge for Rc<T> {
346 fn to_ts() -> String {
347 T::to_ts()
348 }
349 fn to_schema() -> Schema {
350 T::to_schema()
351 }
352}
353
354impl<T: SchemaBridge> SchemaBridge for Arc<T> {
355 fn to_ts() -> String {
356 T::to_ts()
357 }
358 fn to_schema() -> Schema {
359 T::to_schema()
360 }
361}
362
363impl<T: SchemaBridge, E: SchemaBridge> SchemaBridge for Result<T, E> {
364 fn to_ts() -> String {
365 format!("{} | {}", T::to_ts(), E::to_ts())
366 }
367 fn to_schema() -> Schema {
368 Schema::Union(vec![T::to_schema(), E::to_schema()])
369 }
370}
371
372impl<T: SchemaBridge> SchemaBridge for (T,) {
374 fn to_ts() -> String {
375 format!("[{}]", T::to_ts())
376 }
377 fn to_schema() -> Schema {
378 Schema::Tuple(vec![T::to_schema()])
379 }
380}
381
382impl<T1: SchemaBridge, T2: SchemaBridge> SchemaBridge for (T1, T2) {
383 fn to_ts() -> String {
384 format!("[{}, {}]", T1::to_ts(), T2::to_ts())
385 }
386 fn to_schema() -> Schema {
387 Schema::Tuple(vec![T1::to_schema(), T2::to_schema()])
388 }
389}
390
391impl<T1: SchemaBridge, T2: SchemaBridge, T3: SchemaBridge> SchemaBridge for (T1, T2, T3) {
392 fn to_ts() -> String {
393 format!("[{}, {}, {}]", T1::to_ts(), T2::to_ts(), T3::to_ts())
394 }
395 fn to_schema() -> Schema {
396 Schema::Tuple(vec![T1::to_schema(), T2::to_schema(), T3::to_schema()])
397 }
398}
399
400impl<T1: SchemaBridge, T2: SchemaBridge, T3: SchemaBridge, T4: SchemaBridge> SchemaBridge
401 for (T1, T2, T3, T4)
402{
403 fn to_ts() -> String {
404 format!(
405 "[{}, {}, {}, {}]",
406 T1::to_ts(),
407 T2::to_ts(),
408 T3::to_ts(),
409 T4::to_ts()
410 )
411 }
412 fn to_schema() -> Schema {
413 Schema::Tuple(vec![
414 T1::to_schema(),
415 T2::to_schema(),
416 T3::to_schema(),
417 T4::to_schema(),
418 ])
419 }
420}
421
422impl<T1: SchemaBridge, T2: SchemaBridge, T3: SchemaBridge, T4: SchemaBridge, T5: SchemaBridge>
423 SchemaBridge for (T1, T2, T3, T4, T5)
424{
425 fn to_ts() -> String {
426 format!(
427 "[{}, {}, {}, {}, {}]",
428 T1::to_ts(),
429 T2::to_ts(),
430 T3::to_ts(),
431 T4::to_ts(),
432 T5::to_ts()
433 )
434 }
435 fn to_schema() -> Schema {
436 Schema::Tuple(vec![
437 T1::to_schema(),
438 T2::to_schema(),
439 T3::to_schema(),
440 T4::to_schema(),
441 T5::to_schema(),
442 ])
443 }
444}
445
446impl<
447 T1: SchemaBridge,
448 T2: SchemaBridge,
449 T3: SchemaBridge,
450 T4: SchemaBridge,
451 T5: SchemaBridge,
452 T6: SchemaBridge,
453 > SchemaBridge for (T1, T2, T3, T4, T5, T6)
454{
455 fn to_ts() -> String {
456 format!(
457 "[{}, {}, {}, {}, {}, {}]",
458 T1::to_ts(),
459 T2::to_ts(),
460 T3::to_ts(),
461 T4::to_ts(),
462 T5::to_ts(),
463 T6::to_ts()
464 )
465 }
466 fn to_schema() -> Schema {
467 Schema::Tuple(vec![
468 T1::to_schema(),
469 T2::to_schema(),
470 T3::to_schema(),
471 T4::to_schema(),
472 T5::to_schema(),
473 T6::to_schema(),
474 ])
475 }
476}
477
478pub fn generate_ts_file(types: Vec<(&str, String)>) -> String {
480 let mut content = String::new();
481 content.push_str("// This file is auto-generated by schema-bridge\n\n");
482
483 for (name, ts_def) in types {
484 content.push_str(&format!("export type {} = {};\n\n", name, ts_def));
485 }
486
487 content
488}
489
490pub fn export_to_file(types: Vec<(&str, String)>, path: &str) -> std::io::Result<()> {
492 let content = generate_ts_file(types);
493 std::fs::write(path, content)
494}
495
496#[macro_export]
498macro_rules! export_types {
499 ($path:expr, $($name:ident),+ $(,)?) => {{
500 let types = vec![
501 $((stringify!($name), $name::to_ts()),)+
502 ];
503 $crate::export_to_file(types, $path)
504 }};
505}
506
507#[cfg(feature = "mlua")]
510mod lua {
511 use super::*;
512 use mlua::prelude::*;
513
514 impl Schema {
515 pub fn to_lua_table(&self, lua: &Lua) -> LuaResult<LuaValue> {
521 match self {
522 Schema::Object(fields) => {
523 let t = lua.create_table()?;
524 for field in fields {
525 let value = field_to_lua_value(lua, field)?;
526 t.set(field.name.as_str(), value)?;
527 }
528 Ok(LuaValue::Table(t))
529 }
530 _ => {
531 Ok(LuaValue::String(lua.create_string(self.type_name())?))
533 }
534 }
535 }
536 }
537
538 fn field_to_lua_value(lua: &Lua, field: &Field) -> LuaResult<LuaValue> {
539 let has_constraints = field.constraints.min.is_some()
540 || field.constraints.max.is_some()
541 || field.constraints.min_len.is_some()
542 || field.constraints.max_len.is_some()
543 || field.constraints.one_of.is_some();
544
545 if !field.required && !has_constraints {
548 return Ok(LuaValue::String(
549 lua.create_string(field.schema.type_name())?,
550 ));
551 }
552
553 let t = lua.create_table()?;
555 t.set("type", field.schema.type_name())?;
556
557 if field.required {
558 t.set("required", true)?;
559 }
560
561 if let Some(min) = field.constraints.min {
562 t.set("min", min)?;
563 }
564 if let Some(max) = field.constraints.max {
565 t.set("max", max)?;
566 }
567 if let Some(min_len) = field.constraints.min_len {
568 t.set("min_len", min_len as i64)?;
569 }
570 if let Some(max_len) = field.constraints.max_len {
571 t.set("max_len", max_len as i64)?;
572 }
573 if let Some(ref one_of) = field.constraints.one_of {
574 let arr = lua.create_table()?;
575 for (i, val) in one_of.iter().enumerate() {
576 arr.set(i + 1, val.as_str())?;
577 }
578 t.set("one_of", arr)?;
579 }
580
581 Ok(LuaValue::Table(t))
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
590 fn test_string_to_ts() {
591 assert_eq!(String::to_ts(), "string");
592 }
593
594 #[test]
595 fn test_i32_to_ts() {
596 assert_eq!(i32::to_ts(), "number");
597 }
598
599 #[test]
600 fn test_f64_to_ts() {
601 assert_eq!(f64::to_ts(), "number");
602 }
603
604 #[test]
605 fn test_bool_to_ts() {
606 assert_eq!(bool::to_ts(), "boolean");
607 }
608
609 #[test]
610 fn test_option_to_ts() {
611 assert_eq!(Option::<String>::to_ts(), "string | null");
612 assert_eq!(Option::<i32>::to_ts(), "number | null");
613 }
614
615 #[test]
616 fn test_vec_to_ts() {
617 assert_eq!(Vec::<String>::to_ts(), "string[]");
618 assert_eq!(Vec::<i32>::to_ts(), "number[]");
619 }
620
621 #[test]
622 fn test_nested_vec() {
623 assert_eq!(Vec::<Vec::<String>>::to_ts(), "string[][]");
624 }
625
626 #[test]
627 fn test_optional_vec() {
628 assert_eq!(Option::<Vec::<String>>::to_ts(), "string[] | null");
629 }
630
631 #[test]
632 fn test_generate_ts_file() {
633 let types = vec![
634 ("User", "{ name: string; age: number; }".to_string()),
635 ("Status", "'Active' | 'Inactive'".to_string()),
636 ];
637
638 let result = generate_ts_file(types);
639
640 assert!(result.contains("// This file is auto-generated by schema-bridge"));
641 assert!(result.contains("export type User = { name: string; age: number; };"));
642 assert!(result.contains("export type Status = 'Active' | 'Inactive';"));
643 }
644
645 #[test]
646 fn test_schema_enum() {
647 let schema = Schema::String;
648 assert_eq!(schema, Schema::String);
649
650 let schema = Schema::Array(Box::new(Schema::Number));
651 assert!(matches!(schema, Schema::Array(_)));
652 }
653
654 #[test]
655 fn test_integer_schema() {
656 assert_eq!(i32::to_schema(), Schema::Integer);
657 assert_eq!(u64::to_schema(), Schema::Integer);
658 assert_eq!(i8::to_schema(), Schema::Integer);
659 assert_eq!(usize::to_schema(), Schema::Integer);
660 }
661
662 #[test]
663 fn test_float_schema() {
664 assert_eq!(f32::to_schema(), Schema::Number);
665 assert_eq!(f64::to_schema(), Schema::Number);
666 }
667
668 #[test]
669 fn test_pathbuf_to_ts() {
670 assert_eq!(PathBuf::to_ts(), "string");
671 }
672
673 #[test]
674 fn test_pathbuf_to_schema() {
675 assert_eq!(PathBuf::to_schema(), Schema::String);
676 }
677
678 #[test]
679 fn test_hashmap_to_ts() {
680 assert_eq!(HashMap::<String, i32>::to_ts(), "Record<string, number>");
681 assert_eq!(HashMap::<String, String>::to_ts(), "Record<string, string>");
682 }
683
684 #[test]
685 fn test_hashmap_to_schema() {
686 let schema = HashMap::<String, i32>::to_schema();
687 assert!(matches!(schema, Schema::Record { .. }));
688 if let Schema::Record { key, value } = schema {
689 assert_eq!(*key, Schema::String);
690 assert_eq!(*value, Schema::Integer);
691 }
692 }
693
694 #[test]
695 fn test_nested_hashmap() {
696 assert_eq!(
697 HashMap::<String, Vec::<String>>::to_ts(),
698 "Record<string, string[]>"
699 );
700 }
701
702 #[test]
703 fn test_optional_hashmap() {
704 assert_eq!(
705 Option::<HashMap::<String, i32>>::to_ts(),
706 "Record<string, number> | null"
707 );
708 }
709
710 #[test]
712 fn test_numeric_types() {
713 assert_eq!(i8::to_ts(), "number");
714 assert_eq!(i16::to_ts(), "number");
715 assert_eq!(i64::to_ts(), "number");
716 assert_eq!(i128::to_ts(), "number");
717 assert_eq!(isize::to_ts(), "number");
718 assert_eq!(u8::to_ts(), "number");
719 assert_eq!(u16::to_ts(), "number");
720 assert_eq!(u32::to_ts(), "number");
721 assert_eq!(u64::to_ts(), "number");
722 assert_eq!(u128::to_ts(), "number");
723 assert_eq!(usize::to_ts(), "number");
724 assert_eq!(f32::to_ts(), "number");
725 }
726
727 #[test]
728 fn test_char_to_ts() {
729 assert_eq!(char::to_ts(), "string");
730 assert_eq!(char::to_schema(), Schema::String);
731 }
732
733 #[test]
734 fn test_unit_to_ts() {
735 assert_eq!(<()>::to_ts(), "null");
736 assert_eq!(<()>::to_schema(), Schema::Null);
737 }
738
739 #[test]
741 fn test_btreemap_to_ts() {
742 assert_eq!(BTreeMap::<String, i32>::to_ts(), "Record<string, number>");
743 }
744
745 #[test]
747 fn test_hashset_to_ts() {
748 assert_eq!(HashSet::<String>::to_ts(), "string[]");
749 assert_eq!(HashSet::<i32>::to_ts(), "number[]");
750 }
751
752 #[test]
753 fn test_btreeset_to_ts() {
754 assert_eq!(BTreeSet::<String>::to_ts(), "string[]");
755 assert_eq!(BTreeSet::<i32>::to_ts(), "number[]");
756 }
757
758 #[test]
760 fn test_box_to_ts() {
761 assert_eq!(Box::<String>::to_ts(), "string");
762 assert_eq!(Box::<i32>::to_ts(), "number");
763 assert_eq!(Box::<String>::to_schema(), Schema::String);
764 }
765
766 #[test]
767 fn test_rc_to_ts() {
768 assert_eq!(Rc::<String>::to_ts(), "string");
769 assert_eq!(Rc::<i32>::to_ts(), "number");
770 }
771
772 #[test]
773 fn test_arc_to_ts() {
774 assert_eq!(Arc::<String>::to_ts(), "string");
775 assert_eq!(Arc::<i32>::to_ts(), "number");
776 }
777
778 #[test]
780 fn test_result_to_ts() {
781 assert_eq!(Result::<String, String>::to_ts(), "string | string");
782 assert_eq!(Result::<i32, String>::to_ts(), "number | string");
783 }
784
785 #[test]
786 fn test_result_to_schema() {
787 let schema = Result::<String, i32>::to_schema();
788 assert!(matches!(schema, Schema::Union(_)));
789 if let Schema::Union(types) = schema {
790 assert_eq!(types.len(), 2);
791 assert_eq!(types[0], Schema::String);
792 assert_eq!(types[1], Schema::Integer);
793 }
794 }
795
796 #[test]
798 fn test_tuple_1() {
799 assert_eq!(<(String,)>::to_ts(), "[string]");
800 let schema = <(String,)>::to_schema();
801 assert!(matches!(schema, Schema::Tuple(_)));
802 }
803
804 #[test]
805 fn test_tuple_2() {
806 assert_eq!(<(String, i32)>::to_ts(), "[string, number]");
807 }
808
809 #[test]
810 fn test_tuple_3() {
811 assert_eq!(<(String, i32, bool)>::to_ts(), "[string, number, boolean]");
812 }
813
814 #[test]
815 fn test_tuple_4() {
816 assert_eq!(
817 <(String, i32, bool, f64)>::to_ts(),
818 "[string, number, boolean, number]"
819 );
820 }
821
822 #[test]
823 fn test_tuple_schema() {
824 let schema = <(String, i32)>::to_schema();
825 if let Schema::Tuple(types) = schema {
826 assert_eq!(types.len(), 2);
827 assert_eq!(types[0], Schema::String);
828 assert_eq!(types[1], Schema::Integer);
829 } else {
830 panic!("Expected Tuple schema");
831 }
832 }
833
834 #[test]
836 fn test_complex_types() {
837 assert_eq!(Option::<Box::<String>>::to_ts(), "string | null");
838 assert_eq!(Vec::<Arc::<String>>::to_ts(), "string[]");
839 assert_eq!(
840 HashMap::<String, Vec::<i32>>::to_ts(),
841 "Record<string, number[]>"
842 );
843 }
844
845 #[test]
847 fn test_field_new() {
848 let f = Field::new("name", Schema::String);
849 assert_eq!(f.name, "name");
850 assert!(f.required);
851 assert_eq!(f.constraints, Constraints::default());
852 }
853
854 #[test]
855 fn test_field_optional() {
856 let f = Field::optional("email", Schema::String);
857 assert!(!f.required);
858 }
859
860 #[test]
861 fn test_schema_type_name() {
862 assert_eq!(Schema::String.type_name(), "string");
863 assert_eq!(Schema::Number.type_name(), "number");
864 assert_eq!(Schema::Integer.type_name(), "integer");
865 assert_eq!(Schema::Boolean.type_name(), "boolean");
866 assert_eq!(Schema::Null.type_name(), "nil");
867 assert_eq!(Schema::Any.type_name(), "any");
868 }
869
870 #[test]
871 fn test_object_schema() {
872 let schema = Schema::Object(vec![
873 Field::new("name", Schema::String),
874 Field::optional("age", Schema::Integer),
875 ]);
876 if let Schema::Object(fields) = &schema {
877 assert_eq!(fields.len(), 2);
878 assert_eq!(fields[0].name, "name");
879 assert!(fields[0].required);
880 assert_eq!(fields[1].name, "age");
881 assert!(!fields[1].required);
882 } else {
883 panic!("Expected Object schema");
884 }
885 }
886
887 #[test]
888 fn test_constraints_with_values() {
889 let c = Constraints {
890 min: Some(0.0),
891 max: Some(100.0),
892 min_len: None,
893 max_len: Some(255),
894 one_of: None,
895 };
896 assert_eq!(c.min, Some(0.0));
897 assert_eq!(c.max, Some(100.0));
898 assert_eq!(c.max_len, Some(255));
899 }
900}
901
902#[cfg(all(test, feature = "mlua"))]
903mod lua_tests {
904 use super::*;
905 use mlua::prelude::*;
906
907 #[test]
908 fn to_lua_table_simple_object() {
909 let lua = Lua::new();
910 let schema = Schema::Object(vec![
911 Field::new("name", Schema::String),
912 Field::optional("bio", Schema::String),
913 ]);
914
915 let value = schema.to_lua_table(&lua).unwrap();
916 let table = value.as_table().unwrap();
917
918 let name_val: LuaTable = table.get("name").unwrap();
920 let name_type: String = name_val.get("type").unwrap();
921 assert_eq!(name_type, "string");
922 let name_req: bool = name_val.get("required").unwrap();
923 assert!(name_req);
924
925 let bio_val: String = table.get("bio").unwrap();
927 assert_eq!(bio_val, "string");
928 }
929
930 #[test]
931 fn to_lua_table_with_constraints() {
932 let lua = Lua::new();
933 let schema = Schema::Object(vec![Field {
934 name: "age".into(),
935 schema: Schema::Integer,
936 required: true,
937 constraints: Constraints {
938 min: Some(0.0),
939 max: Some(150.0),
940 ..Default::default()
941 },
942 }]);
943
944 let value = schema.to_lua_table(&lua).unwrap();
945 let table = value.as_table().unwrap();
946
947 let age: LuaTable = table.get("age").unwrap();
948 let age_type: String = age.get("type").unwrap();
949 assert_eq!(age_type, "integer");
950 let age_min: f64 = age.get("min").unwrap();
951 assert!((age_min - 0.0).abs() < f64::EPSILON);
952 let age_max: f64 = age.get("max").unwrap();
953 assert!((age_max - 150.0).abs() < f64::EPSILON);
954 }
955
956 #[test]
957 fn to_lua_table_with_one_of() {
958 let lua = Lua::new();
959 let schema = Schema::Object(vec![Field {
960 name: "status".into(),
961 schema: Schema::String,
962 required: true,
963 constraints: Constraints {
964 one_of: Some(vec!["active".into(), "inactive".into()]),
965 ..Default::default()
966 },
967 }]);
968
969 let value = schema.to_lua_table(&lua).unwrap();
970 let table = value.as_table().unwrap();
971
972 let status: LuaTable = table.get("status").unwrap();
973 let one_of: LuaTable = status.get("one_of").unwrap();
974 let v1: String = one_of.get(1).unwrap();
975 let v2: String = one_of.get(2).unwrap();
976 assert_eq!(v1, "active");
977 assert_eq!(v2, "inactive");
978 }
979
980 #[test]
981 fn to_lua_table_non_object_returns_string() {
982 let lua = Lua::new();
983 let value = Schema::String.to_lua_table(&lua).unwrap();
984 let s = value.as_string().map(|s| s.to_string_lossy()).unwrap();
985 assert_eq!(s, "string");
986 }
987}