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)]
28pub enum ValidationError {
29 #[error("module has no name")]
31 #[diagnostic(help("every module must have a non-empty 'name' field"))]
32 NoModuleName,
33 #[error("duplicate module name: {0}")]
35 #[diagnostic(help(
36 "module names must be unique within an API definition; rename or merge the duplicate"
37 ))]
38 DuplicateModuleName(String),
39 #[error("invalid module name '{0}': {1}")]
41 #[diagnostic(help(
42 "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
43 ))]
44 InvalidModuleName(String, &'static str),
45 #[error("duplicate function name in module '{module}': {function}")]
47 #[diagnostic(help("function names must be unique within a module; rename the duplicate"))]
48 DuplicateFunctionName {
49 module: String,
51 function: String,
53 },
54 #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
56 #[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))]
57 DuplicateParamName {
58 module: String,
60 function: String,
62 param: String,
64 },
65 #[error("reserved keyword used: {0}")]
67 #[diagnostic(help("choose a different name that is not a language reserved word"))]
68 ReservedKeyword(String),
69 #[error("invalid identifier '{0}': {1}")]
71 #[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))]
72 InvalidIdentifier(String, &'static str),
73 #[error("error domain missing name in module '{0}'")]
75 #[diagnostic(help("add a non-empty 'name' field to the error domain"))]
76 ErrorDomainMissingName(String),
77 #[error("duplicate error code name in module '{module}': {name}")]
79 #[diagnostic(help("error code names must be unique within a module; rename the duplicate"))]
80 DuplicateErrorName {
81 module: String,
83 name: String,
85 },
86 #[error("duplicate error numeric code in module '{module}': {code}")]
88 #[diagnostic(help(
89 "numeric error codes must be unique within a module; assign a different value"
90 ))]
91 DuplicateErrorCode {
92 module: String,
94 code: i32,
96 },
97 #[error("invalid error code in module '{module}' for '{name}': must be non-zero")]
99 #[diagnostic(help("error codes must be non-zero; use a positive or negative integer"))]
100 InvalidErrorCode {
101 module: String,
103 name: String,
105 },
106 #[error("function name collides with error domain name in module '{module}': {name}")]
108 #[diagnostic(help(
109 "function and error domain names share a namespace; rename one to avoid the collision"
110 ))]
111 NameCollisionWithErrorDomain {
112 module: String,
114 name: String,
116 },
117 #[error("duplicate struct name in module '{module}': {name}")]
119 #[diagnostic(help("struct names must be unique within a module; rename the duplicate"))]
120 DuplicateStructName {
121 module: String,
123 name: String,
125 },
126 #[error("duplicate field name in struct '{struct_name}': {field}")]
128 #[diagnostic(help("field names must be unique within a struct; rename the duplicate"))]
129 DuplicateStructField {
130 struct_name: String,
132 field: String,
134 },
135 #[error("empty struct in module '{module}': {name}")]
137 #[diagnostic(help("structs must have at least one field; add a field or remove the struct"))]
138 EmptyStruct {
139 module: String,
141 name: String,
143 },
144 #[error("duplicate enum name in module '{module}': {name}")]
146 #[diagnostic(help("enum names must be unique within a module; rename the duplicate"))]
147 DuplicateEnumName {
148 module: String,
150 name: String,
152 },
153 #[error("empty enum in module '{module}': {name}")]
155 #[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))]
156 EmptyEnum {
157 module: String,
159 name: String,
161 },
162 #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
164 #[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))]
165 DuplicateEnumVariant {
166 enum_name: String,
168 variant: String,
170 },
171 #[error("duplicate field '{field}' in variant '{variant}' of enum '{enum_name}'")]
173 #[diagnostic(help(
174 "associated field names must be unique within an enum variant; rename the duplicate"
175 ))]
176 DuplicateEnumVariantField {
177 enum_name: String,
179 variant: String,
181 field: String,
183 },
184 #[error("duplicate enum value in enum '{enum_name}': {value}")]
186 #[diagnostic(help(
187 "variant numeric values must be unique within an enum; assign a different value"
188 ))]
189 DuplicateEnumValue {
190 enum_name: String,
192 value: i32,
194 },
195 #[error("unknown type reference: {name}")]
197 #[diagnostic(help(
198 "define a struct or enum with this name in the same module, or check for typos"
199 ))]
200 UnknownTypeRef {
201 name: String,
203 },
204 #[error("invalid map key type: {key_type}; only primitive types and strings are allowed as map keys")]
206 #[diagnostic(help("map keys must be primitive types (i32, u32, i64, f64, bool, string); structs, lists, and maps cannot be keys"))]
207 InvalidMapKey {
208 key_type: String,
210 },
211 #[error(
213 "borrowed type '{ty}' is not valid in {location}; only function parameters are allowed"
214 )]
215 #[diagnostic(help("borrowed types (&str, &[u8]) can only be used as function parameters, not return types or struct fields"))]
216 BorrowedTypeInInvalidPosition {
217 ty: String,
219 location: String,
221 },
222 #[error("duplicate callback name in module '{module}': {name}")]
224 #[diagnostic(help("callback names must be unique within a module; rename the duplicate"))]
225 DuplicateCallbackName {
226 module: String,
228 name: String,
230 },
231 #[error(
233 "listener '{listener}' in module '{module}' references undefined callback '{callback}'"
234 )]
235 #[diagnostic(help(
236 "listener event_callback must reference a callback defined in the same module"
237 ))]
238 ListenerCallbackNotFound {
239 module: String,
241 listener: String,
243 callback: String,
245 },
246 #[error("duplicate listener name in module '{module}': {name}")]
248 #[diagnostic(help("listener names must be unique within a module; rename the duplicate"))]
249 DuplicateListenerName {
250 module: String,
252 name: String,
254 },
255 #[error(
257 "callback '{callback}' in module '{module}' has parameter '{param}' with unsupported \
258 type '{ty}'"
259 )]
260 #[diagnostic(help(
261 "callback parameters are limited to scalars, bool, enums, string, bytes, handles, \
262 structs, optionals of those, lists of scalars/strings, and maps of scalars/strings; \
263 every target must be able to marshal a callback argument without an FFI round-trip"
264 ))]
265 UnsupportedCallbackParamType {
266 module: String,
268 callback: String,
270 param: String,
272 ty: String,
274 },
275 #[error("iterator type is only valid as a function return type, found in {location}")]
277 #[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))]
278 IteratorInInvalidPosition {
279 location: String,
281 },
282 #[error("unsupported element type '{ty}' in {location}")]
284 #[diagnostic(help(
285 "the C ABI lowers lists, maps, and iterators to flat parallel arrays, so element \
286 types must be flat: list/iterator elements may be scalars, bool, enums, strings, \
287 handles, or structs (plus optional structs/handles in lists); map keys and values \
288 may be scalars, bool, enums, or strings"
289 ))]
290 UnsupportedElementType {
291 location: String,
293 ty: String,
295 },
296 #[error("async function '{module}::{function}' cannot return an iterator")]
298 #[diagnostic(help(
299 "the callback-completed async ABI has no streaming protocol; return a list ([T]) \
300 from the async function, or make the function synchronous and return iter<T>"
301 ))]
302 AsyncIteratorReturn {
303 module: String,
305 function: String,
307 },
308 #[error("builder struct '{name}' in module '{module}' must have at least one field")]
310 #[diagnostic(help(
311 "builder structs must have at least one field; add a field or set builder: false"
312 ))]
313 BuilderStructEmpty {
314 module: String,
316 name: String,
318 },
319 #[error("unsupported schema version '{version}'; supported versions: {supported}")]
321 #[diagnostic(help(
322 "set the version field to the current schema version and update the \
323 document to match the current schema (see docs/src/idl.md)"
324 ))]
325 UnsupportedSchemaVersion {
326 version: String,
328 supported: String,
330 },
331}
332
333#[allow(clippy::result_large_err)]
349pub fn validate_api(
350 api: &mut Api,
351 source: Option<(&str, &str)>,
352) -> Result<(), ValidationDiagnostic> {
353 validate_api_inner(api).map_err(|e| ValidationDiagnostic::new(e, source))
354}
355
356fn validate_api_inner(api: &mut Api) -> Result<(), ValidationError> {
357 if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
358 return Err(ValidationError::UnsupportedSchemaVersion {
359 version: api.version.clone(),
360 supported: SUPPORTED_VERSIONS.join(", "),
361 });
362 }
363 let mut module_names = BTreeSet::new();
364 for m in &api.modules {
365 if !module_names.insert(m.name.clone()) {
366 return Err(ValidationError::DuplicateModuleName(m.name.clone()));
367 }
368 rules::validate_module(m, &api.modules)?;
369 }
370 resolve_type_refs(api);
371 Ok(())
372}