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#[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 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("ed) {
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#[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}