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