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