Skip to main content

weaveffi_core/
validate.rs

1use std::collections::{BTreeMap, BTreeSet};
2use weaveffi_ir::ir::{Api, ErrorDomain, Function, Module, Param, TypeRef};
3
4#[derive(Debug, thiserror::Error)]
5pub enum ValidationError {
6    #[error("module has no name")]
7    NoModuleName,
8    #[error("duplicate module name: {0}")]
9    DuplicateModuleName(String),
10    #[error("invalid module name '{0}': {1}")]
11    InvalidModuleName(String, &'static str),
12    #[error("duplicate function name in module '{module}': {function}")]
13    DuplicateFunctionName { module: String, function: String },
14    #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
15    DuplicateParamName {
16        module: String,
17        function: String,
18        param: String,
19    },
20    #[error("reserved keyword used: {0}")]
21    ReservedKeyword(String),
22    #[error("invalid identifier '{0}': {1}")]
23    InvalidIdentifier(String, &'static str),
24    #[error("async functions are not supported in 0.1.0: {module}::{function}")]
25    AsyncNotSupported { module: String, function: String },
26    #[error("error domain missing name in module '{0}'")]
27    ErrorDomainMissingName(String),
28    #[error("duplicate error code name in module '{module}': {name}")]
29    DuplicateErrorName { module: String, name: String },
30    #[error("duplicate error numeric code in module '{module}': {code}")]
31    DuplicateErrorCode { module: String, code: i32 },
32    #[error("invalid error code in module '{module}' for '{name}': must be non-zero")]
33    InvalidErrorCode { module: String, name: String },
34    #[error("function name collides with error domain name in module '{module}': {name}")]
35    NameCollisionWithErrorDomain { module: String, name: String },
36    #[error("duplicate struct name in module '{module}': {name}")]
37    DuplicateStructName { module: String, name: String },
38    #[error("duplicate field name in struct '{struct_name}': {field}")]
39    DuplicateStructField { struct_name: String, field: String },
40    #[error("empty struct in module '{module}': {name}")]
41    EmptyStruct { module: String, name: String },
42    #[error("duplicate enum name in module '{module}': {name}")]
43    DuplicateEnumName { module: String, name: String },
44    #[error("empty enum in module '{module}': {name}")]
45    EmptyEnum { module: String, name: String },
46    #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
47    DuplicateEnumVariant { enum_name: String, variant: String },
48    #[error("duplicate enum value in enum '{enum_name}': {value}")]
49    DuplicateEnumValue { enum_name: String, value: i32 },
50    #[error("unknown type reference: {name}")]
51    UnknownTypeRef { name: String },
52}
53
54const RESERVED: &[&str] = &[
55    "if", "else", "for", "while", "loop", "match", "type", "return", "async", "await", "break",
56    "continue", "fn", "struct", "enum", "mod", "use",
57];
58
59fn is_valid_identifier(s: &str) -> bool {
60    let mut chars = s.chars();
61    match chars.next() {
62        None => false,
63        Some(c) if !(c.is_ascii_alphabetic() || c == '_') => false,
64        _ => chars.all(|c| c.is_ascii_alphanumeric() || c == '_'),
65    }
66}
67
68fn check_identifier(name: &str) -> Result<(), ValidationError> {
69    if !is_valid_identifier(name) {
70        return Err(ValidationError::InvalidIdentifier(
71            name.to_string(),
72            "must start with a letter or underscore and contain only alphanumeric characters or underscores",
73        ));
74    }
75    if RESERVED.contains(&name) {
76        return Err(ValidationError::ReservedKeyword(name.to_string()));
77    }
78    Ok(())
79}
80
81pub fn validate_api(api: &Api) -> Result<(), ValidationError> {
82    let mut module_names = BTreeSet::new();
83    for m in &api.modules {
84        if !module_names.insert(m.name.clone()) {
85            return Err(ValidationError::DuplicateModuleName(m.name.clone()));
86        }
87        validate_module(m)?;
88    }
89    Ok(())
90}
91
92fn validate_module(module: &Module) -> Result<(), ValidationError> {
93    if module.name.trim().is_empty() {
94        return Err(ValidationError::NoModuleName);
95    }
96    check_identifier(&module.name).map_err(|e| match e {
97        ValidationError::ReservedKeyword(_) => {
98            ValidationError::InvalidModuleName(module.name.clone(), "reserved word")
99        }
100        ValidationError::InvalidIdentifier(_, reason) => {
101            ValidationError::InvalidModuleName(module.name.clone(), reason)
102        }
103        other => other,
104    })?;
105
106    let mut function_names = BTreeSet::new();
107    for f in &module.functions {
108        if !function_names.insert(f.name.clone()) {
109            return Err(ValidationError::DuplicateFunctionName {
110                module: module.name.clone(),
111                function: f.name.clone(),
112            });
113        }
114        validate_function(module, f)?;
115    }
116
117    let mut struct_names = BTreeSet::new();
118    for s in &module.structs {
119        check_identifier(&s.name)?;
120        if !struct_names.insert(s.name.clone()) {
121            return Err(ValidationError::DuplicateStructName {
122                module: module.name.clone(),
123                name: s.name.clone(),
124            });
125        }
126        if s.fields.is_empty() {
127            return Err(ValidationError::EmptyStruct {
128                module: module.name.clone(),
129                name: s.name.clone(),
130            });
131        }
132        let mut field_names = BTreeSet::new();
133        for f in &s.fields {
134            check_identifier(&f.name)?;
135            if !field_names.insert(f.name.clone()) {
136                return Err(ValidationError::DuplicateStructField {
137                    struct_name: s.name.clone(),
138                    field: f.name.clone(),
139                });
140            }
141        }
142    }
143
144    let mut enum_names = BTreeSet::new();
145    for e in &module.enums {
146        check_identifier(&e.name)?;
147        if !enum_names.insert(e.name.clone()) {
148            return Err(ValidationError::DuplicateEnumName {
149                module: module.name.clone(),
150                name: e.name.clone(),
151            });
152        }
153        if e.variants.is_empty() {
154            return Err(ValidationError::EmptyEnum {
155                module: module.name.clone(),
156                name: e.name.clone(),
157            });
158        }
159        let mut variant_names = BTreeSet::new();
160        let mut variant_values = BTreeMap::new();
161        for v in &e.variants {
162            check_identifier(&v.name)?;
163            if !variant_names.insert(v.name.clone()) {
164                return Err(ValidationError::DuplicateEnumVariant {
165                    enum_name: e.name.clone(),
166                    variant: v.name.clone(),
167                });
168            }
169            if variant_values.insert(v.value, v.name.clone()).is_some() {
170                return Err(ValidationError::DuplicateEnumValue {
171                    enum_name: e.name.clone(),
172                    value: v.value,
173                });
174            }
175        }
176    }
177
178    let known_types: BTreeSet<&str> = struct_names
179        .iter()
180        .map(|s| s.as_str())
181        .chain(enum_names.iter().map(|s| s.as_str()))
182        .collect();
183    for s in &module.structs {
184        for f in &s.fields {
185            validate_type_ref(&f.ty, &known_types)?;
186        }
187    }
188    for f in &module.functions {
189        for p in &f.params {
190            validate_type_ref(&p.ty, &known_types)?;
191        }
192        if let Some(ret) = &f.returns {
193            validate_type_ref(ret, &known_types)?;
194        }
195    }
196
197    if let Some(errors) = &module.errors {
198        validate_error_domain(module, errors, &function_names)?;
199    }
200
201    Ok(())
202}
203
204fn validate_function(module: &Module, f: &Function) -> Result<(), ValidationError> {
205    check_identifier(&f.name)?;
206    if f.r#async {
207        return Err(ValidationError::AsyncNotSupported {
208            module: module.name.clone(),
209            function: f.name.clone(),
210        });
211    }
212
213    let mut param_names = BTreeSet::new();
214    for p in &f.params {
215        validate_param(p)?;
216        if !param_names.insert(p.name.clone()) {
217            return Err(ValidationError::DuplicateParamName {
218                module: module.name.clone(),
219                function: f.name.clone(),
220                param: p.name.clone(),
221            });
222        }
223    }
224
225    Ok(())
226}
227
228fn validate_param(p: &Param) -> Result<(), ValidationError> {
229    check_identifier(&p.name)?;
230    Ok(())
231}
232
233fn validate_type_ref(ty: &TypeRef, known: &BTreeSet<&str>) -> Result<(), ValidationError> {
234    match ty {
235        TypeRef::Struct(name) | TypeRef::Enum(name) => {
236            if !known.contains(name.as_str()) {
237                return Err(ValidationError::UnknownTypeRef { name: name.clone() });
238            }
239            Ok(())
240        }
241        TypeRef::Optional(inner) | TypeRef::List(inner) => validate_type_ref(inner, known),
242        _ => Ok(()),
243    }
244}
245
246fn validate_error_domain(
247    module: &Module,
248    errors: &ErrorDomain,
249    function_names: &BTreeSet<String>,
250) -> Result<(), ValidationError> {
251    if errors.name.trim().is_empty() {
252        return Err(ValidationError::ErrorDomainMissingName(module.name.clone()));
253    }
254    if function_names.contains(&errors.name) {
255        return Err(ValidationError::NameCollisionWithErrorDomain {
256            module: module.name.clone(),
257            name: errors.name.clone(),
258        });
259    }
260
261    let mut by_name: BTreeSet<String> = BTreeSet::new();
262    let mut by_code: BTreeMap<i32, String> = BTreeMap::new();
263    for c in &errors.codes {
264        if c.code == 0 {
265            return Err(ValidationError::InvalidErrorCode {
266                module: module.name.clone(),
267                name: c.name.clone(),
268            });
269        }
270        if !by_name.insert(c.name.clone()) {
271            return Err(ValidationError::DuplicateErrorName {
272                module: module.name.clone(),
273                name: c.name.clone(),
274            });
275        }
276        if by_code.insert(c.code, c.name.clone()).is_some() {
277            return Err(ValidationError::DuplicateErrorCode {
278                module: module.name.clone(),
279                code: c.code,
280            });
281        }
282    }
283    Ok(())
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use weaveffi_ir::ir::{
290        Api, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, Module, Param, StructDef,
291        StructField, TypeRef,
292    };
293
294    fn simple_function(name: &str) -> Function {
295        Function {
296            name: name.to_string(),
297            params: vec![Param {
298                name: "x".to_string(),
299                ty: TypeRef::I32,
300            }],
301            returns: Some(TypeRef::I32),
302            doc: None,
303            r#async: false,
304        }
305    }
306
307    fn simple_module(name: &str) -> Module {
308        Module {
309            name: name.to_string(),
310            functions: vec![simple_function("do_stuff")],
311            structs: vec![],
312            enums: vec![],
313            errors: None,
314        }
315    }
316
317    fn simple_api() -> Api {
318        Api {
319            version: "0.1.0".to_string(),
320            modules: vec![simple_module("mymod")],
321        }
322    }
323
324    #[test]
325    fn valid_api_passes() {
326        assert!(validate_api(&simple_api()).is_ok());
327    }
328
329    #[test]
330    fn duplicate_module_names_rejected() {
331        let api = Api {
332            version: "0.1.0".to_string(),
333            modules: vec![simple_module("dup"), simple_module("dup")],
334        };
335        assert!(matches!(
336            validate_api(&api).unwrap_err(),
337            ValidationError::DuplicateModuleName(n) if n == "dup"
338        ));
339    }
340
341    #[test]
342    fn duplicate_function_names_rejected() {
343        let api = Api {
344            version: "0.1.0".to_string(),
345            modules: vec![Module {
346                name: "mymod".to_string(),
347                functions: vec![simple_function("same"), simple_function("same")],
348                structs: vec![],
349                enums: vec![],
350                errors: None,
351            }],
352        };
353        assert!(matches!(
354            validate_api(&api).unwrap_err(),
355            ValidationError::DuplicateFunctionName { .. }
356        ));
357    }
358
359    #[test]
360    fn reserved_keywords_rejected() {
361        for kw in ["type", "async"] {
362            let api = Api {
363                version: "0.1.0".to_string(),
364                modules: vec![Module {
365                    name: kw.to_string(),
366                    functions: vec![simple_function("ok_fn")],
367                    structs: vec![],
368                    enums: vec![],
369                    errors: None,
370                }],
371            };
372            assert!(
373                validate_api(&api).is_err(),
374                "Expected reserved keyword '{kw}' to be rejected"
375            );
376        }
377    }
378
379    #[test]
380    fn invalid_identifiers_rejected() {
381        for bad in ["123", "has spaces", ""] {
382            let api = Api {
383                version: "0.1.0".to_string(),
384                modules: vec![Module {
385                    name: bad.to_string(),
386                    functions: vec![simple_function("ok_fn")],
387                    structs: vec![],
388                    enums: vec![],
389                    errors: None,
390                }],
391            };
392            assert!(
393                validate_api(&api).is_err(),
394                "Expected invalid identifier '{bad}' to be rejected"
395            );
396        }
397    }
398
399    #[test]
400    fn async_functions_rejected() {
401        let api = Api {
402            version: "0.1.0".to_string(),
403            modules: vec![Module {
404                name: "mymod".to_string(),
405                functions: vec![Function {
406                    name: "do_async".to_string(),
407                    params: vec![],
408                    returns: None,
409                    doc: None,
410                    r#async: true,
411                }],
412                structs: vec![],
413                enums: vec![],
414                errors: None,
415            }],
416        };
417        assert!(matches!(
418            validate_api(&api).unwrap_err(),
419            ValidationError::AsyncNotSupported { .. }
420        ));
421    }
422
423    #[test]
424    fn empty_module_name_rejected() {
425        let api = Api {
426            version: "0.1.0".to_string(),
427            modules: vec![Module {
428                name: "".to_string(),
429                functions: vec![simple_function("ok_fn")],
430                structs: vec![],
431                enums: vec![],
432                errors: None,
433            }],
434        };
435        assert!(matches!(
436            validate_api(&api).unwrap_err(),
437            ValidationError::NoModuleName
438        ));
439    }
440
441    #[test]
442    fn doc_example_error_domain_validates() {
443        let api = Api {
444            version: "0.1.0".to_string(),
445            modules: vec![Module {
446                name: "contacts".to_string(),
447                functions: vec![
448                    Function {
449                        name: "create_contact".to_string(),
450                        params: vec![
451                            Param {
452                                name: "name".to_string(),
453                                ty: TypeRef::StringUtf8,
454                            },
455                            Param {
456                                name: "email".to_string(),
457                                ty: TypeRef::StringUtf8,
458                            },
459                        ],
460                        returns: Some(TypeRef::Handle),
461                        doc: None,
462                        r#async: false,
463                    },
464                    Function {
465                        name: "get_contact".to_string(),
466                        params: vec![Param {
467                            name: "id".to_string(),
468                            ty: TypeRef::Handle,
469                        }],
470                        returns: Some(TypeRef::StringUtf8),
471                        doc: None,
472                        r#async: false,
473                    },
474                ],
475                structs: vec![],
476                enums: vec![],
477                errors: Some(ErrorDomain {
478                    name: "ContactErrors".to_string(),
479                    codes: vec![
480                        ErrorCode {
481                            name: "not_found".to_string(),
482                            code: 1,
483                            message: "Contact not found".to_string(),
484                        },
485                        ErrorCode {
486                            name: "duplicate".to_string(),
487                            code: 2,
488                            message: "Contact already exists".to_string(),
489                        },
490                        ErrorCode {
491                            name: "invalid_email".to_string(),
492                            code: 3,
493                            message: "Email address is invalid".to_string(),
494                        },
495                    ],
496                }),
497            }],
498        };
499        assert!(validate_api(&api).is_ok());
500    }
501
502    #[test]
503    fn error_code_zero_rejected() {
504        let api = Api {
505            version: "0.1.0".to_string(),
506            modules: vec![Module {
507                name: "mymod".to_string(),
508                functions: vec![simple_function("ok_fn")],
509                structs: vec![],
510                enums: vec![],
511                errors: Some(ErrorDomain {
512                    name: "MyErrors".to_string(),
513                    codes: vec![ErrorCode {
514                        name: "success".to_string(),
515                        code: 0,
516                        message: "should fail".to_string(),
517                    }],
518                }),
519            }],
520        };
521        assert!(matches!(
522            validate_api(&api).unwrap_err(),
523            ValidationError::InvalidErrorCode { module, name }
524                if module == "mymod" && name == "success"
525        ));
526    }
527
528    #[test]
529    fn error_domain_name_collision_rejected() {
530        let api = Api {
531            version: "0.1.0".to_string(),
532            modules: vec![Module {
533                name: "mymod".to_string(),
534                functions: vec![simple_function("do_stuff")],
535                structs: vec![],
536                enums: vec![],
537                errors: Some(ErrorDomain {
538                    name: "do_stuff".to_string(),
539                    codes: vec![ErrorCode {
540                        name: "fail".to_string(),
541                        code: 1,
542                        message: "failed".to_string(),
543                    }],
544                }),
545            }],
546        };
547        assert!(matches!(
548            validate_api(&api).unwrap_err(),
549            ValidationError::NameCollisionWithErrorDomain { module, name }
550                if module == "mymod" && name == "do_stuff"
551        ));
552    }
553
554    #[test]
555    fn duplicate_error_names_rejected() {
556        let api = Api {
557            version: "0.1.0".to_string(),
558            modules: vec![Module {
559                name: "mymod".to_string(),
560                functions: vec![simple_function("ok_fn")],
561                structs: vec![],
562                enums: vec![],
563                errors: Some(ErrorDomain {
564                    name: "MyErrors".to_string(),
565                    codes: vec![
566                        ErrorCode {
567                            name: "fail".to_string(),
568                            code: 1,
569                            message: "failed".to_string(),
570                        },
571                        ErrorCode {
572                            name: "fail".to_string(),
573                            code: 2,
574                            message: "also failed".to_string(),
575                        },
576                    ],
577                }),
578            }],
579        };
580        assert!(matches!(
581            validate_api(&api).unwrap_err(),
582            ValidationError::DuplicateErrorName { module, name }
583                if module == "mymod" && name == "fail"
584        ));
585    }
586
587    #[test]
588    fn duplicate_error_codes_rejected() {
589        let api = Api {
590            version: "0.1.0".to_string(),
591            modules: vec![Module {
592                name: "mymod".to_string(),
593                functions: vec![simple_function("ok_fn")],
594                structs: vec![],
595                enums: vec![],
596                errors: Some(ErrorDomain {
597                    name: "MyErrors".to_string(),
598                    codes: vec![
599                        ErrorCode {
600                            name: "not_found".to_string(),
601                            code: 1,
602                            message: "not found".to_string(),
603                        },
604                        ErrorCode {
605                            name: "timeout".to_string(),
606                            code: 1,
607                            message: "timed out".to_string(),
608                        },
609                    ],
610                }),
611            }],
612        };
613        assert!(matches!(
614            validate_api(&api).unwrap_err(),
615            ValidationError::DuplicateErrorCode { .. }
616        ));
617    }
618
619    fn simple_struct(name: &str) -> StructDef {
620        StructDef {
621            name: name.to_string(),
622            doc: None,
623            fields: vec![StructField {
624                name: "x".to_string(),
625                ty: TypeRef::I32,
626                doc: None,
627            }],
628        }
629    }
630
631    #[test]
632    fn duplicate_struct_names_rejected() {
633        let api = Api {
634            version: "0.1.0".to_string(),
635            modules: vec![Module {
636                name: "mymod".to_string(),
637                functions: vec![simple_function("ok_fn")],
638                structs: vec![simple_struct("Point"), simple_struct("Point")],
639                enums: vec![],
640                errors: None,
641            }],
642        };
643        assert!(matches!(
644            validate_api(&api).unwrap_err(),
645            ValidationError::DuplicateStructName { module, name }
646                if module == "mymod" && name == "Point"
647        ));
648    }
649
650    #[test]
651    fn empty_struct_rejected() {
652        let api = Api {
653            version: "0.1.0".to_string(),
654            modules: vec![Module {
655                name: "mymod".to_string(),
656                functions: vec![simple_function("ok_fn")],
657                structs: vec![StructDef {
658                    name: "Empty".to_string(),
659                    doc: None,
660                    fields: vec![],
661                }],
662                enums: vec![],
663                errors: None,
664            }],
665        };
666        assert!(matches!(
667            validate_api(&api).unwrap_err(),
668            ValidationError::EmptyStruct { module, name }
669                if module == "mymod" && name == "Empty"
670        ));
671    }
672
673    #[test]
674    fn duplicate_struct_field_names_rejected() {
675        let api = Api {
676            version: "0.1.0".to_string(),
677            modules: vec![Module {
678                name: "mymod".to_string(),
679                functions: vec![simple_function("ok_fn")],
680                structs: vec![StructDef {
681                    name: "Point".to_string(),
682                    doc: None,
683                    fields: vec![
684                        StructField {
685                            name: "x".to_string(),
686                            ty: TypeRef::I32,
687                            doc: None,
688                        },
689                        StructField {
690                            name: "x".to_string(),
691                            ty: TypeRef::F64,
692                            doc: None,
693                        },
694                    ],
695                }],
696                enums: vec![],
697                errors: None,
698            }],
699        };
700        assert!(matches!(
701            validate_api(&api).unwrap_err(),
702            ValidationError::DuplicateStructField { struct_name, field }
703                if struct_name == "Point" && field == "x"
704        ));
705    }
706
707    fn simple_enum(name: &str) -> EnumDef {
708        EnumDef {
709            name: name.to_string(),
710            doc: None,
711            variants: vec![
712                EnumVariant {
713                    name: "A".to_string(),
714                    value: 0,
715                    doc: None,
716                },
717                EnumVariant {
718                    name: "B".to_string(),
719                    value: 1,
720                    doc: None,
721                },
722            ],
723        }
724    }
725
726    #[test]
727    fn duplicate_enum_names_rejected() {
728        let api = Api {
729            version: "0.1.0".to_string(),
730            modules: vec![Module {
731                name: "mymod".to_string(),
732                functions: vec![simple_function("ok_fn")],
733                structs: vec![],
734                enums: vec![simple_enum("Color"), simple_enum("Color")],
735                errors: None,
736            }],
737        };
738        assert!(matches!(
739            validate_api(&api).unwrap_err(),
740            ValidationError::DuplicateEnumName { module, name }
741                if module == "mymod" && name == "Color"
742        ));
743    }
744
745    #[test]
746    fn empty_enum_rejected() {
747        let api = Api {
748            version: "0.1.0".to_string(),
749            modules: vec![Module {
750                name: "mymod".to_string(),
751                functions: vec![simple_function("ok_fn")],
752                structs: vec![],
753                enums: vec![EnumDef {
754                    name: "Empty".to_string(),
755                    doc: None,
756                    variants: vec![],
757                }],
758                errors: None,
759            }],
760        };
761        assert!(matches!(
762            validate_api(&api).unwrap_err(),
763            ValidationError::EmptyEnum { module, name }
764                if module == "mymod" && name == "Empty"
765        ));
766    }
767
768    #[test]
769    fn duplicate_enum_variant_rejected() {
770        let api = Api {
771            version: "0.1.0".to_string(),
772            modules: vec![Module {
773                name: "mymod".to_string(),
774                functions: vec![simple_function("ok_fn")],
775                structs: vec![],
776                enums: vec![EnumDef {
777                    name: "Color".to_string(),
778                    doc: None,
779                    variants: vec![
780                        EnumVariant {
781                            name: "Red".to_string(),
782                            value: 0,
783                            doc: None,
784                        },
785                        EnumVariant {
786                            name: "Red".to_string(),
787                            value: 1,
788                            doc: None,
789                        },
790                    ],
791                }],
792                errors: None,
793            }],
794        };
795        assert!(matches!(
796            validate_api(&api).unwrap_err(),
797            ValidationError::DuplicateEnumVariant { enum_name, variant }
798                if enum_name == "Color" && variant == "Red"
799        ));
800    }
801
802    #[test]
803    fn duplicate_enum_value_rejected() {
804        let api = Api {
805            version: "0.1.0".to_string(),
806            modules: vec![Module {
807                name: "mymod".to_string(),
808                functions: vec![simple_function("ok_fn")],
809                structs: vec![],
810                enums: vec![EnumDef {
811                    name: "Color".to_string(),
812                    doc: None,
813                    variants: vec![
814                        EnumVariant {
815                            name: "Red".to_string(),
816                            value: 0,
817                            doc: None,
818                        },
819                        EnumVariant {
820                            name: "Green".to_string(),
821                            value: 0,
822                            doc: None,
823                        },
824                    ],
825                }],
826                errors: None,
827            }],
828        };
829        assert!(matches!(
830            validate_api(&api).unwrap_err(),
831            ValidationError::DuplicateEnumValue { enum_name, value }
832                if enum_name == "Color" && value == 0
833        ));
834    }
835
836    #[test]
837    fn unknown_type_ref_rejected() {
838        let api = Api {
839            version: "0.1.0".to_string(),
840            modules: vec![Module {
841                name: "mymod".to_string(),
842                functions: vec![Function {
843                    name: "do_stuff".to_string(),
844                    params: vec![Param {
845                        name: "x".to_string(),
846                        ty: TypeRef::Struct("Foo".to_string()),
847                    }],
848                    returns: None,
849                    doc: None,
850                    r#async: false,
851                }],
852                structs: vec![],
853                enums: vec![],
854                errors: None,
855            }],
856        };
857        assert!(matches!(
858            validate_api(&api).unwrap_err(),
859            ValidationError::UnknownTypeRef { name } if name == "Foo"
860        ));
861    }
862
863    #[test]
864    fn valid_struct_ref_passes() {
865        let api = Api {
866            version: "0.1.0".to_string(),
867            modules: vec![Module {
868                name: "mymod".to_string(),
869                functions: vec![Function {
870                    name: "do_stuff".to_string(),
871                    params: vec![Param {
872                        name: "p".to_string(),
873                        ty: TypeRef::Struct("Point".to_string()),
874                    }],
875                    returns: None,
876                    doc: None,
877                    r#async: false,
878                }],
879                structs: vec![simple_struct("Point")],
880                enums: vec![],
881                errors: None,
882            }],
883        };
884        assert!(validate_api(&api).is_ok());
885    }
886
887    #[test]
888    fn unknown_type_ref_in_optional_rejected() {
889        let api = Api {
890            version: "0.1.0".to_string(),
891            modules: vec![Module {
892                name: "mymod".to_string(),
893                functions: vec![Function {
894                    name: "do_stuff".to_string(),
895                    params: vec![Param {
896                        name: "x".to_string(),
897                        ty: TypeRef::Optional(Box::new(TypeRef::Struct("Bar".to_string()))),
898                    }],
899                    returns: None,
900                    doc: None,
901                    r#async: false,
902                }],
903                structs: vec![],
904                enums: vec![],
905                errors: None,
906            }],
907        };
908        assert!(matches!(
909            validate_api(&api).unwrap_err(),
910            ValidationError::UnknownTypeRef { name } if name == "Bar"
911        ));
912    }
913
914    #[test]
915    fn unknown_type_ref_in_list_rejected() {
916        let api = Api {
917            version: "0.1.0".to_string(),
918            modules: vec![Module {
919                name: "mymod".to_string(),
920                functions: vec![Function {
921                    name: "do_stuff".to_string(),
922                    params: vec![],
923                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Baz".to_string())))),
924                    doc: None,
925                    r#async: false,
926                }],
927                structs: vec![],
928                enums: vec![],
929                errors: None,
930            }],
931        };
932        assert!(matches!(
933            validate_api(&api).unwrap_err(),
934            ValidationError::UnknownTypeRef { name } if name == "Baz"
935        ));
936    }
937
938    #[test]
939    fn struct_field_referencing_unknown_type() {
940        let api = Api {
941            version: "0.1.0".to_string(),
942            modules: vec![Module {
943                name: "mymod".to_string(),
944                functions: vec![simple_function("ok_fn")],
945                structs: vec![StructDef {
946                    name: "Wrapper".to_string(),
947                    doc: None,
948                    fields: vec![StructField {
949                        name: "inner".to_string(),
950                        ty: TypeRef::Struct("Nonexistent".to_string()),
951                        doc: None,
952                    }],
953                }],
954                enums: vec![],
955                errors: None,
956            }],
957        };
958        assert!(matches!(
959            validate_api(&api).unwrap_err(),
960            ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
961        ));
962    }
963
964    #[test]
965    fn function_param_with_optional_struct() {
966        let api = Api {
967            version: "0.1.0".to_string(),
968            modules: vec![Module {
969                name: "mymod".to_string(),
970                functions: vec![Function {
971                    name: "save".to_string(),
972                    params: vec![Param {
973                        name: "c".to_string(),
974                        ty: TypeRef::Optional(Box::new(TypeRef::Struct("Contact".to_string()))),
975                    }],
976                    returns: None,
977                    doc: None,
978                    r#async: false,
979                }],
980                structs: vec![StructDef {
981                    name: "Contact".to_string(),
982                    doc: None,
983                    fields: vec![StructField {
984                        name: "name".to_string(),
985                        ty: TypeRef::StringUtf8,
986                        doc: None,
987                    }],
988                }],
989                enums: vec![],
990                errors: None,
991            }],
992        };
993        assert!(validate_api(&api).is_ok());
994    }
995
996    #[test]
997    fn function_param_with_list_of_enums() {
998        let api = Api {
999            version: "0.1.0".to_string(),
1000            modules: vec![Module {
1001                name: "mymod".to_string(),
1002                functions: vec![Function {
1003                    name: "paint".to_string(),
1004                    params: vec![Param {
1005                        name: "colors".to_string(),
1006                        ty: TypeRef::List(Box::new(TypeRef::Enum("Color".to_string()))),
1007                    }],
1008                    returns: None,
1009                    doc: None,
1010                    r#async: false,
1011                }],
1012                structs: vec![],
1013                enums: vec![simple_enum("Color")],
1014                errors: None,
1015            }],
1016        };
1017        assert!(validate_api(&api).is_ok());
1018    }
1019
1020    #[test]
1021    fn nested_optional_list_validates() {
1022        let api = Api {
1023            version: "0.1.0".to_string(),
1024            modules: vec![Module {
1025                name: "mymod".to_string(),
1026                functions: vec![Function {
1027                    name: "list_contacts".to_string(),
1028                    params: vec![],
1029                    returns: Some(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
1030                        TypeRef::Struct("Contact".to_string()),
1031                    ))))),
1032                    doc: None,
1033                    r#async: false,
1034                }],
1035                structs: vec![StructDef {
1036                    name: "Contact".to_string(),
1037                    doc: None,
1038                    fields: vec![StructField {
1039                        name: "name".to_string(),
1040                        ty: TypeRef::StringUtf8,
1041                        doc: None,
1042                    }],
1043                }],
1044                enums: vec![],
1045                errors: None,
1046            }],
1047        };
1048        assert!(validate_api(&api).is_ok());
1049    }
1050
1051    #[test]
1052    fn enum_variant_value_zero_allowed() {
1053        let api = Api {
1054            version: "0.1.0".to_string(),
1055            modules: vec![Module {
1056                name: "mymod".to_string(),
1057                functions: vec![simple_function("ok_fn")],
1058                structs: vec![],
1059                enums: vec![EnumDef {
1060                    name: "Status".to_string(),
1061                    doc: None,
1062                    variants: vec![
1063                        EnumVariant {
1064                            name: "Unknown".to_string(),
1065                            value: 0,
1066                            doc: None,
1067                        },
1068                        EnumVariant {
1069                            name: "Active".to_string(),
1070                            value: 1,
1071                            doc: None,
1072                        },
1073                    ],
1074                }],
1075                errors: None,
1076            }],
1077        };
1078        assert!(validate_api(&api).is_ok());
1079    }
1080
1081    #[test]
1082    fn valid_enum_ref_passes() {
1083        let api = Api {
1084            version: "0.1.0".to_string(),
1085            modules: vec![Module {
1086                name: "mymod".to_string(),
1087                functions: vec![Function {
1088                    name: "get_color".to_string(),
1089                    params: vec![],
1090                    returns: Some(TypeRef::Enum("Color".to_string())),
1091                    doc: None,
1092                    r#async: false,
1093                }],
1094                structs: vec![],
1095                enums: vec![simple_enum("Color")],
1096                errors: None,
1097            }],
1098        };
1099        assert!(validate_api(&api).is_ok());
1100    }
1101}