1use miette::Diagnostic;
7use std::collections::BTreeSet;
8use weaveffi_ir::ir::{Api, SUPPORTED_VERSIONS};
9
10mod diagnostic;
11mod resolve;
12mod rules;
13#[cfg(test)]
14mod tests;
15mod warnings;
16
17pub use diagnostic::ValidationDiagnostic;
18pub use resolve::{find_type_in_api, resolve_type_refs};
19pub use warnings::{collect_warnings, ValidationWarning};
20
21#[derive(Debug, thiserror::Error, Diagnostic)]
22pub enum ValidationError {
23 #[error("module has no name")]
24 #[diagnostic(help("every module must have a non-empty 'name' field"))]
25 NoModuleName,
26 #[error("duplicate module name: {0}")]
27 #[diagnostic(help(
28 "module names must be unique within an API definition; rename or merge the duplicate"
29 ))]
30 DuplicateModuleName(String),
31 #[error("invalid module name '{0}': {1}")]
32 #[diagnostic(help(
33 "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
34 ))]
35 InvalidModuleName(String, &'static str),
36 #[error("duplicate function name in module '{module}': {function}")]
37 #[diagnostic(help("function names must be unique within a module; rename the duplicate"))]
38 DuplicateFunctionName { module: String, function: String },
39 #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
40 #[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))]
41 DuplicateParamName {
42 module: String,
43 function: String,
44 param: String,
45 },
46 #[error("reserved keyword used: {0}")]
47 #[diagnostic(help("choose a different name that is not a language reserved word"))]
48 ReservedKeyword(String),
49 #[error("invalid identifier '{0}': {1}")]
50 #[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))]
51 InvalidIdentifier(String, &'static str),
52 #[error("error domain missing name in module '{0}'")]
53 #[diagnostic(help("add a non-empty 'name' field to the error domain"))]
54 ErrorDomainMissingName(String),
55 #[error("duplicate error code name in module '{module}': {name}")]
56 #[diagnostic(help("error code names must be unique within a module; rename the duplicate"))]
57 DuplicateErrorName { module: String, name: String },
58 #[error("duplicate error numeric code in module '{module}': {code}")]
59 #[diagnostic(help(
60 "numeric error codes must be unique within a module; assign a different value"
61 ))]
62 DuplicateErrorCode { module: String, code: i32 },
63 #[error("invalid error code in module '{module}' for '{name}': must be non-zero")]
64 #[diagnostic(help("error codes must be non-zero; use a positive or negative integer"))]
65 InvalidErrorCode { module: String, name: String },
66 #[error("function name collides with error domain name in module '{module}': {name}")]
67 #[diagnostic(help(
68 "function and error domain names share a namespace; rename one to avoid the collision"
69 ))]
70 NameCollisionWithErrorDomain { module: String, name: String },
71 #[error("duplicate struct name in module '{module}': {name}")]
72 #[diagnostic(help("struct names must be unique within a module; rename the duplicate"))]
73 DuplicateStructName { module: String, name: String },
74 #[error("duplicate field name in struct '{struct_name}': {field}")]
75 #[diagnostic(help("field names must be unique within a struct; rename the duplicate"))]
76 DuplicateStructField { struct_name: String, field: String },
77 #[error("empty struct in module '{module}': {name}")]
78 #[diagnostic(help("structs must have at least one field; add a field or remove the struct"))]
79 EmptyStruct { module: String, name: String },
80 #[error("duplicate enum name in module '{module}': {name}")]
81 #[diagnostic(help("enum names must be unique within a module; rename the duplicate"))]
82 DuplicateEnumName { module: String, name: String },
83 #[error("empty enum in module '{module}': {name}")]
84 #[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))]
85 EmptyEnum { module: String, name: String },
86 #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
87 #[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))]
88 DuplicateEnumVariant { enum_name: String, variant: String },
89 #[error("duplicate enum value in enum '{enum_name}': {value}")]
90 #[diagnostic(help(
91 "variant numeric values must be unique within an enum; assign a different value"
92 ))]
93 DuplicateEnumValue { enum_name: String, value: i32 },
94 #[error("unknown type reference: {name}")]
95 #[diagnostic(help(
96 "define a struct or enum with this name in the same module, or check for typos"
97 ))]
98 UnknownTypeRef { name: String },
99 #[error("invalid map key type: {key_type}; only primitive types and strings are allowed as map keys")]
100 #[diagnostic(help("map keys must be primitive types (i32, u32, i64, f64, bool, string); structs, lists, and maps cannot be keys"))]
101 InvalidMapKey { key_type: String },
102 #[error(
103 "borrowed type '{ty}' is not valid in {location}; only function parameters are allowed"
104 )]
105 #[diagnostic(help("borrowed types (&str, &[u8]) can only be used as function parameters, not return types or struct fields"))]
106 BorrowedTypeInInvalidPosition { ty: String, location: String },
107 #[error("duplicate callback name in module '{module}': {name}")]
108 #[diagnostic(help("callback names must be unique within a module; rename the duplicate"))]
109 DuplicateCallbackName { module: String, name: String },
110 #[error(
111 "listener '{listener}' in module '{module}' references undefined callback '{callback}'"
112 )]
113 #[diagnostic(help(
114 "listener event_callback must reference a callback defined in the same module"
115 ))]
116 ListenerCallbackNotFound {
117 module: String,
118 listener: String,
119 callback: String,
120 },
121 #[error("duplicate listener name in module '{module}': {name}")]
122 #[diagnostic(help("listener names must be unique within a module; rename the duplicate"))]
123 DuplicateListenerName { module: String, name: String },
124 #[error(
125 "callback '{callback}' in module '{module}' has parameter '{param}' with unsupported \
126 type '{ty}'"
127 )]
128 #[diagnostic(help(
129 "callback parameters are limited to scalars, bool, enums, string, bytes, handles, \
130 structs, optionals of those, lists of scalars/strings, and maps of scalars/strings — \
131 every target must be able to marshal a callback argument without an FFI round-trip"
132 ))]
133 UnsupportedCallbackParamType {
134 module: String,
135 callback: String,
136 param: String,
137 ty: String,
138 },
139 #[error("iterator type is only valid as a function return type, found in {location}")]
140 #[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))]
141 IteratorInInvalidPosition { location: String },
142 #[error("unsupported element type '{ty}' in {location}")]
143 #[diagnostic(help(
144 "the C ABI lowers lists, maps, and iterators to flat parallel arrays, so element \
145 types must be flat: list/iterator elements may be scalars, bool, enums, strings, \
146 handles, or structs (plus optional structs/handles in lists); map keys and values \
147 may be scalars, bool, enums, or strings"
148 ))]
149 UnsupportedElementType { location: String, ty: String },
150 #[error("async function '{module}::{function}' cannot return an iterator")]
151 #[diagnostic(help(
152 "the callback-completed async ABI has no streaming protocol; return a list ([T]) \
153 from the async function, or make the function synchronous and return iter<T>"
154 ))]
155 AsyncIteratorReturn { module: String, function: String },
156 #[error("builder struct '{name}' in module '{module}' must have at least one field")]
157 #[diagnostic(help(
158 "builder structs must have at least one field; add a field or set builder: false"
159 ))]
160 BuilderStructEmpty { module: String, name: String },
161 #[error("unsupported schema version '{version}'; supported versions: {supported}")]
162 #[diagnostic(help(
163 "set the version field to the current schema version and update the \
164 document to match the current schema (see docs/src/idl.md)"
165 ))]
166 UnsupportedSchemaVersion { version: String, supported: String },
167}
168
169#[allow(clippy::result_large_err)]
174pub fn validate_api(
175 api: &mut Api,
176 source: Option<(&str, &str)>,
177) -> Result<(), ValidationDiagnostic> {
178 validate_api_inner(api).map_err(|e| ValidationDiagnostic::new(e, source))
179}
180
181fn validate_api_inner(api: &mut Api) -> Result<(), ValidationError> {
182 if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
183 return Err(ValidationError::UnsupportedSchemaVersion {
184 version: api.version.clone(),
185 supported: SUPPORTED_VERSIONS.join(", "),
186 });
187 }
188 let mut module_names = BTreeSet::new();
189 for m in &api.modules {
190 if !module_names.insert(m.name.clone()) {
191 return Err(ValidationError::DuplicateModuleName(m.name.clone()));
192 }
193 rules::validate_module(m, &api.modules)?;
194 }
195 resolve_type_refs(api);
196 Ok(())
197}