Skip to main content

schema_bridge_core/
lib.rs

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
94// Implement for basic types
95impl 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
239// Implement for char
240impl SchemaBridge for char {
241    fn to_ts() -> String {
242        "string".to_string()
243    }
244    fn to_schema() -> Schema {
245        Schema::String
246    }
247}
248
249// Implement for unit type
250impl 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
372// Tuple implementations
373impl<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
478// Helper to generate the full TS file content
479pub 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
490/// Export types to a TypeScript file
491pub 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 to easily export types to a file
497#[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// --- mlua integration ---
508
509#[cfg(feature = "mlua")]
510mod lua {
511    use super::*;
512    use mlua::prelude::*;
513
514    impl Schema {
515        /// Convert this schema to a Lua table compatible with
516        /// `mlua_batteries::validate::check()`.
517        ///
518        /// For `Schema::Object`, produces a table where each key maps to
519        /// either a type-name string (shorthand) or a full constraint table.
520        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                    // Non-object schemas: return the type name string
532                    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        // Use shorthand format when: not required AND no constraints
546        // (shorthand means the field is optional with just a type check)
547        if !field.required && !has_constraints {
548            return Ok(LuaValue::String(
549                lua.create_string(field.schema.type_name())?,
550            ));
551        }
552
553        // Full format: { type = "...", required = true/false, ... }
554        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 numeric types
711    #[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 BTreeMap
740    #[test]
741    fn test_btreemap_to_ts() {
742        assert_eq!(BTreeMap::<String, i32>::to_ts(), "Record<string, number>");
743    }
744
745    // Test HashSet and BTreeSet
746    #[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 smart pointers
759    #[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 Result
779    #[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 tuples
797    #[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 complex combinations
835    #[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 Field and Constraints
846    #[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        // "name" is required → full format
919        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        // "bio" is optional, no constraints → shorthand
926        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}