Skip to main content

weaveffi_core/
validate.rs

1use miette::{Diagnostic, NamedSource, SourceSpan};
2use std::collections::{BTreeMap, BTreeSet};
3use weaveffi_ir::ir::{Api, ErrorDomain, Function, Module, Param, TypeRef, SUPPORTED_VERSIONS};
4
5#[derive(Debug, Clone)]
6pub enum ValidationWarning {
7    LargeEnumVariantCount {
8        enum_name: String,
9        count: usize,
10    },
11    DeepNesting {
12        location: String,
13        depth: usize,
14    },
15    EmptyModuleDoc {
16        module: String,
17    },
18    AsyncVoidFunction {
19        module: String,
20        function: String,
21    },
22    MutableOnValueType {
23        module: String,
24        function: String,
25        param: String,
26    },
27    DeprecatedFunction {
28        module: String,
29        function: String,
30        message: String,
31    },
32}
33
34impl std::fmt::Display for ValidationWarning {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::LargeEnumVariantCount { enum_name, count } => {
38                write!(f, "enum '{enum_name}' has {count} variants (>100)")
39            }
40            Self::DeepNesting { location, depth } => {
41                write!(
42                    f,
43                    "deep type nesting at {location} (depth {depth}, max recommended 3)"
44                )
45            }
46            Self::EmptyModuleDoc { module } => {
47                write!(f, "module '{module}' has no doc comments on any function")
48            }
49            Self::AsyncVoidFunction { module, function } => {
50                write!(
51                    f,
52                    "async function {module}::{function} has no return type; async void is unusual"
53                )
54            }
55            Self::MutableOnValueType {
56                module,
57                function,
58                param,
59            } => {
60                write!(
61                    f,
62                    "'mutable' on value-type parameter {module}::{function}::{param} has no effect; only meaningful for pointer/reference types (struct, string, bytes)"
63                )
64            }
65            Self::DeprecatedFunction {
66                module,
67                function,
68                message,
69            } => {
70                write!(f, "function {module}::{function} is deprecated: {message}")
71            }
72        }
73    }
74}
75
76pub fn collect_warnings(api: &Api) -> Vec<ValidationWarning> {
77    let mut warnings = Vec::new();
78    for module in &api.modules {
79        for e in &module.enums {
80            if e.variants.len() > 100 {
81                warnings.push(ValidationWarning::LargeEnumVariantCount {
82                    enum_name: e.name.clone(),
83                    count: e.variants.len(),
84                });
85            }
86        }
87
88        for f in &module.functions {
89            for p in &f.params {
90                let depth = nesting_depth(&p.ty);
91                if depth > 3 {
92                    warnings.push(ValidationWarning::DeepNesting {
93                        location: format!("{}::{}::{}", module.name, f.name, p.name),
94                        depth,
95                    });
96                }
97            }
98            if let Some(ret) = &f.returns {
99                let depth = nesting_depth(ret);
100                if depth > 3 {
101                    warnings.push(ValidationWarning::DeepNesting {
102                        location: format!("{}::{}::return", module.name, f.name),
103                        depth,
104                    });
105                }
106            }
107        }
108        for s in &module.structs {
109            for field in &s.fields {
110                let depth = nesting_depth(&field.ty);
111                if depth > 3 {
112                    warnings.push(ValidationWarning::DeepNesting {
113                        location: format!("{}::{}::{}", module.name, s.name, field.name),
114                        depth,
115                    });
116                }
117            }
118        }
119
120        for f in &module.functions {
121            if f.r#async && f.returns.is_none() {
122                warnings.push(ValidationWarning::AsyncVoidFunction {
123                    module: module.name.clone(),
124                    function: f.name.clone(),
125                });
126            }
127            for p in &f.params {
128                if p.mutable && is_value_type(&p.ty) {
129                    warnings.push(ValidationWarning::MutableOnValueType {
130                        module: module.name.clone(),
131                        function: f.name.clone(),
132                        param: p.name.clone(),
133                    });
134                }
135            }
136        }
137
138        for f in &module.functions {
139            if let Some(msg) = &f.deprecated {
140                warnings.push(ValidationWarning::DeprecatedFunction {
141                    module: module.name.clone(),
142                    function: f.name.clone(),
143                    message: msg.clone(),
144                });
145            }
146        }
147
148        if !module.functions.is_empty() && module.functions.iter().all(|f| f.doc.is_none()) {
149            warnings.push(ValidationWarning::EmptyModuleDoc {
150                module: module.name.clone(),
151            });
152        }
153    }
154    warnings
155}
156
157fn is_value_type(ty: &TypeRef) -> bool {
158    matches!(
159        ty,
160        TypeRef::I32
161            | TypeRef::U32
162            | TypeRef::I64
163            | TypeRef::F64
164            | TypeRef::Bool
165            | TypeRef::Enum(_)
166            | TypeRef::Handle
167    )
168}
169
170fn nesting_depth(ty: &TypeRef) -> usize {
171    match ty {
172        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
173            1 + nesting_depth(inner)
174        }
175        TypeRef::Map(k, v) => nesting_depth(k).max(nesting_depth(v)),
176        _ => 0,
177    }
178}
179
180#[derive(Debug, thiserror::Error, Diagnostic)]
181pub enum ValidationError {
182    #[error("module has no name")]
183    #[diagnostic(help("every module must have a non-empty 'name' field"))]
184    NoModuleName,
185    #[error("duplicate module name: {0}")]
186    #[diagnostic(help(
187        "module names must be unique within an API definition; rename or merge the duplicate"
188    ))]
189    DuplicateModuleName(String),
190    #[error("invalid module name '{0}': {1}")]
191    #[diagnostic(help(
192        "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
193    ))]
194    InvalidModuleName(String, &'static str),
195    #[error("duplicate function name in module '{module}': {function}")]
196    #[diagnostic(help("function names must be unique within a module; rename the duplicate"))]
197    DuplicateFunctionName { module: String, function: String },
198    #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
199    #[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))]
200    DuplicateParamName {
201        module: String,
202        function: String,
203        param: String,
204    },
205    #[error("reserved keyword used: {0}")]
206    #[diagnostic(help("choose a different name that is not a language reserved word"))]
207    ReservedKeyword(String),
208    #[error("invalid identifier '{0}': {1}")]
209    #[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))]
210    InvalidIdentifier(String, &'static str),
211    #[error("error domain missing name in module '{0}'")]
212    #[diagnostic(help("add a non-empty 'name' field to the error domain"))]
213    ErrorDomainMissingName(String),
214    #[error("duplicate error code name in module '{module}': {name}")]
215    #[diagnostic(help("error code names must be unique within a module; rename the duplicate"))]
216    DuplicateErrorName { module: String, name: String },
217    #[error("duplicate error numeric code in module '{module}': {code}")]
218    #[diagnostic(help(
219        "numeric error codes must be unique within a module; assign a different value"
220    ))]
221    DuplicateErrorCode { module: String, code: i32 },
222    #[error("invalid error code in module '{module}' for '{name}': must be non-zero")]
223    #[diagnostic(help("error codes must be non-zero; use a positive or negative integer"))]
224    InvalidErrorCode { module: String, name: String },
225    #[error("function name collides with error domain name in module '{module}': {name}")]
226    #[diagnostic(help(
227        "function and error domain names share a namespace; rename one to avoid the collision"
228    ))]
229    NameCollisionWithErrorDomain { module: String, name: String },
230    #[error("duplicate struct name in module '{module}': {name}")]
231    #[diagnostic(help("struct names must be unique within a module; rename the duplicate"))]
232    DuplicateStructName { module: String, name: String },
233    #[error("duplicate field name in struct '{struct_name}': {field}")]
234    #[diagnostic(help("field names must be unique within a struct; rename the duplicate"))]
235    DuplicateStructField { struct_name: String, field: String },
236    #[error("empty struct in module '{module}': {name}")]
237    #[diagnostic(help("structs must have at least one field; add a field or remove the struct"))]
238    EmptyStruct { module: String, name: String },
239    #[error("duplicate enum name in module '{module}': {name}")]
240    #[diagnostic(help("enum names must be unique within a module; rename the duplicate"))]
241    DuplicateEnumName { module: String, name: String },
242    #[error("empty enum in module '{module}': {name}")]
243    #[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))]
244    EmptyEnum { module: String, name: String },
245    #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
246    #[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))]
247    DuplicateEnumVariant { enum_name: String, variant: String },
248    #[error("duplicate enum value in enum '{enum_name}': {value}")]
249    #[diagnostic(help(
250        "variant numeric values must be unique within an enum; assign a different value"
251    ))]
252    DuplicateEnumValue { enum_name: String, value: i32 },
253    #[error("unknown type reference: {name}")]
254    #[diagnostic(help(
255        "define a struct or enum with this name in the same module, or check for typos"
256    ))]
257    UnknownTypeRef { name: String },
258    #[error("invalid map key type: {key_type}; only primitive types and strings are allowed as map keys")]
259    #[diagnostic(help("map keys must be primitive types (i32, u32, i64, f64, bool, string); structs, lists, and maps cannot be keys"))]
260    InvalidMapKey { key_type: String },
261    #[error(
262        "borrowed type '{ty}' is not valid in {location}; only function parameters are allowed"
263    )]
264    #[diagnostic(help("borrowed types (&str, &[u8]) can only be used as function parameters, not return types or struct fields"))]
265    BorrowedTypeInInvalidPosition { ty: String, location: String },
266    #[error("duplicate callback name in module '{module}': {name}")]
267    #[diagnostic(help("callback names must be unique within a module; rename the duplicate"))]
268    DuplicateCallbackName { module: String, name: String },
269    #[error(
270        "listener '{listener}' in module '{module}' references undefined callback '{callback}'"
271    )]
272    #[diagnostic(help(
273        "listener event_callback must reference a callback defined in the same module"
274    ))]
275    ListenerCallbackNotFound {
276        module: String,
277        listener: String,
278        callback: String,
279    },
280    #[error("duplicate listener name in module '{module}': {name}")]
281    #[diagnostic(help("listener names must be unique within a module; rename the duplicate"))]
282    DuplicateListenerName { module: String, name: String },
283    #[error("iterator type is only valid as a function return type, found in {location}")]
284    #[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))]
285    IteratorInInvalidPosition { location: String },
286    #[error("builder struct '{name}' in module '{module}' must have at least one field")]
287    #[diagnostic(help(
288        "builder structs must have at least one field; add a field or set builder: false"
289    ))]
290    BuilderStructEmpty { module: String, name: String },
291    #[error("unsupported schema version '{version}'; supported versions: {supported}")]
292    #[diagnostic(help("run 'weaveffi upgrade <file>' to migrate to the current schema version"))]
293    UnsupportedSchemaVersion { version: String, supported: String },
294}
295
296/// Diagnostic wrapper that attaches an optional source code snippet and a
297/// best-effort byte range to a [`ValidationError`] for fancy rendering via
298/// [`miette`]. The wrapper delegates `help()` and `code()` to the inner error
299/// while exposing its own `source_code` and `labels` so the renderer can
300/// underline the offending identifier in the input.
301#[derive(Debug)]
302pub struct ValidationDiagnostic {
303    pub error: ValidationError,
304    pub src: Option<NamedSource<String>>,
305    pub span: Option<SourceSpan>,
306}
307
308impl std::fmt::Display for ValidationDiagnostic {
309    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        std::fmt::Display::fmt(&self.error, f)
311    }
312}
313
314impl std::error::Error for ValidationDiagnostic {
315    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
316        self.error.source()
317    }
318}
319
320impl Diagnostic for ValidationDiagnostic {
321    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
322        self.error.code()
323    }
324
325    fn severity(&self) -> Option<miette::Severity> {
326        self.error.severity()
327    }
328
329    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
330        self.error.help()
331    }
332
333    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
334        self.error.url()
335    }
336
337    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
338        self.src
339            .as_ref()
340            .map(|s| s as &dyn miette::SourceCode)
341            .or_else(|| self.error.source_code())
342    }
343
344    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
345        if let Some(span) = self.span {
346            Some(Box::new(std::iter::once(
347                miette::LabeledSpan::new_with_span(Some("here".to_string()), span),
348            )))
349        } else {
350            self.error.labels()
351        }
352    }
353}
354
355impl ValidationDiagnostic {
356    /// Build a [`ValidationDiagnostic`] from a [`ValidationError`] and an
357    /// optional `(filename, contents)` source. When a source is provided the
358    /// constructor performs a best-effort search for the offending identifier
359    /// (e.g. a duplicate module name or unknown type reference) and attaches
360    /// a [`SourceSpan`] for fancy rendering. If no span can be computed the
361    /// label is omitted and miette still produces a nicer message + help
362    /// section than plain `Display`.
363    pub fn new(error: ValidationError, source: Option<(&str, &str)>) -> Self {
364        let (src, span) = match source {
365            Some((filename, contents)) => {
366                let span = find_offending_span(&error, contents);
367                (Some(NamedSource::new(filename, contents.to_string())), span)
368            }
369            None => (None, None),
370        };
371        Self { error, src, span }
372    }
373}
374
375fn find_offending_span(err: &ValidationError, src: &str) -> Option<SourceSpan> {
376    let needle: &str = match err {
377        ValidationError::DuplicateModuleName(n) => Some(n.as_str()),
378        ValidationError::InvalidModuleName(n, _) => Some(n.as_str()),
379        ValidationError::DuplicateFunctionName { function, .. } => Some(function.as_str()),
380        ValidationError::DuplicateParamName { param, .. } => Some(param.as_str()),
381        ValidationError::ReservedKeyword(n) => Some(n.as_str()),
382        ValidationError::InvalidIdentifier(n, _) => Some(n.as_str()),
383        ValidationError::DuplicateErrorName { name, .. } => Some(name.as_str()),
384        ValidationError::InvalidErrorCode { name, .. } => Some(name.as_str()),
385        ValidationError::NameCollisionWithErrorDomain { name, .. } => Some(name.as_str()),
386        ValidationError::DuplicateStructName { name, .. } => Some(name.as_str()),
387        ValidationError::DuplicateStructField { field, .. } => Some(field.as_str()),
388        ValidationError::EmptyStruct { name, .. } => Some(name.as_str()),
389        ValidationError::DuplicateEnumName { name, .. } => Some(name.as_str()),
390        ValidationError::EmptyEnum { name, .. } => Some(name.as_str()),
391        ValidationError::DuplicateEnumVariant { variant, .. } => Some(variant.as_str()),
392        ValidationError::UnknownTypeRef { name } => Some(name.as_str()),
393        ValidationError::DuplicateCallbackName { name, .. } => Some(name.as_str()),
394        ValidationError::ListenerCallbackNotFound { callback, .. } => Some(callback.as_str()),
395        ValidationError::DuplicateListenerName { name, .. } => Some(name.as_str()),
396        ValidationError::BuilderStructEmpty { name, .. } => Some(name.as_str()),
397        ValidationError::UnsupportedSchemaVersion { version, .. } => Some(version.as_str()),
398        _ => None,
399    }?;
400    let quoted = format!("\"{needle}\"");
401    if let Some(pos) = src.find(&quoted) {
402        return Some(SourceSpan::new(pos.into(), quoted.len()));
403    }
404    src.find(needle)
405        .map(|pos| SourceSpan::new(pos.into(), needle.len()))
406}
407
408const RESERVED: &[&str] = &[
409    "if", "else", "for", "while", "loop", "match", "type", "return", "async", "await", "break",
410    "continue", "fn", "struct", "enum", "mod", "use",
411];
412
413fn is_valid_identifier(s: &str) -> bool {
414    let mut chars = s.chars();
415    match chars.next() {
416        None => false,
417        Some(c) if !(c.is_ascii_alphabetic() || c == '_') => false,
418        _ => chars.all(|c| c.is_ascii_alphanumeric() || c == '_'),
419    }
420}
421
422fn check_identifier(name: &str) -> Result<(), ValidationError> {
423    if !is_valid_identifier(name) {
424        return Err(ValidationError::InvalidIdentifier(
425            name.to_string(),
426            "must start with a letter or underscore and contain only alphanumeric characters or underscores",
427        ));
428    }
429    if RESERVED.contains(&name) {
430        return Err(ValidationError::ReservedKeyword(name.to_string()));
431    }
432    Ok(())
433}
434
435/// Validate an [`Api`]. The optional `source` is `(filename, contents)` of the
436/// IDL file and is used at the call site to attach a span to a returned error
437/// via [`ValidationDiagnostic::new`]. Pass `None` when the API is constructed
438/// in memory (tests, programmatic builds) and there is no on-disk source.
439#[allow(clippy::result_large_err)]
440pub fn validate_api(
441    api: &mut Api,
442    source: Option<(&str, &str)>,
443) -> Result<(), ValidationDiagnostic> {
444    validate_api_inner(api).map_err(|e| ValidationDiagnostic::new(e, source))
445}
446
447fn validate_api_inner(api: &mut Api) -> Result<(), ValidationError> {
448    if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
449        return Err(ValidationError::UnsupportedSchemaVersion {
450            version: api.version.clone(),
451            supported: SUPPORTED_VERSIONS.join(", "),
452        });
453    }
454    let mut module_names = BTreeSet::new();
455    for m in &api.modules {
456        if !module_names.insert(m.name.clone()) {
457            return Err(ValidationError::DuplicateModuleName(m.name.clone()));
458        }
459        validate_module(m, &api.modules)?;
460    }
461    resolve_type_refs(api);
462    Ok(())
463}
464
465pub fn resolve_type_refs(api: &mut Api) {
466    let mut global_types: BTreeMap<String, (String, bool)> = BTreeMap::new();
467    for module in &api.modules {
468        for s in &module.structs {
469            global_types
470                .entry(s.name.clone())
471                .or_insert((module.name.clone(), false));
472        }
473        for e in &module.enums {
474            global_types
475                .entry(e.name.clone())
476                .or_insert((module.name.clone(), true));
477        }
478    }
479
480    for module in &mut api.modules {
481        let local_enum_names: BTreeSet<String> =
482            module.enums.iter().map(|e| e.name.clone()).collect();
483        let local_struct_names: BTreeSet<String> =
484            module.structs.iter().map(|s| s.name.clone()).collect();
485        let module_name = module.name.clone();
486        for f in &mut module.functions {
487            for p in &mut f.params {
488                resolve_single_type_ref(
489                    &mut p.ty,
490                    &local_enum_names,
491                    &local_struct_names,
492                    &module_name,
493                    &global_types,
494                );
495            }
496            if let Some(ret) = &mut f.returns {
497                resolve_single_type_ref(
498                    ret,
499                    &local_enum_names,
500                    &local_struct_names,
501                    &module_name,
502                    &global_types,
503                );
504            }
505        }
506        for s in &mut module.structs {
507            for field in &mut s.fields {
508                resolve_single_type_ref(
509                    &mut field.ty,
510                    &local_enum_names,
511                    &local_struct_names,
512                    &module_name,
513                    &global_types,
514                );
515            }
516        }
517    }
518}
519
520fn resolve_single_type_ref(
521    ty: &mut TypeRef,
522    local_enum_names: &BTreeSet<String>,
523    local_struct_names: &BTreeSet<String>,
524    current_module: &str,
525    global_types: &BTreeMap<String, (String, bool)>,
526) {
527    match ty {
528        TypeRef::Struct(name) if local_enum_names.contains(name.as_str()) => {
529            let name = std::mem::take(name);
530            *ty = TypeRef::Enum(name);
531        }
532        TypeRef::Struct(name) if !local_struct_names.contains(name.as_str()) => {
533            if let Some((mod_name, is_enum)) = global_types.get(name.as_str()) {
534                if mod_name != current_module {
535                    let qualified = format!("{mod_name}.{name}");
536                    if *is_enum {
537                        *ty = TypeRef::Enum(qualified);
538                    } else {
539                        *name = qualified;
540                    }
541                }
542            }
543        }
544        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
545            resolve_single_type_ref(
546                inner,
547                local_enum_names,
548                local_struct_names,
549                current_module,
550                global_types,
551            );
552        }
553        TypeRef::Map(k, v) => {
554            resolve_single_type_ref(
555                k,
556                local_enum_names,
557                local_struct_names,
558                current_module,
559                global_types,
560            );
561            resolve_single_type_ref(
562                v,
563                local_enum_names,
564                local_struct_names,
565                current_module,
566                global_types,
567            );
568        }
569        _ => {}
570    }
571}
572
573pub fn find_type_in_api(api: &Api, name: &str) -> Option<(String, bool)> {
574    for module in &api.modules {
575        if module.structs.iter().any(|s| s.name == name) {
576            return Some((module.name.clone(), false));
577        }
578        if module.enums.iter().any(|e| e.name == name) {
579            return Some((module.name.clone(), true));
580        }
581    }
582    None
583}
584
585fn validate_module(module: &Module, all_modules: &[Module]) -> Result<(), ValidationError> {
586    if module.name.trim().is_empty() {
587        return Err(ValidationError::NoModuleName);
588    }
589    check_identifier(&module.name).map_err(|e| match e {
590        ValidationError::ReservedKeyword(_) => {
591            ValidationError::InvalidModuleName(module.name.clone(), "reserved word")
592        }
593        ValidationError::InvalidIdentifier(_, reason) => {
594            ValidationError::InvalidModuleName(module.name.clone(), reason)
595        }
596        other => other,
597    })?;
598
599    let mut function_names = BTreeSet::new();
600    for f in &module.functions {
601        if !function_names.insert(f.name.clone()) {
602            return Err(ValidationError::DuplicateFunctionName {
603                module: module.name.clone(),
604                function: f.name.clone(),
605            });
606        }
607        validate_function(module, f)?;
608    }
609
610    let mut struct_names = BTreeSet::new();
611    for s in &module.structs {
612        check_identifier(&s.name)?;
613        if !struct_names.insert(s.name.clone()) {
614            return Err(ValidationError::DuplicateStructName {
615                module: module.name.clone(),
616                name: s.name.clone(),
617            });
618        }
619        if s.fields.is_empty() {
620            if s.builder {
621                return Err(ValidationError::BuilderStructEmpty {
622                    module: module.name.clone(),
623                    name: s.name.clone(),
624                });
625            }
626            return Err(ValidationError::EmptyStruct {
627                module: module.name.clone(),
628                name: s.name.clone(),
629            });
630        }
631        let mut field_names = BTreeSet::new();
632        for f in &s.fields {
633            check_identifier(&f.name)?;
634            if !field_names.insert(f.name.clone()) {
635                return Err(ValidationError::DuplicateStructField {
636                    struct_name: s.name.clone(),
637                    field: f.name.clone(),
638                });
639            }
640        }
641    }
642
643    let mut enum_names = BTreeSet::new();
644    for e in &module.enums {
645        check_identifier(&e.name)?;
646        if !enum_names.insert(e.name.clone()) {
647            return Err(ValidationError::DuplicateEnumName {
648                module: module.name.clone(),
649                name: e.name.clone(),
650            });
651        }
652        if e.variants.is_empty() {
653            return Err(ValidationError::EmptyEnum {
654                module: module.name.clone(),
655                name: e.name.clone(),
656            });
657        }
658        let mut variant_names = BTreeSet::new();
659        let mut variant_values = BTreeMap::new();
660        for v in &e.variants {
661            check_identifier(&v.name)?;
662            if !variant_names.insert(v.name.clone()) {
663                return Err(ValidationError::DuplicateEnumVariant {
664                    enum_name: e.name.clone(),
665                    variant: v.name.clone(),
666                });
667            }
668            if variant_values.insert(v.value, v.name.clone()).is_some() {
669                return Err(ValidationError::DuplicateEnumValue {
670                    enum_name: e.name.clone(),
671                    value: v.value,
672                });
673            }
674        }
675    }
676
677    let known_types: BTreeSet<&str> = struct_names
678        .iter()
679        .map(|s| s.as_str())
680        .chain(enum_names.iter().map(|s| s.as_str()))
681        .collect();
682    for s in &module.structs {
683        for f in &s.fields {
684            if let Some(ty) = contains_borrowed(&f.ty) {
685                return Err(ValidationError::BorrowedTypeInInvalidPosition {
686                    ty: ty.to_string(),
687                    location: format!("field '{}' of struct '{}'", f.name, s.name),
688                });
689            }
690            if contains_iterator(&f.ty) {
691                return Err(ValidationError::IteratorInInvalidPosition {
692                    location: format!("field '{}' of struct '{}'", f.name, s.name),
693                });
694            }
695            validate_type_ref(&f.ty, &known_types, all_modules, &module.name)?;
696        }
697    }
698    for f in &module.functions {
699        for p in &f.params {
700            if contains_iterator(&p.ty) {
701                return Err(ValidationError::IteratorInInvalidPosition {
702                    location: format!(
703                        "param '{}' of function '{}::{}'",
704                        p.name, module.name, f.name
705                    ),
706                });
707            }
708            validate_type_ref(&p.ty, &known_types, all_modules, &module.name)?;
709        }
710        if let Some(ret) = &f.returns {
711            if let Some(ty) = contains_borrowed(ret) {
712                return Err(ValidationError::BorrowedTypeInInvalidPosition {
713                    ty: ty.to_string(),
714                    location: format!("return type of {}::{}", module.name, f.name),
715                });
716            }
717            validate_type_ref(ret, &known_types, all_modules, &module.name)?;
718        }
719    }
720
721    let mut callback_names = BTreeSet::new();
722    for cb in &module.callbacks {
723        check_identifier(&cb.name)?;
724        if !callback_names.insert(cb.name.clone()) {
725            return Err(ValidationError::DuplicateCallbackName {
726                module: module.name.clone(),
727                name: cb.name.clone(),
728            });
729        }
730        for p in &cb.params {
731            validate_param(p)?;
732        }
733    }
734
735    let mut listener_names = BTreeSet::new();
736    for l in &module.listeners {
737        check_identifier(&l.name)?;
738        if !listener_names.insert(l.name.clone()) {
739            return Err(ValidationError::DuplicateListenerName {
740                module: module.name.clone(),
741                name: l.name.clone(),
742            });
743        }
744        if !callback_names.contains(&l.event_callback) {
745            return Err(ValidationError::ListenerCallbackNotFound {
746                module: module.name.clone(),
747                listener: l.name.clone(),
748                callback: l.event_callback.clone(),
749            });
750        }
751    }
752
753    if let Some(errors) = &module.errors {
754        validate_error_domain(module, errors, &function_names)?;
755    }
756
757    let mut sub_module_names = BTreeSet::new();
758    for sub in &module.modules {
759        if !sub_module_names.insert(sub.name.clone()) {
760            return Err(ValidationError::DuplicateModuleName(sub.name.clone()));
761        }
762        validate_module(sub, all_modules)?;
763    }
764
765    Ok(())
766}
767
768fn validate_function(module: &Module, f: &Function) -> Result<(), ValidationError> {
769    check_identifier(&f.name)?;
770
771    let mut param_names = BTreeSet::new();
772    for p in &f.params {
773        validate_param(p)?;
774        if !param_names.insert(p.name.clone()) {
775            return Err(ValidationError::DuplicateParamName {
776                module: module.name.clone(),
777                function: f.name.clone(),
778                param: p.name.clone(),
779            });
780        }
781    }
782
783    Ok(())
784}
785
786fn validate_param(p: &Param) -> Result<(), ValidationError> {
787    check_identifier(&p.name)?;
788    Ok(())
789}
790
791fn contains_borrowed(ty: &TypeRef) -> Option<&'static str> {
792    match ty {
793        TypeRef::BorrowedStr => Some("&str"),
794        TypeRef::BorrowedBytes => Some("&[u8]"),
795        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
796            contains_borrowed(inner)
797        }
798        TypeRef::Map(k, v) => contains_borrowed(k).or_else(|| contains_borrowed(v)),
799        _ => None,
800    }
801}
802
803fn contains_iterator(ty: &TypeRef) -> bool {
804    match ty {
805        TypeRef::Iterator(_) => true,
806        TypeRef::Optional(inner) | TypeRef::List(inner) => contains_iterator(inner),
807        TypeRef::Map(k, v) => contains_iterator(k) || contains_iterator(v),
808        _ => false,
809    }
810}
811
812fn validate_type_ref(
813    ty: &TypeRef,
814    known: &BTreeSet<&str>,
815    all_modules: &[Module],
816    current_module: &str,
817) -> Result<(), ValidationError> {
818    match ty {
819        TypeRef::Struct(name) | TypeRef::Enum(name) | TypeRef::TypedHandle(name) => {
820            if !known.contains(name.as_str()) {
821                let found_elsewhere = all_modules.iter().any(|m| {
822                    m.name != current_module
823                        && (m.structs.iter().any(|s| s.name == *name)
824                            || m.enums.iter().any(|e| e.name == *name))
825                });
826                if !found_elsewhere {
827                    return Err(ValidationError::UnknownTypeRef { name: name.clone() });
828                }
829            }
830            Ok(())
831        }
832        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
833            validate_type_ref(inner, known, all_modules, current_module)
834        }
835        TypeRef::Map(k, v) => {
836            let bad_key = match k.as_ref() {
837                TypeRef::Struct(name) => Some(format!("struct {name}")),
838                TypeRef::List(_) => Some("list".to_string()),
839                TypeRef::Map(_, _) => Some("map".to_string()),
840                _ => None,
841            };
842            if let Some(key_type) = bad_key {
843                return Err(ValidationError::InvalidMapKey { key_type });
844            }
845            validate_type_ref(k, known, all_modules, current_module)?;
846            validate_type_ref(v, known, all_modules, current_module)
847        }
848        _ => Ok(()),
849    }
850}
851
852fn validate_error_domain(
853    module: &Module,
854    errors: &ErrorDomain,
855    function_names: &BTreeSet<String>,
856) -> Result<(), ValidationError> {
857    if errors.name.trim().is_empty() {
858        return Err(ValidationError::ErrorDomainMissingName(module.name.clone()));
859    }
860    if function_names.contains(&errors.name) {
861        return Err(ValidationError::NameCollisionWithErrorDomain {
862            module: module.name.clone(),
863            name: errors.name.clone(),
864        });
865    }
866
867    let mut by_name: BTreeSet<String> = BTreeSet::new();
868    let mut by_code: BTreeMap<i32, String> = BTreeMap::new();
869    for c in &errors.codes {
870        if c.code == 0 {
871            return Err(ValidationError::InvalidErrorCode {
872                module: module.name.clone(),
873                name: c.name.clone(),
874            });
875        }
876        if !by_name.insert(c.name.clone()) {
877            return Err(ValidationError::DuplicateErrorName {
878                module: module.name.clone(),
879                name: c.name.clone(),
880            });
881        }
882        if by_code.insert(c.code, c.name.clone()).is_some() {
883            return Err(ValidationError::DuplicateErrorCode {
884                module: module.name.clone(),
885                code: c.code,
886            });
887        }
888    }
889    Ok(())
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895    use weaveffi_ir::ir::{
896        Api, CallbackDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, ListenerDef,
897        Module, Param, StructDef, StructField, TypeRef,
898    };
899
900    fn simple_function(name: &str) -> Function {
901        Function {
902            name: name.to_string(),
903            params: vec![Param {
904                name: "x".to_string(),
905                ty: TypeRef::I32,
906                mutable: false,
907                doc: None,
908            }],
909            returns: Some(TypeRef::I32),
910            doc: None,
911            r#async: false,
912            cancellable: false,
913            deprecated: None,
914            since: None,
915        }
916    }
917
918    fn simple_module(name: &str) -> Module {
919        Module {
920            name: name.to_string(),
921            functions: vec![simple_function("do_stuff")],
922            structs: vec![],
923            enums: vec![],
924            callbacks: vec![],
925            listeners: vec![],
926            errors: None,
927            modules: vec![],
928        }
929    }
930
931    fn simple_api() -> Api {
932        Api {
933            version: "0.1.0".to_string(),
934            modules: vec![simple_module("mymod")],
935            generators: None,
936        }
937    }
938
939    #[test]
940    fn valid_api_passes() {
941        let mut api = simple_api();
942        assert!(validate_api(&mut api, None).is_ok());
943    }
944
945    #[test]
946    fn duplicate_module_names_rejected() {
947        let mut api = Api {
948            version: "0.1.0".to_string(),
949            modules: vec![simple_module("dup"), simple_module("dup")],
950            generators: None,
951        };
952        assert!(matches!(
953            validate_api(&mut api, None).unwrap_err().error,
954            ValidationError::DuplicateModuleName(n) if n == "dup"
955        ));
956    }
957
958    #[test]
959    fn duplicate_function_names_rejected() {
960        let mut api = Api {
961            version: "0.1.0".to_string(),
962            modules: vec![Module {
963                name: "mymod".to_string(),
964                functions: vec![simple_function("same"), simple_function("same")],
965                structs: vec![],
966                enums: vec![],
967                callbacks: vec![],
968                listeners: vec![],
969                errors: None,
970                modules: vec![],
971            }],
972            generators: None,
973        };
974        assert!(matches!(
975            validate_api(&mut api, None).unwrap_err().error,
976            ValidationError::DuplicateFunctionName { .. }
977        ));
978    }
979
980    #[test]
981    fn reserved_keywords_rejected() {
982        for kw in ["type", "async"] {
983            let mut api = Api {
984                version: "0.1.0".to_string(),
985                modules: vec![Module {
986                    name: kw.to_string(),
987                    functions: vec![simple_function("ok_fn")],
988                    structs: vec![],
989                    enums: vec![],
990                    callbacks: vec![],
991                    listeners: vec![],
992                    errors: None,
993                    modules: vec![],
994                }],
995                generators: None,
996            };
997            assert!(
998                validate_api(&mut api, None).is_err(),
999                "Expected reserved keyword '{kw}' to be rejected"
1000            );
1001        }
1002    }
1003
1004    #[test]
1005    fn invalid_identifiers_rejected() {
1006        for bad in ["123", "has spaces", ""] {
1007            let mut api = Api {
1008                version: "0.1.0".to_string(),
1009                modules: vec![Module {
1010                    name: bad.to_string(),
1011                    functions: vec![simple_function("ok_fn")],
1012                    structs: vec![],
1013                    enums: vec![],
1014                    callbacks: vec![],
1015                    listeners: vec![],
1016                    errors: None,
1017                    modules: vec![],
1018                }],
1019                generators: None,
1020            };
1021            assert!(
1022                validate_api(&mut api, None).is_err(),
1023                "Expected invalid identifier '{bad}' to be rejected"
1024            );
1025        }
1026    }
1027
1028    #[test]
1029    fn async_function_passes_validation() {
1030        let mut api = Api {
1031            version: "0.1.0".to_string(),
1032            modules: vec![Module {
1033                name: "mymod".to_string(),
1034                functions: vec![Function {
1035                    name: "do_async".to_string(),
1036                    params: vec![],
1037                    returns: None,
1038                    doc: None,
1039                    r#async: true,
1040                    cancellable: false,
1041                    deprecated: None,
1042                    since: None,
1043                }],
1044                structs: vec![],
1045                enums: vec![],
1046                callbacks: vec![],
1047                listeners: vec![],
1048                errors: None,
1049                modules: vec![],
1050            }],
1051            generators: None,
1052        };
1053        assert!(validate_api(&mut api, None).is_ok());
1054    }
1055
1056    #[test]
1057    fn async_function_with_return_passes() {
1058        let mut api = Api {
1059            version: "0.1.0".to_string(),
1060            modules: vec![Module {
1061                name: "mymod".to_string(),
1062                functions: vec![Function {
1063                    name: "fetch_data".to_string(),
1064                    params: vec![Param {
1065                        name: "url".to_string(),
1066                        ty: TypeRef::StringUtf8,
1067                        mutable: false,
1068                        doc: None,
1069                    }],
1070                    returns: Some(TypeRef::StringUtf8),
1071                    doc: None,
1072                    r#async: true,
1073                    cancellable: false,
1074                    deprecated: None,
1075                    since: None,
1076                }],
1077                structs: vec![],
1078                enums: vec![],
1079                callbacks: vec![],
1080                listeners: vec![],
1081                errors: None,
1082                modules: vec![],
1083            }],
1084            generators: None,
1085        };
1086        assert!(validate_api(&mut api, None).is_ok());
1087    }
1088
1089    #[test]
1090    fn async_void_function_emits_warning() {
1091        let api = Api {
1092            version: "0.1.0".to_string(),
1093            modules: vec![Module {
1094                name: "mymod".to_string(),
1095                functions: vec![Function {
1096                    name: "fire_and_forget".to_string(),
1097                    params: vec![],
1098                    returns: None,
1099                    doc: Some("documented".to_string()),
1100                    r#async: true,
1101                    cancellable: false,
1102                    deprecated: None,
1103                    since: None,
1104                }],
1105                structs: vec![],
1106                enums: vec![],
1107                callbacks: vec![],
1108                listeners: vec![],
1109                errors: None,
1110                modules: vec![],
1111            }],
1112            generators: None,
1113        };
1114        let warnings = collect_warnings(&api);
1115        assert!(warnings.iter().any(|w| matches!(
1116            w,
1117            ValidationWarning::AsyncVoidFunction { module, function }
1118                if module == "mymod" && function == "fire_and_forget"
1119        )));
1120    }
1121
1122    #[test]
1123    fn async_function_with_return_no_void_warning() {
1124        let api = Api {
1125            version: "0.1.0".to_string(),
1126            modules: vec![Module {
1127                name: "mymod".to_string(),
1128                functions: vec![Function {
1129                    name: "fetch".to_string(),
1130                    params: vec![],
1131                    returns: Some(TypeRef::StringUtf8),
1132                    doc: Some("documented".to_string()),
1133                    r#async: true,
1134                    cancellable: false,
1135                    deprecated: None,
1136                    since: None,
1137                }],
1138                structs: vec![],
1139                enums: vec![],
1140                callbacks: vec![],
1141                listeners: vec![],
1142                errors: None,
1143                modules: vec![],
1144            }],
1145            generators: None,
1146        };
1147        let warnings = collect_warnings(&api);
1148        assert!(!warnings
1149            .iter()
1150            .any(|w| matches!(w, ValidationWarning::AsyncVoidFunction { .. })));
1151    }
1152
1153    #[test]
1154    fn empty_module_name_rejected() {
1155        let mut api = Api {
1156            version: "0.1.0".to_string(),
1157            modules: vec![Module {
1158                name: "".to_string(),
1159                functions: vec![simple_function("ok_fn")],
1160                structs: vec![],
1161                enums: vec![],
1162                callbacks: vec![],
1163                listeners: vec![],
1164                errors: None,
1165                modules: vec![],
1166            }],
1167            generators: None,
1168        };
1169        assert!(matches!(
1170            validate_api(&mut api, None).unwrap_err().error,
1171            ValidationError::NoModuleName
1172        ));
1173    }
1174
1175    #[test]
1176    fn doc_example_error_domain_validates() {
1177        let mut api = Api {
1178            version: "0.1.0".to_string(),
1179            modules: vec![Module {
1180                name: "contacts".to_string(),
1181                functions: vec![
1182                    Function {
1183                        name: "create_contact".to_string(),
1184                        params: vec![
1185                            Param {
1186                                name: "name".to_string(),
1187                                ty: TypeRef::StringUtf8,
1188                                mutable: false,
1189                                doc: None,
1190                            },
1191                            Param {
1192                                name: "email".to_string(),
1193                                ty: TypeRef::StringUtf8,
1194                                mutable: false,
1195                                doc: None,
1196                            },
1197                        ],
1198                        returns: Some(TypeRef::Handle),
1199                        doc: None,
1200                        r#async: false,
1201                        cancellable: false,
1202                        deprecated: None,
1203                        since: None,
1204                    },
1205                    Function {
1206                        name: "get_contact".to_string(),
1207                        params: vec![Param {
1208                            name: "id".to_string(),
1209                            ty: TypeRef::Handle,
1210                            mutable: false,
1211                            doc: None,
1212                        }],
1213                        returns: Some(TypeRef::StringUtf8),
1214                        doc: None,
1215                        r#async: false,
1216                        cancellable: false,
1217                        deprecated: None,
1218                        since: None,
1219                    },
1220                ],
1221                structs: vec![],
1222                enums: vec![],
1223                callbacks: vec![],
1224                listeners: vec![],
1225                errors: Some(ErrorDomain {
1226                    name: "ContactErrors".to_string(),
1227                    codes: vec![
1228                        ErrorCode {
1229                            name: "not_found".to_string(),
1230                            code: 1,
1231                            message: "Contact not found".to_string(),
1232                            doc: None,
1233                        },
1234                        ErrorCode {
1235                            name: "duplicate".to_string(),
1236                            code: 2,
1237                            message: "Contact already exists".to_string(),
1238                            doc: None,
1239                        },
1240                        ErrorCode {
1241                            name: "invalid_email".to_string(),
1242                            code: 3,
1243                            message: "Email address is invalid".to_string(),
1244                            doc: None,
1245                        },
1246                    ],
1247                }),
1248                modules: vec![],
1249            }],
1250            generators: None,
1251        };
1252        assert!(validate_api(&mut api, None).is_ok());
1253    }
1254
1255    #[test]
1256    fn error_code_zero_rejected() {
1257        let mut api = Api {
1258            version: "0.1.0".to_string(),
1259            modules: vec![Module {
1260                name: "mymod".to_string(),
1261                functions: vec![simple_function("ok_fn")],
1262                structs: vec![],
1263                enums: vec![],
1264                callbacks: vec![],
1265                listeners: vec![],
1266                errors: Some(ErrorDomain {
1267                    name: "MyErrors".to_string(),
1268                    codes: vec![ErrorCode {
1269                        name: "success".to_string(),
1270                        code: 0,
1271                        message: "should fail".to_string(),
1272                        doc: None,
1273                    }],
1274                }),
1275                modules: vec![],
1276            }],
1277            generators: None,
1278        };
1279        assert!(matches!(
1280            validate_api(&mut api, None).unwrap_err().error,
1281            ValidationError::InvalidErrorCode { module, name }
1282                if module == "mymod" && name == "success"
1283        ));
1284    }
1285
1286    #[test]
1287    fn error_domain_name_collision_rejected() {
1288        let mut api = Api {
1289            version: "0.1.0".to_string(),
1290            modules: vec![Module {
1291                name: "mymod".to_string(),
1292                functions: vec![simple_function("do_stuff")],
1293                structs: vec![],
1294                enums: vec![],
1295                callbacks: vec![],
1296                listeners: vec![],
1297                errors: Some(ErrorDomain {
1298                    name: "do_stuff".to_string(),
1299                    codes: vec![ErrorCode {
1300                        name: "fail".to_string(),
1301                        code: 1,
1302                        message: "failed".to_string(),
1303                        doc: None,
1304                    }],
1305                }),
1306                modules: vec![],
1307            }],
1308            generators: None,
1309        };
1310        assert!(matches!(
1311            validate_api(&mut api, None).unwrap_err().error,
1312            ValidationError::NameCollisionWithErrorDomain { module, name }
1313                if module == "mymod" && name == "do_stuff"
1314        ));
1315    }
1316
1317    #[test]
1318    fn duplicate_error_names_rejected() {
1319        let mut api = Api {
1320            version: "0.1.0".to_string(),
1321            modules: vec![Module {
1322                name: "mymod".to_string(),
1323                functions: vec![simple_function("ok_fn")],
1324                structs: vec![],
1325                enums: vec![],
1326                callbacks: vec![],
1327                listeners: vec![],
1328                errors: Some(ErrorDomain {
1329                    name: "MyErrors".to_string(),
1330                    codes: vec![
1331                        ErrorCode {
1332                            name: "fail".to_string(),
1333                            code: 1,
1334                            message: "failed".to_string(),
1335                            doc: None,
1336                        },
1337                        ErrorCode {
1338                            name: "fail".to_string(),
1339                            code: 2,
1340                            message: "also failed".to_string(),
1341                            doc: None,
1342                        },
1343                    ],
1344                }),
1345                modules: vec![],
1346            }],
1347            generators: None,
1348        };
1349        assert!(matches!(
1350            validate_api(&mut api, None).unwrap_err().error,
1351            ValidationError::DuplicateErrorName { module, name }
1352                if module == "mymod" && name == "fail"
1353        ));
1354    }
1355
1356    #[test]
1357    fn duplicate_error_codes_rejected() {
1358        let mut api = Api {
1359            version: "0.1.0".to_string(),
1360            modules: vec![Module {
1361                name: "mymod".to_string(),
1362                functions: vec![simple_function("ok_fn")],
1363                structs: vec![],
1364                enums: vec![],
1365                callbacks: vec![],
1366                listeners: vec![],
1367                errors: Some(ErrorDomain {
1368                    name: "MyErrors".to_string(),
1369                    codes: vec![
1370                        ErrorCode {
1371                            name: "not_found".to_string(),
1372                            code: 1,
1373                            message: "not found".to_string(),
1374                            doc: None,
1375                        },
1376                        ErrorCode {
1377                            name: "timeout".to_string(),
1378                            code: 1,
1379                            message: "timed out".to_string(),
1380                            doc: None,
1381                        },
1382                    ],
1383                }),
1384                modules: vec![],
1385            }],
1386            generators: None,
1387        };
1388        assert!(matches!(
1389            validate_api(&mut api, None).unwrap_err().error,
1390            ValidationError::DuplicateErrorCode { .. }
1391        ));
1392    }
1393
1394    fn simple_struct(name: &str) -> StructDef {
1395        StructDef {
1396            name: name.to_string(),
1397            doc: None,
1398            fields: vec![StructField {
1399                name: "x".to_string(),
1400                ty: TypeRef::I32,
1401                doc: None,
1402                default: None,
1403            }],
1404            builder: false,
1405        }
1406    }
1407
1408    #[test]
1409    fn duplicate_struct_names_rejected() {
1410        let mut api = Api {
1411            version: "0.1.0".to_string(),
1412            modules: vec![Module {
1413                name: "mymod".to_string(),
1414                functions: vec![simple_function("ok_fn")],
1415                structs: vec![simple_struct("Point"), simple_struct("Point")],
1416                enums: vec![],
1417                callbacks: vec![],
1418                listeners: vec![],
1419                errors: None,
1420                modules: vec![],
1421            }],
1422            generators: None,
1423        };
1424        assert!(matches!(
1425            validate_api(&mut api, None).unwrap_err().error,
1426            ValidationError::DuplicateStructName { module, name }
1427                if module == "mymod" && name == "Point"
1428        ));
1429    }
1430
1431    #[test]
1432    fn empty_struct_rejected() {
1433        let mut api = Api {
1434            version: "0.1.0".to_string(),
1435            modules: vec![Module {
1436                name: "mymod".to_string(),
1437                functions: vec![simple_function("ok_fn")],
1438                structs: vec![StructDef {
1439                    name: "Empty".to_string(),
1440                    doc: None,
1441                    fields: vec![],
1442                    builder: false,
1443                }],
1444                enums: vec![],
1445                callbacks: vec![],
1446                listeners: vec![],
1447                errors: None,
1448                modules: vec![],
1449            }],
1450            generators: None,
1451        };
1452        assert!(matches!(
1453            validate_api(&mut api, None).unwrap_err().error,
1454            ValidationError::EmptyStruct { module, name }
1455                if module == "mymod" && name == "Empty"
1456        ));
1457    }
1458
1459    #[test]
1460    fn duplicate_struct_field_names_rejected() {
1461        let mut api = Api {
1462            version: "0.1.0".to_string(),
1463            modules: vec![Module {
1464                name: "mymod".to_string(),
1465                functions: vec![simple_function("ok_fn")],
1466                structs: vec![StructDef {
1467                    name: "Point".to_string(),
1468                    doc: None,
1469                    fields: vec![
1470                        StructField {
1471                            name: "x".to_string(),
1472                            ty: TypeRef::I32,
1473                            doc: None,
1474                            default: None,
1475                        },
1476                        StructField {
1477                            name: "x".to_string(),
1478                            ty: TypeRef::F64,
1479                            doc: None,
1480                            default: None,
1481                        },
1482                    ],
1483                    builder: false,
1484                }],
1485                enums: vec![],
1486                callbacks: vec![],
1487                listeners: vec![],
1488                errors: None,
1489                modules: vec![],
1490            }],
1491            generators: None,
1492        };
1493        assert!(matches!(
1494            validate_api(&mut api, None).unwrap_err().error,
1495            ValidationError::DuplicateStructField { struct_name, field }
1496                if struct_name == "Point" && field == "x"
1497        ));
1498    }
1499
1500    fn simple_enum(name: &str) -> EnumDef {
1501        EnumDef {
1502            name: name.to_string(),
1503            doc: None,
1504            variants: vec![
1505                EnumVariant {
1506                    name: "A".to_string(),
1507                    value: 0,
1508                    doc: None,
1509                },
1510                EnumVariant {
1511                    name: "B".to_string(),
1512                    value: 1,
1513                    doc: None,
1514                },
1515            ],
1516        }
1517    }
1518
1519    #[test]
1520    fn duplicate_enum_names_rejected() {
1521        let mut api = Api {
1522            version: "0.1.0".to_string(),
1523            modules: vec![Module {
1524                name: "mymod".to_string(),
1525                functions: vec![simple_function("ok_fn")],
1526                structs: vec![],
1527                enums: vec![simple_enum("Color"), simple_enum("Color")],
1528                callbacks: vec![],
1529                listeners: vec![],
1530                errors: None,
1531                modules: vec![],
1532            }],
1533            generators: None,
1534        };
1535        assert!(matches!(
1536            validate_api(&mut api, None).unwrap_err().error,
1537            ValidationError::DuplicateEnumName { module, name }
1538                if module == "mymod" && name == "Color"
1539        ));
1540    }
1541
1542    #[test]
1543    fn empty_enum_rejected() {
1544        let mut api = Api {
1545            version: "0.1.0".to_string(),
1546            modules: vec![Module {
1547                name: "mymod".to_string(),
1548                functions: vec![simple_function("ok_fn")],
1549                structs: vec![],
1550                enums: vec![EnumDef {
1551                    name: "Empty".to_string(),
1552                    doc: None,
1553                    variants: vec![],
1554                }],
1555                callbacks: vec![],
1556                listeners: vec![],
1557                errors: None,
1558                modules: vec![],
1559            }],
1560            generators: None,
1561        };
1562        assert!(matches!(
1563            validate_api(&mut api, None).unwrap_err().error,
1564            ValidationError::EmptyEnum { module, name }
1565                if module == "mymod" && name == "Empty"
1566        ));
1567    }
1568
1569    #[test]
1570    fn duplicate_enum_variant_rejected() {
1571        let mut api = Api {
1572            version: "0.1.0".to_string(),
1573            modules: vec![Module {
1574                name: "mymod".to_string(),
1575                functions: vec![simple_function("ok_fn")],
1576                structs: vec![],
1577                enums: vec![EnumDef {
1578                    name: "Color".to_string(),
1579                    doc: None,
1580                    variants: vec![
1581                        EnumVariant {
1582                            name: "Red".to_string(),
1583                            value: 0,
1584                            doc: None,
1585                        },
1586                        EnumVariant {
1587                            name: "Red".to_string(),
1588                            value: 1,
1589                            doc: None,
1590                        },
1591                    ],
1592                }],
1593                callbacks: vec![],
1594                listeners: vec![],
1595                errors: None,
1596                modules: vec![],
1597            }],
1598            generators: None,
1599        };
1600        assert!(matches!(
1601            validate_api(&mut api, None).unwrap_err().error,
1602            ValidationError::DuplicateEnumVariant { enum_name, variant }
1603                if enum_name == "Color" && variant == "Red"
1604        ));
1605    }
1606
1607    #[test]
1608    fn duplicate_enum_value_rejected() {
1609        let mut api = Api {
1610            version: "0.1.0".to_string(),
1611            modules: vec![Module {
1612                name: "mymod".to_string(),
1613                functions: vec![simple_function("ok_fn")],
1614                structs: vec![],
1615                enums: vec![EnumDef {
1616                    name: "Color".to_string(),
1617                    doc: None,
1618                    variants: vec![
1619                        EnumVariant {
1620                            name: "Red".to_string(),
1621                            value: 0,
1622                            doc: None,
1623                        },
1624                        EnumVariant {
1625                            name: "Green".to_string(),
1626                            value: 0,
1627                            doc: None,
1628                        },
1629                    ],
1630                }],
1631                callbacks: vec![],
1632                listeners: vec![],
1633                errors: None,
1634                modules: vec![],
1635            }],
1636            generators: None,
1637        };
1638        assert!(matches!(
1639            validate_api(&mut api, None).unwrap_err().error,
1640            ValidationError::DuplicateEnumValue { enum_name, value }
1641                if enum_name == "Color" && value == 0
1642        ));
1643    }
1644
1645    #[test]
1646    fn unknown_type_ref_rejected() {
1647        let mut api = Api {
1648            version: "0.1.0".to_string(),
1649            modules: vec![Module {
1650                name: "mymod".to_string(),
1651                functions: vec![Function {
1652                    name: "do_stuff".to_string(),
1653                    params: vec![Param {
1654                        name: "x".to_string(),
1655                        ty: TypeRef::Struct("Foo".to_string()),
1656                        mutable: false,
1657                        doc: None,
1658                    }],
1659                    returns: None,
1660                    doc: None,
1661                    r#async: false,
1662                    cancellable: false,
1663                    deprecated: None,
1664                    since: None,
1665                }],
1666                structs: vec![],
1667                enums: vec![],
1668                callbacks: vec![],
1669                listeners: vec![],
1670                errors: None,
1671                modules: vec![],
1672            }],
1673            generators: None,
1674        };
1675        assert!(matches!(
1676            validate_api(&mut api, None).unwrap_err().error,
1677            ValidationError::UnknownTypeRef { name } if name == "Foo"
1678        ));
1679    }
1680
1681    #[test]
1682    fn valid_struct_ref_passes() {
1683        let mut api = Api {
1684            version: "0.1.0".to_string(),
1685            modules: vec![Module {
1686                name: "mymod".to_string(),
1687                functions: vec![Function {
1688                    name: "do_stuff".to_string(),
1689                    params: vec![Param {
1690                        name: "p".to_string(),
1691                        ty: TypeRef::Struct("Point".to_string()),
1692                        mutable: false,
1693                        doc: None,
1694                    }],
1695                    returns: None,
1696                    doc: None,
1697                    r#async: false,
1698                    cancellable: false,
1699                    deprecated: None,
1700                    since: None,
1701                }],
1702                structs: vec![simple_struct("Point")],
1703                enums: vec![],
1704                callbacks: vec![],
1705                listeners: vec![],
1706                errors: None,
1707                modules: vec![],
1708            }],
1709            generators: None,
1710        };
1711        assert!(validate_api(&mut api, None).is_ok());
1712    }
1713
1714    #[test]
1715    fn unknown_type_ref_in_optional_rejected() {
1716        let mut api = Api {
1717            version: "0.1.0".to_string(),
1718            modules: vec![Module {
1719                name: "mymod".to_string(),
1720                functions: vec![Function {
1721                    name: "do_stuff".to_string(),
1722                    params: vec![Param {
1723                        name: "x".to_string(),
1724                        ty: TypeRef::Optional(Box::new(TypeRef::Struct("Bar".to_string()))),
1725                        mutable: false,
1726                        doc: None,
1727                    }],
1728                    returns: None,
1729                    doc: None,
1730                    r#async: false,
1731                    cancellable: false,
1732                    deprecated: None,
1733                    since: None,
1734                }],
1735                structs: vec![],
1736                enums: vec![],
1737                callbacks: vec![],
1738                listeners: vec![],
1739                errors: None,
1740                modules: vec![],
1741            }],
1742            generators: None,
1743        };
1744        assert!(matches!(
1745            validate_api(&mut api, None).unwrap_err().error,
1746            ValidationError::UnknownTypeRef { name } if name == "Bar"
1747        ));
1748    }
1749
1750    #[test]
1751    fn unknown_type_ref_in_list_rejected() {
1752        let mut api = Api {
1753            version: "0.1.0".to_string(),
1754            modules: vec![Module {
1755                name: "mymod".to_string(),
1756                functions: vec![Function {
1757                    name: "do_stuff".to_string(),
1758                    params: vec![],
1759                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Baz".to_string())))),
1760                    doc: None,
1761                    r#async: false,
1762                    cancellable: false,
1763                    deprecated: None,
1764                    since: None,
1765                }],
1766                structs: vec![],
1767                enums: vec![],
1768                callbacks: vec![],
1769                listeners: vec![],
1770                errors: None,
1771                modules: vec![],
1772            }],
1773            generators: None,
1774        };
1775        assert!(matches!(
1776            validate_api(&mut api, None).unwrap_err().error,
1777            ValidationError::UnknownTypeRef { name } if name == "Baz"
1778        ));
1779    }
1780
1781    #[test]
1782    fn struct_field_referencing_unknown_type() {
1783        let mut api = Api {
1784            version: "0.1.0".to_string(),
1785            modules: vec![Module {
1786                name: "mymod".to_string(),
1787                functions: vec![simple_function("ok_fn")],
1788                structs: vec![StructDef {
1789                    name: "Wrapper".to_string(),
1790                    doc: None,
1791                    fields: vec![StructField {
1792                        name: "inner".to_string(),
1793                        ty: TypeRef::Struct("Nonexistent".to_string()),
1794                        doc: None,
1795                        default: None,
1796                    }],
1797                    builder: false,
1798                }],
1799                enums: vec![],
1800                callbacks: vec![],
1801                listeners: vec![],
1802                errors: None,
1803                modules: vec![],
1804            }],
1805            generators: None,
1806        };
1807        assert!(matches!(
1808            validate_api(&mut api, None).unwrap_err().error,
1809            ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
1810        ));
1811    }
1812
1813    #[test]
1814    fn function_param_with_optional_struct() {
1815        let mut api = Api {
1816            version: "0.1.0".to_string(),
1817            modules: vec![Module {
1818                name: "mymod".to_string(),
1819                functions: vec![Function {
1820                    name: "save".to_string(),
1821                    params: vec![Param {
1822                        name: "c".to_string(),
1823                        ty: TypeRef::Optional(Box::new(TypeRef::Struct("Contact".to_string()))),
1824                        mutable: false,
1825                        doc: None,
1826                    }],
1827                    returns: None,
1828                    doc: None,
1829                    r#async: false,
1830                    cancellable: false,
1831                    deprecated: None,
1832                    since: None,
1833                }],
1834                structs: vec![StructDef {
1835                    name: "Contact".to_string(),
1836                    doc: None,
1837                    fields: vec![StructField {
1838                        name: "name".to_string(),
1839                        ty: TypeRef::StringUtf8,
1840                        doc: None,
1841                        default: None,
1842                    }],
1843                    builder: false,
1844                }],
1845                enums: vec![],
1846                callbacks: vec![],
1847                listeners: vec![],
1848                errors: None,
1849                modules: vec![],
1850            }],
1851            generators: None,
1852        };
1853        assert!(validate_api(&mut api, None).is_ok());
1854    }
1855
1856    #[test]
1857    fn function_param_with_list_of_enums() {
1858        let mut api = Api {
1859            version: "0.1.0".to_string(),
1860            modules: vec![Module {
1861                name: "mymod".to_string(),
1862                functions: vec![Function {
1863                    name: "paint".to_string(),
1864                    params: vec![Param {
1865                        name: "colors".to_string(),
1866                        ty: TypeRef::List(Box::new(TypeRef::Enum("Color".to_string()))),
1867                        mutable: false,
1868                        doc: None,
1869                    }],
1870                    returns: None,
1871                    doc: None,
1872                    r#async: false,
1873                    cancellable: false,
1874                    deprecated: None,
1875                    since: None,
1876                }],
1877                structs: vec![],
1878                enums: vec![simple_enum("Color")],
1879                callbacks: vec![],
1880                listeners: vec![],
1881                errors: None,
1882                modules: vec![],
1883            }],
1884            generators: None,
1885        };
1886        assert!(validate_api(&mut api, None).is_ok());
1887    }
1888
1889    #[test]
1890    fn nested_optional_list_validates() {
1891        let mut api = Api {
1892            version: "0.1.0".to_string(),
1893            modules: vec![Module {
1894                name: "mymod".to_string(),
1895                functions: vec![Function {
1896                    name: "list_contacts".to_string(),
1897                    params: vec![],
1898                    returns: Some(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
1899                        TypeRef::Struct("Contact".to_string()),
1900                    ))))),
1901                    doc: None,
1902                    r#async: false,
1903                    cancellable: false,
1904                    deprecated: None,
1905                    since: None,
1906                }],
1907                structs: vec![StructDef {
1908                    name: "Contact".to_string(),
1909                    doc: None,
1910                    fields: vec![StructField {
1911                        name: "name".to_string(),
1912                        ty: TypeRef::StringUtf8,
1913                        doc: None,
1914                        default: None,
1915                    }],
1916                    builder: false,
1917                }],
1918                enums: vec![],
1919                callbacks: vec![],
1920                listeners: vec![],
1921                errors: None,
1922                modules: vec![],
1923            }],
1924            generators: None,
1925        };
1926        assert!(validate_api(&mut api, None).is_ok());
1927    }
1928
1929    #[test]
1930    fn enum_variant_value_zero_allowed() {
1931        let mut api = Api {
1932            version: "0.1.0".to_string(),
1933            modules: vec![Module {
1934                name: "mymod".to_string(),
1935                functions: vec![simple_function("ok_fn")],
1936                structs: vec![],
1937                enums: vec![EnumDef {
1938                    name: "Status".to_string(),
1939                    doc: None,
1940                    variants: vec![
1941                        EnumVariant {
1942                            name: "Unknown".to_string(),
1943                            value: 0,
1944                            doc: None,
1945                        },
1946                        EnumVariant {
1947                            name: "Active".to_string(),
1948                            value: 1,
1949                            doc: None,
1950                        },
1951                    ],
1952                }],
1953                callbacks: vec![],
1954                listeners: vec![],
1955                errors: None,
1956                modules: vec![],
1957            }],
1958            generators: None,
1959        };
1960        assert!(validate_api(&mut api, None).is_ok());
1961    }
1962
1963    #[test]
1964    fn valid_enum_ref_passes() {
1965        let mut api = Api {
1966            version: "0.1.0".to_string(),
1967            modules: vec![Module {
1968                name: "mymod".to_string(),
1969                functions: vec![Function {
1970                    name: "get_color".to_string(),
1971                    params: vec![],
1972                    returns: Some(TypeRef::Enum("Color".to_string())),
1973                    doc: None,
1974                    r#async: false,
1975                    cancellable: false,
1976                    deprecated: None,
1977                    since: None,
1978                }],
1979                structs: vec![],
1980                enums: vec![simple_enum("Color")],
1981                callbacks: vec![],
1982                listeners: vec![],
1983                errors: None,
1984                modules: vec![],
1985            }],
1986            generators: None,
1987        };
1988        assert!(validate_api(&mut api, None).is_ok());
1989    }
1990
1991    #[test]
1992    fn resolve_enum_ref_in_function_param() {
1993        let mut api = Api {
1994            version: "0.1.0".to_string(),
1995            modules: vec![Module {
1996                name: "mymod".to_string(),
1997                functions: vec![Function {
1998                    name: "paint".to_string(),
1999                    params: vec![Param {
2000                        name: "color".to_string(),
2001                        ty: TypeRef::Struct("Color".to_string()),
2002                        mutable: false,
2003                        doc: None,
2004                    }],
2005                    returns: None,
2006                    doc: None,
2007                    r#async: false,
2008                    cancellable: false,
2009                    deprecated: None,
2010                    since: None,
2011                }],
2012                structs: vec![],
2013                enums: vec![simple_enum("Color")],
2014                callbacks: vec![],
2015                listeners: vec![],
2016                errors: None,
2017                modules: vec![],
2018            }],
2019            generators: None,
2020        };
2021        validate_api(&mut api, None).unwrap();
2022        assert_eq!(
2023            api.modules[0].functions[0].params[0].ty,
2024            TypeRef::Enum("Color".to_string())
2025        );
2026    }
2027
2028    #[test]
2029    fn resolve_enum_ref_in_optional() {
2030        let mut api = Api {
2031            version: "0.1.0".to_string(),
2032            modules: vec![Module {
2033                name: "mymod".to_string(),
2034                functions: vec![Function {
2035                    name: "paint".to_string(),
2036                    params: vec![Param {
2037                        name: "color".to_string(),
2038                        ty: TypeRef::Optional(Box::new(TypeRef::Struct("Color".to_string()))),
2039                        mutable: false,
2040                        doc: None,
2041                    }],
2042                    returns: None,
2043                    doc: None,
2044                    r#async: false,
2045                    cancellable: false,
2046                    deprecated: None,
2047                    since: None,
2048                }],
2049                structs: vec![],
2050                enums: vec![simple_enum("Color")],
2051                callbacks: vec![],
2052                listeners: vec![],
2053                errors: None,
2054                modules: vec![],
2055            }],
2056            generators: None,
2057        };
2058        validate_api(&mut api, None).unwrap();
2059        assert_eq!(
2060            api.modules[0].functions[0].params[0].ty,
2061            TypeRef::Optional(Box::new(TypeRef::Enum("Color".to_string())))
2062        );
2063    }
2064
2065    #[test]
2066    fn struct_ref_not_changed() {
2067        let mut api = Api {
2068            version: "0.1.0".to_string(),
2069            modules: vec![Module {
2070                name: "mymod".to_string(),
2071                functions: vec![Function {
2072                    name: "save".to_string(),
2073                    params: vec![Param {
2074                        name: "c".to_string(),
2075                        ty: TypeRef::Struct("Contact".to_string()),
2076                        mutable: false,
2077                        doc: None,
2078                    }],
2079                    returns: None,
2080                    doc: None,
2081                    r#async: false,
2082                    cancellable: false,
2083                    deprecated: None,
2084                    since: None,
2085                }],
2086                structs: vec![simple_struct("Contact")],
2087                enums: vec![],
2088                callbacks: vec![],
2089                listeners: vec![],
2090                errors: None,
2091                modules: vec![],
2092            }],
2093            generators: None,
2094        };
2095        validate_api(&mut api, None).unwrap();
2096        assert_eq!(
2097            api.modules[0].functions[0].params[0].ty,
2098            TypeRef::Struct("Contact".to_string())
2099        );
2100    }
2101
2102    #[test]
2103    fn map_with_string_key_passes() {
2104        let mut api = Api {
2105            version: "0.1.0".to_string(),
2106            modules: vec![Module {
2107                name: "mymod".to_string(),
2108                functions: vec![Function {
2109                    name: "get_map".to_string(),
2110                    params: vec![],
2111                    returns: Some(TypeRef::Map(
2112                        Box::new(TypeRef::StringUtf8),
2113                        Box::new(TypeRef::I32),
2114                    )),
2115                    doc: None,
2116                    r#async: false,
2117                    cancellable: false,
2118                    deprecated: None,
2119                    since: None,
2120                }],
2121                structs: vec![],
2122                enums: vec![],
2123                callbacks: vec![],
2124                listeners: vec![],
2125                errors: None,
2126                modules: vec![],
2127            }],
2128            generators: None,
2129        };
2130        assert!(validate_api(&mut api, None).is_ok());
2131    }
2132
2133    #[test]
2134    fn map_with_struct_key_rejected() {
2135        let mut api = Api {
2136            version: "0.1.0".to_string(),
2137            modules: vec![Module {
2138                name: "mymod".to_string(),
2139                functions: vec![Function {
2140                    name: "get_map".to_string(),
2141                    params: vec![],
2142                    returns: Some(TypeRef::Map(
2143                        Box::new(TypeRef::Struct("Point".to_string())),
2144                        Box::new(TypeRef::I32),
2145                    )),
2146                    doc: None,
2147                    r#async: false,
2148                    cancellable: false,
2149                    deprecated: None,
2150                    since: None,
2151                }],
2152                structs: vec![simple_struct("Point")],
2153                enums: vec![],
2154                callbacks: vec![],
2155                listeners: vec![],
2156                errors: None,
2157                modules: vec![],
2158            }],
2159            generators: None,
2160        };
2161        assert!(matches!(
2162            validate_api(&mut api, None).unwrap_err().error,
2163            ValidationError::InvalidMapKey { key_type } if key_type == "struct Point"
2164        ));
2165    }
2166
2167    #[test]
2168    fn map_with_enum_key_passes() {
2169        let mut api = Api {
2170            version: "0.1.0".to_string(),
2171            modules: vec![Module {
2172                name: "mymod".to_string(),
2173                functions: vec![Function {
2174                    name: "get_map".to_string(),
2175                    params: vec![],
2176                    returns: Some(TypeRef::Map(
2177                        Box::new(TypeRef::Enum("Color".to_string())),
2178                        Box::new(TypeRef::StringUtf8),
2179                    )),
2180                    doc: None,
2181                    r#async: false,
2182                    cancellable: false,
2183                    deprecated: None,
2184                    since: None,
2185                }],
2186                structs: vec![],
2187                enums: vec![simple_enum("Color")],
2188                callbacks: vec![],
2189                listeners: vec![],
2190                errors: None,
2191                modules: vec![],
2192            }],
2193            generators: None,
2194        };
2195        assert!(validate_api(&mut api, None).is_ok());
2196    }
2197
2198    #[test]
2199    fn warning_large_enum_variant_count() {
2200        let variants: Vec<EnumVariant> = (0..101)
2201            .map(|i| EnumVariant {
2202                name: format!("V{i}"),
2203                value: i,
2204                doc: None,
2205            })
2206            .collect();
2207        let api = Api {
2208            version: "0.1.0".to_string(),
2209            modules: vec![Module {
2210                name: "mymod".to_string(),
2211                functions: vec![simple_function("ok_fn")],
2212                structs: vec![],
2213                enums: vec![EnumDef {
2214                    name: "BigEnum".to_string(),
2215                    doc: None,
2216                    variants,
2217                }],
2218                callbacks: vec![],
2219                listeners: vec![],
2220                errors: None,
2221                modules: vec![],
2222            }],
2223            generators: None,
2224        };
2225        let warnings = collect_warnings(&api);
2226        assert!(warnings.iter().any(|w| matches!(
2227            w,
2228            ValidationWarning::LargeEnumVariantCount { enum_name, count }
2229                if enum_name == "BigEnum" && *count == 101
2230        )));
2231    }
2232
2233    #[test]
2234    fn warning_enum_at_100_no_warning() {
2235        let variants: Vec<EnumVariant> = (0..100)
2236            .map(|i| EnumVariant {
2237                name: format!("V{i}"),
2238                value: i,
2239                doc: None,
2240            })
2241            .collect();
2242        let api = Api {
2243            version: "0.1.0".to_string(),
2244            modules: vec![Module {
2245                name: "mymod".to_string(),
2246                functions: vec![simple_function("ok_fn")],
2247                structs: vec![],
2248                enums: vec![EnumDef {
2249                    name: "BigEnum".to_string(),
2250                    doc: None,
2251                    variants,
2252                }],
2253                callbacks: vec![],
2254                listeners: vec![],
2255                errors: None,
2256                modules: vec![],
2257            }],
2258            generators: None,
2259        };
2260        let warnings = collect_warnings(&api);
2261        assert!(!warnings
2262            .iter()
2263            .any(|w| matches!(w, ValidationWarning::LargeEnumVariantCount { .. })));
2264    }
2265
2266    #[test]
2267    fn warning_deep_nesting_in_param() {
2268        let deep = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2269            Box::new(TypeRef::List(Box::new(TypeRef::I32))),
2270        )))));
2271        let api = Api {
2272            version: "0.1.0".to_string(),
2273            modules: vec![Module {
2274                name: "mymod".to_string(),
2275                functions: vec![Function {
2276                    name: "nested_fn".to_string(),
2277                    params: vec![Param {
2278                        name: "data".to_string(),
2279                        ty: deep,
2280                        mutable: false,
2281                        doc: None,
2282                    }],
2283                    returns: None,
2284                    doc: Some("documented".to_string()),
2285                    r#async: false,
2286                    cancellable: false,
2287                    deprecated: None,
2288                    since: None,
2289                }],
2290                structs: vec![],
2291                enums: vec![],
2292                callbacks: vec![],
2293                listeners: vec![],
2294                errors: None,
2295                modules: vec![],
2296            }],
2297            generators: None,
2298        };
2299        let warnings = collect_warnings(&api);
2300        assert!(warnings.iter().any(|w| matches!(
2301            w,
2302            ValidationWarning::DeepNesting { location, depth }
2303                if location == "mymod::nested_fn::data" && *depth == 4
2304        )));
2305    }
2306
2307    #[test]
2308    fn warning_nesting_at_3_no_warning() {
2309        let nested = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2310            Box::new(TypeRef::I32),
2311        )))));
2312        let api = Api {
2313            version: "0.1.0".to_string(),
2314            modules: vec![Module {
2315                name: "mymod".to_string(),
2316                functions: vec![Function {
2317                    name: "ok_fn".to_string(),
2318                    params: vec![Param {
2319                        name: "data".to_string(),
2320                        ty: nested,
2321                        mutable: false,
2322                        doc: None,
2323                    }],
2324                    returns: None,
2325                    doc: Some("documented".to_string()),
2326                    r#async: false,
2327                    cancellable: false,
2328                    deprecated: None,
2329                    since: None,
2330                }],
2331                structs: vec![],
2332                enums: vec![],
2333                callbacks: vec![],
2334                listeners: vec![],
2335                errors: None,
2336                modules: vec![],
2337            }],
2338            generators: None,
2339        };
2340        let warnings = collect_warnings(&api);
2341        assert!(!warnings
2342            .iter()
2343            .any(|w| matches!(w, ValidationWarning::DeepNesting { .. })));
2344    }
2345
2346    #[test]
2347    fn warning_deep_nesting_in_struct_field() {
2348        let deep = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2349            Box::new(TypeRef::List(Box::new(TypeRef::I32))),
2350        )))));
2351        let api = Api {
2352            version: "0.1.0".to_string(),
2353            modules: vec![Module {
2354                name: "mymod".to_string(),
2355                functions: vec![simple_function("ok_fn")],
2356                structs: vec![StructDef {
2357                    name: "Widget".to_string(),
2358                    doc: None,
2359                    fields: vec![StructField {
2360                        name: "data".to_string(),
2361                        ty: deep,
2362                        doc: None,
2363                        default: None,
2364                    }],
2365                    builder: false,
2366                }],
2367                enums: vec![],
2368                callbacks: vec![],
2369                listeners: vec![],
2370                errors: None,
2371                modules: vec![],
2372            }],
2373            generators: None,
2374        };
2375        let warnings = collect_warnings(&api);
2376        assert!(warnings.iter().any(|w| matches!(
2377            w,
2378            ValidationWarning::DeepNesting { location, .. }
2379                if location == "mymod::Widget::data"
2380        )));
2381    }
2382
2383    #[test]
2384    fn warning_empty_module_doc() {
2385        let api = Api {
2386            version: "0.1.0".to_string(),
2387            modules: vec![Module {
2388                name: "undocumented".to_string(),
2389                functions: vec![
2390                    Function {
2391                        name: "a".to_string(),
2392                        params: vec![],
2393                        returns: None,
2394                        doc: None,
2395                        r#async: false,
2396                        cancellable: false,
2397                        deprecated: None,
2398                        since: None,
2399                    },
2400                    Function {
2401                        name: "b".to_string(),
2402                        params: vec![],
2403                        returns: None,
2404                        doc: None,
2405                        r#async: false,
2406                        cancellable: false,
2407                        deprecated: None,
2408                        since: None,
2409                    },
2410                ],
2411                structs: vec![],
2412                enums: vec![],
2413                callbacks: vec![],
2414                listeners: vec![],
2415                errors: None,
2416                modules: vec![],
2417            }],
2418            generators: None,
2419        };
2420        let warnings = collect_warnings(&api);
2421        assert!(warnings.iter().any(|w| matches!(
2422            w,
2423            ValidationWarning::EmptyModuleDoc { module } if module == "undocumented"
2424        )));
2425    }
2426
2427    #[test]
2428    fn warning_partial_docs_no_warning() {
2429        let api = Api {
2430            version: "0.1.0".to_string(),
2431            modules: vec![Module {
2432                name: "partial".to_string(),
2433                functions: vec![
2434                    Function {
2435                        name: "a".to_string(),
2436                        params: vec![],
2437                        returns: None,
2438                        doc: Some("has doc".to_string()),
2439                        r#async: false,
2440                        cancellable: false,
2441                        deprecated: None,
2442                        since: None,
2443                    },
2444                    Function {
2445                        name: "b".to_string(),
2446                        params: vec![],
2447                        returns: None,
2448                        doc: None,
2449                        r#async: false,
2450                        cancellable: false,
2451                        deprecated: None,
2452                        since: None,
2453                    },
2454                ],
2455                structs: vec![],
2456                enums: vec![],
2457                callbacks: vec![],
2458                listeners: vec![],
2459                errors: None,
2460                modules: vec![],
2461            }],
2462            generators: None,
2463        };
2464        let warnings = collect_warnings(&api);
2465        assert!(!warnings
2466            .iter()
2467            .any(|w| matches!(w, ValidationWarning::EmptyModuleDoc { .. })));
2468    }
2469
2470    #[test]
2471    fn warning_no_functions_no_empty_doc_warning() {
2472        let api = Api {
2473            version: "0.1.0".to_string(),
2474            modules: vec![Module {
2475                name: "empty".to_string(),
2476                functions: vec![],
2477                structs: vec![],
2478                enums: vec![],
2479                callbacks: vec![],
2480                listeners: vec![],
2481                errors: None,
2482                modules: vec![],
2483            }],
2484            generators: None,
2485        };
2486        let warnings = collect_warnings(&api);
2487        assert!(!warnings
2488            .iter()
2489            .any(|w| matches!(w, ValidationWarning::EmptyModuleDoc { .. })));
2490    }
2491
2492    #[test]
2493    fn warning_clean_api_no_warnings() {
2494        let api = Api {
2495            version: "0.1.0".to_string(),
2496            modules: vec![Module {
2497                name: "clean".to_string(),
2498                functions: vec![Function {
2499                    name: "add".to_string(),
2500                    params: vec![Param {
2501                        name: "x".to_string(),
2502                        ty: TypeRef::I32,
2503                        mutable: false,
2504                        doc: None,
2505                    }],
2506                    returns: Some(TypeRef::I32),
2507                    doc: Some("Adds numbers".to_string()),
2508                    r#async: false,
2509                    cancellable: false,
2510                    deprecated: None,
2511                    since: None,
2512                }],
2513                structs: vec![],
2514                enums: vec![simple_enum("Color")],
2515                callbacks: vec![],
2516                listeners: vec![],
2517                errors: None,
2518                modules: vec![],
2519            }],
2520            generators: None,
2521        };
2522        let warnings = collect_warnings(&api);
2523        assert!(warnings.is_empty());
2524    }
2525
2526    #[test]
2527    fn resolve_enum_ref_in_struct_field() {
2528        let mut api = Api {
2529            version: "0.1.0".to_string(),
2530            modules: vec![Module {
2531                name: "mymod".to_string(),
2532                functions: vec![simple_function("ok_fn")],
2533                structs: vec![StructDef {
2534                    name: "Widget".to_string(),
2535                    doc: None,
2536                    fields: vec![StructField {
2537                        name: "color".to_string(),
2538                        ty: TypeRef::Struct("Color".to_string()),
2539                        doc: None,
2540                        default: None,
2541                    }],
2542                    builder: false,
2543                }],
2544                enums: vec![simple_enum("Color")],
2545                callbacks: vec![],
2546                listeners: vec![],
2547                errors: None,
2548                modules: vec![],
2549            }],
2550            generators: None,
2551        };
2552        validate_api(&mut api, None).unwrap();
2553        assert_eq!(
2554            api.modules[0].structs[0].fields[0].ty,
2555            TypeRef::Enum("Color".to_string())
2556        );
2557    }
2558
2559    #[test]
2560    fn typed_handle_valid_struct_passes() {
2561        let mut api = Api {
2562            version: "0.1.0".to_string(),
2563            modules: vec![Module {
2564                name: "mymod".to_string(),
2565                functions: vec![Function {
2566                    name: "get_session".to_string(),
2567                    params: vec![Param {
2568                        name: "h".to_string(),
2569                        ty: TypeRef::TypedHandle("Session".to_string()),
2570                        mutable: false,
2571                        doc: None,
2572                    }],
2573                    returns: None,
2574                    doc: None,
2575                    r#async: false,
2576                    cancellable: false,
2577                    deprecated: None,
2578                    since: None,
2579                }],
2580                structs: vec![simple_struct("Session")],
2581                enums: vec![],
2582                callbacks: vec![],
2583                listeners: vec![],
2584                errors: None,
2585                modules: vec![],
2586            }],
2587            generators: None,
2588        };
2589        assert!(validate_api(&mut api, None).is_ok());
2590    }
2591
2592    #[test]
2593    fn typed_handle_unknown_struct_rejected() {
2594        let mut api = Api {
2595            version: "0.1.0".to_string(),
2596            modules: vec![Module {
2597                name: "mymod".to_string(),
2598                functions: vec![Function {
2599                    name: "get_session".to_string(),
2600                    params: vec![Param {
2601                        name: "h".to_string(),
2602                        ty: TypeRef::TypedHandle("Nonexistent".to_string()),
2603                        mutable: false,
2604                        doc: None,
2605                    }],
2606                    returns: None,
2607                    doc: None,
2608                    r#async: false,
2609                    cancellable: false,
2610                    deprecated: None,
2611                    since: None,
2612                }],
2613                structs: vec![],
2614                enums: vec![],
2615                callbacks: vec![],
2616                listeners: vec![],
2617                errors: None,
2618                modules: vec![],
2619            }],
2620            generators: None,
2621        };
2622        assert!(matches!(
2623            validate_api(&mut api, None).unwrap_err().error,
2624            ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
2625        ));
2626    }
2627
2628    #[test]
2629    fn borrowed_str_param_accepted() {
2630        let mut api = Api {
2631            version: "0.1.0".to_string(),
2632            modules: vec![Module {
2633                name: "io".to_string(),
2634                functions: vec![Function {
2635                    name: "write".to_string(),
2636                    params: vec![Param {
2637                        name: "data".to_string(),
2638                        ty: TypeRef::BorrowedStr,
2639                        mutable: false,
2640                        doc: None,
2641                    }],
2642                    returns: None,
2643                    doc: None,
2644                    r#async: false,
2645                    cancellable: false,
2646                    deprecated: None,
2647                    since: None,
2648                }],
2649                structs: vec![],
2650                enums: vec![],
2651                callbacks: vec![],
2652                listeners: vec![],
2653                errors: None,
2654                modules: vec![],
2655            }],
2656            generators: None,
2657        };
2658        assert!(validate_api(&mut api, None).is_ok());
2659    }
2660
2661    #[test]
2662    fn borrowed_bytes_param_accepted() {
2663        let mut api = Api {
2664            version: "0.1.0".to_string(),
2665            modules: vec![Module {
2666                name: "io".to_string(),
2667                functions: vec![Function {
2668                    name: "upload".to_string(),
2669                    params: vec![Param {
2670                        name: "raw".to_string(),
2671                        ty: TypeRef::BorrowedBytes,
2672                        mutable: false,
2673                        doc: None,
2674                    }],
2675                    returns: None,
2676                    doc: None,
2677                    r#async: false,
2678                    cancellable: false,
2679                    deprecated: None,
2680                    since: None,
2681                }],
2682                structs: vec![],
2683                enums: vec![],
2684                callbacks: vec![],
2685                listeners: vec![],
2686                errors: None,
2687                modules: vec![],
2688            }],
2689            generators: None,
2690        };
2691        assert!(validate_api(&mut api, None).is_ok());
2692    }
2693
2694    #[test]
2695    fn borrowed_str_in_return_rejected() {
2696        let mut api = Api {
2697            version: "0.1.0".to_string(),
2698            modules: vec![Module {
2699                name: "io".to_string(),
2700                functions: vec![Function {
2701                    name: "read".to_string(),
2702                    params: vec![],
2703                    returns: Some(TypeRef::BorrowedStr),
2704                    doc: None,
2705                    r#async: false,
2706                    cancellable: false,
2707                    deprecated: None,
2708                    since: None,
2709                }],
2710                structs: vec![],
2711                enums: vec![],
2712                callbacks: vec![],
2713                listeners: vec![],
2714                errors: None,
2715                modules: vec![],
2716            }],
2717            generators: None,
2718        };
2719        assert!(matches!(
2720            validate_api(&mut api, None).unwrap_err().error,
2721            ValidationError::BorrowedTypeInInvalidPosition { ty, location }
2722                if ty == "&str" && location.contains("return type")
2723        ));
2724    }
2725
2726    #[test]
2727    fn borrowed_bytes_in_return_rejected() {
2728        let mut api = Api {
2729            version: "0.1.0".to_string(),
2730            modules: vec![Module {
2731                name: "io".to_string(),
2732                functions: vec![Function {
2733                    name: "read_raw".to_string(),
2734                    params: vec![],
2735                    returns: Some(TypeRef::BorrowedBytes),
2736                    doc: None,
2737                    r#async: false,
2738                    cancellable: false,
2739                    deprecated: None,
2740                    since: None,
2741                }],
2742                structs: vec![],
2743                enums: vec![],
2744                callbacks: vec![],
2745                listeners: vec![],
2746                errors: None,
2747                modules: vec![],
2748            }],
2749            generators: None,
2750        };
2751        assert!(matches!(
2752            validate_api(&mut api, None).unwrap_err().error,
2753            ValidationError::BorrowedTypeInInvalidPosition { ty, location }
2754                if ty == "&[u8]" && location.contains("return type")
2755        ));
2756    }
2757
2758    #[test]
2759    fn borrowed_str_in_struct_field_rejected() {
2760        let mut api = Api {
2761            version: "0.1.0".to_string(),
2762            modules: vec![Module {
2763                name: "data".to_string(),
2764                functions: vec![],
2765                structs: vec![StructDef {
2766                    name: "Msg".to_string(),
2767                    fields: vec![StructField {
2768                        name: "text".to_string(),
2769                        ty: TypeRef::BorrowedStr,
2770                        doc: None,
2771                        default: None,
2772                    }],
2773                    builder: false,
2774                    doc: None,
2775                }],
2776                enums: vec![],
2777                callbacks: vec![],
2778                listeners: vec![],
2779                errors: None,
2780                modules: vec![],
2781            }],
2782            generators: None,
2783        };
2784        assert!(matches!(
2785            validate_api(&mut api, None).unwrap_err().error,
2786            ValidationError::BorrowedTypeInInvalidPosition { ty, location }
2787                if ty == "&str" && location.contains("struct")
2788        ));
2789    }
2790
2791    #[test]
2792    fn borrowed_bytes_in_struct_field_rejected() {
2793        let mut api = Api {
2794            version: "0.1.0".to_string(),
2795            modules: vec![Module {
2796                name: "data".to_string(),
2797                functions: vec![],
2798                structs: vec![StructDef {
2799                    name: "Blob".to_string(),
2800                    fields: vec![StructField {
2801                        name: "content".to_string(),
2802                        ty: TypeRef::BorrowedBytes,
2803                        doc: None,
2804                        default: None,
2805                    }],
2806                    builder: false,
2807                    doc: None,
2808                }],
2809                enums: vec![],
2810                callbacks: vec![],
2811                listeners: vec![],
2812                errors: None,
2813                modules: vec![],
2814            }],
2815            generators: None,
2816        };
2817        assert!(matches!(
2818            validate_api(&mut api, None).unwrap_err().error,
2819            ValidationError::BorrowedTypeInInvalidPosition { ty, location }
2820                if ty == "&[u8]" && location.contains("struct")
2821        ));
2822    }
2823
2824    #[test]
2825    fn borrowed_str_nested_in_optional_return_rejected() {
2826        let mut api = Api {
2827            version: "0.1.0".to_string(),
2828            modules: vec![Module {
2829                name: "io".to_string(),
2830                functions: vec![Function {
2831                    name: "maybe_read".to_string(),
2832                    params: vec![],
2833                    returns: Some(TypeRef::Optional(Box::new(TypeRef::BorrowedStr))),
2834                    doc: None,
2835                    r#async: false,
2836                    cancellable: false,
2837                    deprecated: None,
2838                    since: None,
2839                }],
2840                structs: vec![],
2841                enums: vec![],
2842                callbacks: vec![],
2843                listeners: vec![],
2844                errors: None,
2845                modules: vec![],
2846            }],
2847            generators: None,
2848        };
2849        assert!(matches!(
2850            validate_api(&mut api, None).unwrap_err().error,
2851            ValidationError::BorrowedTypeInInvalidPosition { ty, .. }
2852                if ty == "&str"
2853        ));
2854    }
2855
2856    #[test]
2857    fn cross_module_struct_ref_passes() {
2858        let mut api = Api {
2859            version: "0.1.0".to_string(),
2860            modules: vec![
2861                Module {
2862                    name: "orders".to_string(),
2863                    functions: vec![Function {
2864                        name: "place_order".to_string(),
2865                        params: vec![Param {
2866                            name: "item".to_string(),
2867                            ty: TypeRef::Struct("Product".to_string()),
2868                            mutable: false,
2869                            doc: None,
2870                        }],
2871                        returns: None,
2872                        doc: None,
2873                        r#async: false,
2874                        cancellable: false,
2875                        deprecated: None,
2876                        since: None,
2877                    }],
2878                    structs: vec![],
2879                    enums: vec![],
2880                    callbacks: vec![],
2881                    listeners: vec![],
2882                    errors: None,
2883                    modules: vec![],
2884                },
2885                Module {
2886                    name: "catalog".to_string(),
2887                    functions: vec![simple_function("list_products")],
2888                    structs: vec![simple_struct("Product")],
2889                    enums: vec![],
2890                    callbacks: vec![],
2891                    listeners: vec![],
2892                    errors: None,
2893                    modules: vec![],
2894                },
2895            ],
2896            generators: None,
2897        };
2898        validate_api(&mut api, None).unwrap();
2899        assert_eq!(
2900            api.modules[0].functions[0].params[0].ty,
2901            TypeRef::Struct("catalog.Product".to_string())
2902        );
2903    }
2904
2905    #[test]
2906    fn cross_module_enum_ref_passes() {
2907        let mut api = Api {
2908            version: "0.1.0".to_string(),
2909            modules: vec![
2910                Module {
2911                    name: "orders".to_string(),
2912                    functions: vec![Function {
2913                        name: "get_status".to_string(),
2914                        params: vec![],
2915                        returns: Some(TypeRef::Struct("Status".to_string())),
2916                        doc: None,
2917                        r#async: false,
2918                        cancellable: false,
2919                        deprecated: None,
2920                        since: None,
2921                    }],
2922                    structs: vec![],
2923                    enums: vec![],
2924                    callbacks: vec![],
2925                    listeners: vec![],
2926                    errors: None,
2927                    modules: vec![],
2928                },
2929                Module {
2930                    name: "shared".to_string(),
2931                    functions: vec![simple_function("noop")],
2932                    structs: vec![],
2933                    enums: vec![simple_enum("Status")],
2934                    callbacks: vec![],
2935                    listeners: vec![],
2936                    errors: None,
2937                    modules: vec![],
2938                },
2939            ],
2940            generators: None,
2941        };
2942        validate_api(&mut api, None).unwrap();
2943        assert_eq!(
2944            api.modules[0].functions[0].returns,
2945            Some(TypeRef::Enum("shared.Status".to_string()))
2946        );
2947    }
2948
2949    #[test]
2950    fn cross_module_unknown_still_rejected() {
2951        let mut api = Api {
2952            version: "0.1.0".to_string(),
2953            modules: vec![
2954                Module {
2955                    name: "orders".to_string(),
2956                    functions: vec![Function {
2957                        name: "do_stuff".to_string(),
2958                        params: vec![Param {
2959                            name: "x".to_string(),
2960                            ty: TypeRef::Struct("Nonexistent".to_string()),
2961                            mutable: false,
2962                            doc: None,
2963                        }],
2964                        returns: None,
2965                        doc: None,
2966                        r#async: false,
2967                        cancellable: false,
2968                        deprecated: None,
2969                        since: None,
2970                    }],
2971                    structs: vec![],
2972                    enums: vec![],
2973                    callbacks: vec![],
2974                    listeners: vec![],
2975                    errors: None,
2976                    modules: vec![],
2977                },
2978                Module {
2979                    name: "catalog".to_string(),
2980                    functions: vec![simple_function("list_products")],
2981                    structs: vec![simple_struct("Product")],
2982                    enums: vec![],
2983                    callbacks: vec![],
2984                    listeners: vec![],
2985                    errors: None,
2986                    modules: vec![],
2987                },
2988            ],
2989            generators: None,
2990        };
2991        assert!(matches!(
2992            validate_api(&mut api, None).unwrap_err().error,
2993            ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
2994        ));
2995    }
2996
2997    #[test]
2998    fn find_type_in_api_finds_struct() {
2999        let api = Api {
3000            version: "0.1.0".to_string(),
3001            modules: vec![Module {
3002                name: "catalog".to_string(),
3003                functions: vec![],
3004                structs: vec![simple_struct("Product")],
3005                enums: vec![],
3006                callbacks: vec![],
3007                listeners: vec![],
3008                errors: None,
3009                modules: vec![],
3010            }],
3011            generators: None,
3012        };
3013        let result = find_type_in_api(&api, "Product");
3014        assert_eq!(result, Some(("catalog".to_string(), false)));
3015    }
3016
3017    #[test]
3018    fn find_type_in_api_finds_enum() {
3019        let api = Api {
3020            version: "0.1.0".to_string(),
3021            modules: vec![Module {
3022                name: "shared".to_string(),
3023                functions: vec![],
3024                structs: vec![],
3025                enums: vec![simple_enum("Status")],
3026                callbacks: vec![],
3027                listeners: vec![],
3028                errors: None,
3029                modules: vec![],
3030            }],
3031            generators: None,
3032        };
3033        let result = find_type_in_api(&api, "Status");
3034        assert_eq!(result, Some(("shared".to_string(), true)));
3035    }
3036
3037    #[test]
3038    fn find_type_in_api_returns_none_for_unknown() {
3039        let api = Api {
3040            version: "0.1.0".to_string(),
3041            modules: vec![simple_module("mymod")],
3042            generators: None,
3043        };
3044        assert_eq!(find_type_in_api(&api, "Nonexistent"), None);
3045    }
3046
3047    #[test]
3048    fn validate_nested_module_passes() {
3049        let mut api = Api {
3050            version: "0.1.0".to_string(),
3051            modules: vec![Module {
3052                name: "parent".to_string(),
3053                functions: vec![simple_function("top_fn")],
3054                structs: vec![],
3055                enums: vec![],
3056                callbacks: vec![],
3057                listeners: vec![],
3058                errors: None,
3059                modules: vec![Module {
3060                    name: "child".to_string(),
3061                    functions: vec![simple_function("inner_fn")],
3062                    structs: vec![],
3063                    enums: vec![],
3064                    callbacks: vec![],
3065                    listeners: vec![],
3066                    errors: None,
3067                    modules: vec![],
3068                }],
3069            }],
3070            generators: None,
3071        };
3072        assert!(validate_api(&mut api, None).is_ok());
3073    }
3074
3075    #[test]
3076    fn duplicate_callback_names_rejected() {
3077        let mut api = Api {
3078            version: "0.1.0".to_string(),
3079            modules: vec![Module {
3080                name: "events".to_string(),
3081                functions: vec![],
3082                structs: vec![],
3083                enums: vec![],
3084                callbacks: vec![
3085                    CallbackDef {
3086                        name: "on_data".to_string(),
3087                        params: vec![],
3088                        doc: None,
3089                    },
3090                    CallbackDef {
3091                        name: "on_data".to_string(),
3092                        params: vec![],
3093                        doc: None,
3094                    },
3095                ],
3096                listeners: vec![],
3097                errors: None,
3098                modules: vec![],
3099            }],
3100            generators: None,
3101        };
3102        assert!(matches!(
3103            validate_api(&mut api, None).unwrap_err().error,
3104            ValidationError::DuplicateCallbackName { module, name }
3105                if module == "events" && name == "on_data"
3106        ));
3107    }
3108
3109    #[test]
3110    fn listener_referencing_undefined_callback_rejected() {
3111        let mut api = Api {
3112            version: "0.1.0".to_string(),
3113            modules: vec![Module {
3114                name: "events".to_string(),
3115                functions: vec![],
3116                structs: vec![],
3117                enums: vec![],
3118                callbacks: vec![],
3119                listeners: vec![ListenerDef {
3120                    name: "watcher".to_string(),
3121                    event_callback: "nonexistent".to_string(),
3122                    doc: None,
3123                }],
3124                errors: None,
3125                modules: vec![],
3126            }],
3127            generators: None,
3128        };
3129        assert!(matches!(
3130            validate_api(&mut api, None).unwrap_err().error,
3131            ValidationError::ListenerCallbackNotFound { module, listener, callback }
3132                if module == "events" && listener == "watcher" && callback == "nonexistent"
3133        ));
3134    }
3135
3136    #[test]
3137    fn listener_referencing_defined_callback_passes() {
3138        let mut api = Api {
3139            version: "0.1.0".to_string(),
3140            modules: vec![Module {
3141                name: "events".to_string(),
3142                functions: vec![],
3143                structs: vec![],
3144                enums: vec![],
3145                callbacks: vec![CallbackDef {
3146                    name: "on_data".to_string(),
3147                    params: vec![Param {
3148                        name: "payload".to_string(),
3149                        ty: TypeRef::StringUtf8,
3150                        mutable: false,
3151                        doc: None,
3152                    }],
3153                    doc: None,
3154                }],
3155                listeners: vec![ListenerDef {
3156                    name: "data_stream".to_string(),
3157                    event_callback: "on_data".to_string(),
3158                    doc: Some("Subscribe to data".to_string()),
3159                }],
3160                errors: None,
3161                modules: vec![],
3162            }],
3163            generators: None,
3164        };
3165        assert!(validate_api(&mut api, None).is_ok());
3166    }
3167
3168    #[test]
3169    fn duplicate_listener_names_rejected() {
3170        let mut api = Api {
3171            version: "0.1.0".to_string(),
3172            modules: vec![Module {
3173                name: "events".to_string(),
3174                functions: vec![],
3175                structs: vec![],
3176                enums: vec![],
3177                callbacks: vec![CallbackDef {
3178                    name: "on_data".to_string(),
3179                    params: vec![],
3180                    doc: None,
3181                }],
3182                listeners: vec![
3183                    ListenerDef {
3184                        name: "watcher".to_string(),
3185                        event_callback: "on_data".to_string(),
3186                        doc: None,
3187                    },
3188                    ListenerDef {
3189                        name: "watcher".to_string(),
3190                        event_callback: "on_data".to_string(),
3191                        doc: None,
3192                    },
3193                ],
3194                errors: None,
3195                modules: vec![],
3196            }],
3197            generators: None,
3198        };
3199        assert!(matches!(
3200            validate_api(&mut api, None).unwrap_err().error,
3201            ValidationError::DuplicateListenerName { module, name }
3202                if module == "events" && name == "watcher"
3203        ));
3204    }
3205
3206    #[test]
3207    fn iterator_valid_as_return_type() {
3208        let mut api = Api {
3209            version: "0.2.0".to_string(),
3210            modules: vec![Module {
3211                name: "data".to_string(),
3212                functions: vec![Function {
3213                    name: "list_items".to_string(),
3214                    params: vec![],
3215                    returns: Some(TypeRef::Iterator(Box::new(TypeRef::I32))),
3216                    doc: None,
3217                    r#async: false,
3218                    cancellable: false,
3219                    deprecated: None,
3220                    since: None,
3221                }],
3222                structs: vec![],
3223                enums: vec![],
3224                callbacks: vec![],
3225                listeners: vec![],
3226                errors: None,
3227                modules: vec![],
3228            }],
3229            generators: None,
3230        };
3231        assert!(validate_api(&mut api, None).is_ok());
3232    }
3233
3234    #[test]
3235    fn iterator_rejected_as_param() {
3236        let mut api = Api {
3237            version: "0.2.0".to_string(),
3238            modules: vec![Module {
3239                name: "data".to_string(),
3240                functions: vec![Function {
3241                    name: "consume".to_string(),
3242                    params: vec![Param {
3243                        name: "items".to_string(),
3244                        ty: TypeRef::Iterator(Box::new(TypeRef::I32)),
3245                        mutable: false,
3246                        doc: None,
3247                    }],
3248                    returns: None,
3249                    doc: None,
3250                    r#async: false,
3251                    cancellable: false,
3252                    deprecated: None,
3253                    since: None,
3254                }],
3255                structs: vec![],
3256                enums: vec![],
3257                callbacks: vec![],
3258                listeners: vec![],
3259                errors: None,
3260                modules: vec![],
3261            }],
3262            generators: None,
3263        };
3264        assert!(matches!(
3265            validate_api(&mut api, None).unwrap_err().error,
3266            ValidationError::IteratorInInvalidPosition { .. }
3267        ));
3268    }
3269
3270    #[test]
3271    fn iterator_rejected_in_struct_field() {
3272        let mut api = Api {
3273            version: "0.2.0".to_string(),
3274            modules: vec![Module {
3275                name: "data".to_string(),
3276                functions: vec![],
3277                structs: vec![StructDef {
3278                    name: "Container".to_string(),
3279                    doc: None,
3280                    fields: vec![StructField {
3281                        name: "items".to_string(),
3282                        ty: TypeRef::Iterator(Box::new(TypeRef::I32)),
3283                        doc: None,
3284                        default: None,
3285                    }],
3286                    builder: false,
3287                }],
3288                enums: vec![],
3289                callbacks: vec![],
3290                listeners: vec![],
3291                errors: None,
3292                modules: vec![],
3293            }],
3294            generators: None,
3295        };
3296        assert!(matches!(
3297            validate_api(&mut api, None).unwrap_err().error,
3298            ValidationError::IteratorInInvalidPosition { .. }
3299        ));
3300    }
3301
3302    #[test]
3303    fn builder_struct_empty_is_error() {
3304        let mut api = Api {
3305            version: "0.2.0".to_string(),
3306            modules: vec![Module {
3307                name: "m".into(),
3308                functions: vec![],
3309                structs: vec![StructDef {
3310                    name: "Empty".into(),
3311                    doc: None,
3312                    fields: vec![],
3313                    builder: true,
3314                }],
3315                enums: vec![],
3316                callbacks: vec![],
3317                listeners: vec![],
3318                errors: None,
3319                modules: vec![],
3320            }],
3321            generators: None,
3322        };
3323        let err = validate_api(&mut api, None).unwrap_err();
3324        assert!(
3325            matches!(err.error, ValidationError::BuilderStructEmpty { .. }),
3326            "expected BuilderStructEmpty, got: {err}"
3327        );
3328    }
3329
3330    #[test]
3331    fn warning_mutable_on_value_type() {
3332        let api = Api {
3333            version: "0.1.0".to_string(),
3334            modules: vec![Module {
3335                name: "math".to_string(),
3336                functions: vec![Function {
3337                    name: "add".to_string(),
3338                    params: vec![Param {
3339                        name: "x".to_string(),
3340                        ty: TypeRef::I32,
3341                        mutable: true,
3342                        doc: None,
3343                    }],
3344                    returns: Some(TypeRef::I32),
3345                    doc: Some("add".to_string()),
3346                    r#async: false,
3347                    cancellable: false,
3348                    deprecated: None,
3349                    since: None,
3350                }],
3351                structs: vec![],
3352                enums: vec![],
3353                callbacks: vec![],
3354                listeners: vec![],
3355                errors: None,
3356                modules: vec![],
3357            }],
3358            generators: None,
3359        };
3360        let warnings = collect_warnings(&api);
3361        assert!(warnings.iter().any(|w| matches!(
3362            w,
3363            ValidationWarning::MutableOnValueType {
3364                param,
3365                ..
3366            } if param == "x"
3367        )));
3368    }
3369
3370    #[test]
3371    fn no_warning_mutable_on_pointer_type() {
3372        let api = Api {
3373            version: "0.1.0".to_string(),
3374            modules: vec![Module {
3375                name: "io".to_string(),
3376                functions: vec![Function {
3377                    name: "fill".to_string(),
3378                    params: vec![
3379                        Param {
3380                            name: "buf".to_string(),
3381                            ty: TypeRef::Bytes,
3382                            mutable: true,
3383                            doc: None,
3384                        },
3385                        Param {
3386                            name: "msg".to_string(),
3387                            ty: TypeRef::StringUtf8,
3388                            mutable: true,
3389                            doc: None,
3390                        },
3391                        Param {
3392                            name: "obj".to_string(),
3393                            ty: TypeRef::Struct("Thing".into()),
3394                            mutable: true,
3395                            doc: None,
3396                        },
3397                    ],
3398                    returns: None,
3399                    doc: Some("fill".to_string()),
3400                    r#async: false,
3401                    cancellable: false,
3402                    deprecated: None,
3403                    since: None,
3404                }],
3405                structs: vec![],
3406                enums: vec![],
3407                callbacks: vec![],
3408                listeners: vec![],
3409                errors: None,
3410                modules: vec![],
3411            }],
3412            generators: None,
3413        };
3414        let warnings = collect_warnings(&api);
3415        assert!(
3416            !warnings
3417                .iter()
3418                .any(|w| matches!(w, ValidationWarning::MutableOnValueType { .. })),
3419            "pointer types should not trigger mutable warning"
3420        );
3421    }
3422
3423    #[test]
3424    fn no_warning_mutable_false_on_value_type() {
3425        let api = Api {
3426            version: "0.1.0".to_string(),
3427            modules: vec![Module {
3428                name: "math".to_string(),
3429                functions: vec![Function {
3430                    name: "add".to_string(),
3431                    params: vec![Param {
3432                        name: "x".to_string(),
3433                        ty: TypeRef::I32,
3434                        mutable: false,
3435                        doc: None,
3436                    }],
3437                    returns: Some(TypeRef::I32),
3438                    doc: Some("add".to_string()),
3439                    r#async: false,
3440                    cancellable: false,
3441                    deprecated: None,
3442                    since: None,
3443                }],
3444                structs: vec![],
3445                enums: vec![],
3446                callbacks: vec![],
3447                listeners: vec![],
3448                errors: None,
3449                modules: vec![],
3450            }],
3451            generators: None,
3452        };
3453        let warnings = collect_warnings(&api);
3454        assert!(
3455            !warnings
3456                .iter()
3457                .any(|w| matches!(w, ValidationWarning::MutableOnValueType { .. })),
3458            "mutable=false should not trigger warning"
3459        );
3460    }
3461
3462    #[test]
3463    fn warning_mutable_on_enum_type() {
3464        let api = Api {
3465            version: "0.1.0".to_string(),
3466            modules: vec![Module {
3467                name: "paint".to_string(),
3468                functions: vec![Function {
3469                    name: "set_color".to_string(),
3470                    params: vec![Param {
3471                        name: "color".to_string(),
3472                        ty: TypeRef::Enum("Color".into()),
3473                        mutable: true,
3474                        doc: None,
3475                    }],
3476                    returns: None,
3477                    doc: Some("set".to_string()),
3478                    r#async: false,
3479                    cancellable: false,
3480                    deprecated: None,
3481                    since: None,
3482                }],
3483                structs: vec![],
3484                enums: vec![],
3485                callbacks: vec![],
3486                listeners: vec![],
3487                errors: None,
3488                modules: vec![],
3489            }],
3490            generators: None,
3491        };
3492        let warnings = collect_warnings(&api);
3493        assert!(warnings.iter().any(|w| matches!(
3494            w,
3495            ValidationWarning::MutableOnValueType { param, .. } if param == "color"
3496        )));
3497    }
3498
3499    #[test]
3500    fn warning_deprecated_function() {
3501        let api = Api {
3502            version: "0.2.0".to_string(),
3503            modules: vec![Module {
3504                name: "math".to_string(),
3505                functions: vec![Function {
3506                    name: "add_old".to_string(),
3507                    params: vec![],
3508                    returns: Some(TypeRef::I32),
3509                    doc: Some("old add".to_string()),
3510                    r#async: false,
3511                    cancellable: false,
3512                    deprecated: Some("Use add_v2 instead".to_string()),
3513                    since: Some("0.1.0".to_string()),
3514                }],
3515                structs: vec![],
3516                enums: vec![],
3517                callbacks: vec![],
3518                listeners: vec![],
3519                errors: None,
3520                modules: vec![],
3521            }],
3522            generators: None,
3523        };
3524        let warnings = collect_warnings(&api);
3525        assert!(warnings.iter().any(|w| matches!(
3526            w,
3527            ValidationWarning::DeprecatedFunction { function, message, .. }
3528                if function == "add_old" && message == "Use add_v2 instead"
3529        )));
3530    }
3531
3532    #[test]
3533    fn no_warning_for_non_deprecated_function() {
3534        let api = Api {
3535            version: "0.2.0".to_string(),
3536            modules: vec![Module {
3537                name: "math".to_string(),
3538                functions: vec![Function {
3539                    name: "add".to_string(),
3540                    params: vec![],
3541                    returns: Some(TypeRef::I32),
3542                    doc: Some("add things".to_string()),
3543                    r#async: false,
3544                    cancellable: false,
3545                    deprecated: None,
3546                    since: None,
3547                }],
3548                structs: vec![],
3549                enums: vec![],
3550                callbacks: vec![],
3551                listeners: vec![],
3552                errors: None,
3553                modules: vec![],
3554            }],
3555            generators: None,
3556        };
3557        let warnings = collect_warnings(&api);
3558        assert!(!warnings
3559            .iter()
3560            .any(|w| matches!(w, ValidationWarning::DeprecatedFunction { .. })));
3561    }
3562}