use miette::{Diagnostic, NamedSource, SourceSpan};
use std::collections::{BTreeMap, BTreeSet};
use weaveffi_ir::ir::{Api, ErrorDomain, Function, Module, Param, TypeRef, SUPPORTED_VERSIONS};
#[derive(Debug, Clone)]
pub enum ValidationWarning {
LargeEnumVariantCount {
enum_name: String,
count: usize,
},
DeepNesting {
location: String,
depth: usize,
},
EmptyModuleDoc {
module: String,
},
AsyncVoidFunction {
module: String,
function: String,
},
MutableOnValueType {
module: String,
function: String,
param: String,
},
DeprecatedFunction {
module: String,
function: String,
message: String,
},
}
impl std::fmt::Display for ValidationWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LargeEnumVariantCount { enum_name, count } => {
write!(f, "enum '{enum_name}' has {count} variants (>100)")
}
Self::DeepNesting { location, depth } => {
write!(
f,
"deep type nesting at {location} (depth {depth}, max recommended 3)"
)
}
Self::EmptyModuleDoc { module } => {
write!(f, "module '{module}' has no doc comments on any function")
}
Self::AsyncVoidFunction { module, function } => {
write!(
f,
"async function {module}::{function} has no return type; async void is unusual"
)
}
Self::MutableOnValueType {
module,
function,
param,
} => {
write!(
f,
"'mutable' on value-type parameter {module}::{function}::{param} has no effect; only meaningful for pointer/reference types (struct, string, bytes)"
)
}
Self::DeprecatedFunction {
module,
function,
message,
} => {
write!(f, "function {module}::{function} is deprecated: {message}")
}
}
}
}
pub fn collect_warnings(api: &Api) -> Vec<ValidationWarning> {
let mut warnings = Vec::new();
for module in &api.modules {
for e in &module.enums {
if e.variants.len() > 100 {
warnings.push(ValidationWarning::LargeEnumVariantCount {
enum_name: e.name.clone(),
count: e.variants.len(),
});
}
}
for f in &module.functions {
for p in &f.params {
let depth = nesting_depth(&p.ty);
if depth > 3 {
warnings.push(ValidationWarning::DeepNesting {
location: format!("{}::{}::{}", module.name, f.name, p.name),
depth,
});
}
}
if let Some(ret) = &f.returns {
let depth = nesting_depth(ret);
if depth > 3 {
warnings.push(ValidationWarning::DeepNesting {
location: format!("{}::{}::return", module.name, f.name),
depth,
});
}
}
}
for s in &module.structs {
for field in &s.fields {
let depth = nesting_depth(&field.ty);
if depth > 3 {
warnings.push(ValidationWarning::DeepNesting {
location: format!("{}::{}::{}", module.name, s.name, field.name),
depth,
});
}
}
}
for f in &module.functions {
if f.r#async && f.returns.is_none() {
warnings.push(ValidationWarning::AsyncVoidFunction {
module: module.name.clone(),
function: f.name.clone(),
});
}
for p in &f.params {
if p.mutable && is_value_type(&p.ty) {
warnings.push(ValidationWarning::MutableOnValueType {
module: module.name.clone(),
function: f.name.clone(),
param: p.name.clone(),
});
}
}
}
for f in &module.functions {
if let Some(msg) = &f.deprecated {
warnings.push(ValidationWarning::DeprecatedFunction {
module: module.name.clone(),
function: f.name.clone(),
message: msg.clone(),
});
}
}
if !module.functions.is_empty() && module.functions.iter().all(|f| f.doc.is_none()) {
warnings.push(ValidationWarning::EmptyModuleDoc {
module: module.name.clone(),
});
}
}
warnings
}
fn is_value_type(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::I32
| TypeRef::U32
| TypeRef::I64
| TypeRef::F64
| TypeRef::Bool
| TypeRef::Enum(_)
| TypeRef::Handle
)
}
fn nesting_depth(ty: &TypeRef) -> usize {
match ty {
TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
1 + nesting_depth(inner)
}
TypeRef::Map(k, v) => nesting_depth(k).max(nesting_depth(v)),
_ => 0,
}
}
#[derive(Debug, thiserror::Error, Diagnostic)]
pub enum ValidationError {
#[error("module has no name")]
#[diagnostic(help("every module must have a non-empty 'name' field"))]
NoModuleName,
#[error("duplicate module name: {0}")]
#[diagnostic(help(
"module names must be unique within an API definition; rename or merge the duplicate"
))]
DuplicateModuleName(String),
#[error("invalid module name '{0}': {1}")]
#[diagnostic(help(
"choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
))]
InvalidModuleName(String, &'static str),
#[error("duplicate function name in module '{module}': {function}")]
#[diagnostic(help("function names must be unique within a module; rename the duplicate"))]
DuplicateFunctionName { module: String, function: String },
#[error("duplicate param name in function '{function}' of module '{module}': {param}")]
#[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))]
DuplicateParamName {
module: String,
function: String,
param: String,
},
#[error("reserved keyword used: {0}")]
#[diagnostic(help("choose a different name that is not a language reserved word"))]
ReservedKeyword(String),
#[error("invalid identifier '{0}': {1}")]
#[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))]
InvalidIdentifier(String, &'static str),
#[error("error domain missing name in module '{0}'")]
#[diagnostic(help("add a non-empty 'name' field to the error domain"))]
ErrorDomainMissingName(String),
#[error("duplicate error code name in module '{module}': {name}")]
#[diagnostic(help("error code names must be unique within a module; rename the duplicate"))]
DuplicateErrorName { module: String, name: String },
#[error("duplicate error numeric code in module '{module}': {code}")]
#[diagnostic(help(
"numeric error codes must be unique within a module; assign a different value"
))]
DuplicateErrorCode { module: String, code: i32 },
#[error("invalid error code in module '{module}' for '{name}': must be non-zero")]
#[diagnostic(help("error codes must be non-zero; use a positive or negative integer"))]
InvalidErrorCode { module: String, name: String },
#[error("function name collides with error domain name in module '{module}': {name}")]
#[diagnostic(help(
"function and error domain names share a namespace; rename one to avoid the collision"
))]
NameCollisionWithErrorDomain { module: String, name: String },
#[error("duplicate struct name in module '{module}': {name}")]
#[diagnostic(help("struct names must be unique within a module; rename the duplicate"))]
DuplicateStructName { module: String, name: String },
#[error("duplicate field name in struct '{struct_name}': {field}")]
#[diagnostic(help("field names must be unique within a struct; rename the duplicate"))]
DuplicateStructField { struct_name: String, field: String },
#[error("empty struct in module '{module}': {name}")]
#[diagnostic(help("structs must have at least one field; add a field or remove the struct"))]
EmptyStruct { module: String, name: String },
#[error("duplicate enum name in module '{module}': {name}")]
#[diagnostic(help("enum names must be unique within a module; rename the duplicate"))]
DuplicateEnumName { module: String, name: String },
#[error("empty enum in module '{module}': {name}")]
#[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))]
EmptyEnum { module: String, name: String },
#[error("duplicate enum variant in enum '{enum_name}': {variant}")]
#[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))]
DuplicateEnumVariant { enum_name: String, variant: String },
#[error("duplicate enum value in enum '{enum_name}': {value}")]
#[diagnostic(help(
"variant numeric values must be unique within an enum; assign a different value"
))]
DuplicateEnumValue { enum_name: String, value: i32 },
#[error("unknown type reference: {name}")]
#[diagnostic(help(
"define a struct or enum with this name in the same module, or check for typos"
))]
UnknownTypeRef { name: String },
#[error("invalid map key type: {key_type}; only primitive types and strings are allowed as map keys")]
#[diagnostic(help("map keys must be primitive types (i32, u32, i64, f64, bool, string); structs, lists, and maps cannot be keys"))]
InvalidMapKey { key_type: String },
#[error(
"borrowed type '{ty}' is not valid in {location}; only function parameters are allowed"
)]
#[diagnostic(help("borrowed types (&str, &[u8]) can only be used as function parameters, not return types or struct fields"))]
BorrowedTypeInInvalidPosition { ty: String, location: String },
#[error("duplicate callback name in module '{module}': {name}")]
#[diagnostic(help("callback names must be unique within a module; rename the duplicate"))]
DuplicateCallbackName { module: String, name: String },
#[error(
"listener '{listener}' in module '{module}' references undefined callback '{callback}'"
)]
#[diagnostic(help(
"listener event_callback must reference a callback defined in the same module"
))]
ListenerCallbackNotFound {
module: String,
listener: String,
callback: String,
},
#[error("duplicate listener name in module '{module}': {name}")]
#[diagnostic(help("listener names must be unique within a module; rename the duplicate"))]
DuplicateListenerName { module: String, name: String },
#[error("iterator type is only valid as a function return type, found in {location}")]
#[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))]
IteratorInInvalidPosition { location: String },
#[error("builder struct '{name}' in module '{module}' must have at least one field")]
#[diagnostic(help(
"builder structs must have at least one field; add a field or set builder: false"
))]
BuilderStructEmpty { module: String, name: String },
#[error("unsupported schema version '{version}'; supported versions: {supported}")]
#[diagnostic(help("run 'weaveffi upgrade <file>' to migrate to the current schema version"))]
UnsupportedSchemaVersion { version: String, supported: String },
}
#[derive(Debug)]
pub struct ValidationDiagnostic {
pub error: ValidationError,
pub src: Option<NamedSource<String>>,
pub span: Option<SourceSpan>,
}
impl std::fmt::Display for ValidationDiagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.error, f)
}
}
impl std::error::Error for ValidationDiagnostic {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.error.source()
}
}
impl Diagnostic for ValidationDiagnostic {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.error.code()
}
fn severity(&self) -> Option<miette::Severity> {
self.error.severity()
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.error.help()
}
fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.error.url()
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.src
.as_ref()
.map(|s| s as &dyn miette::SourceCode)
.or_else(|| self.error.source_code())
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
if let Some(span) = self.span {
Some(Box::new(std::iter::once(
miette::LabeledSpan::new_with_span(Some("here".to_string()), span),
)))
} else {
self.error.labels()
}
}
}
impl ValidationDiagnostic {
pub fn new(error: ValidationError, source: Option<(&str, &str)>) -> Self {
let (src, span) = match source {
Some((filename, contents)) => {
let span = find_offending_span(&error, contents);
(Some(NamedSource::new(filename, contents.to_string())), span)
}
None => (None, None),
};
Self { error, src, span }
}
}
fn find_offending_span(err: &ValidationError, src: &str) -> Option<SourceSpan> {
let needle: &str = match err {
ValidationError::DuplicateModuleName(n) => Some(n.as_str()),
ValidationError::InvalidModuleName(n, _) => Some(n.as_str()),
ValidationError::DuplicateFunctionName { function, .. } => Some(function.as_str()),
ValidationError::DuplicateParamName { param, .. } => Some(param.as_str()),
ValidationError::ReservedKeyword(n) => Some(n.as_str()),
ValidationError::InvalidIdentifier(n, _) => Some(n.as_str()),
ValidationError::DuplicateErrorName { name, .. } => Some(name.as_str()),
ValidationError::InvalidErrorCode { name, .. } => Some(name.as_str()),
ValidationError::NameCollisionWithErrorDomain { name, .. } => Some(name.as_str()),
ValidationError::DuplicateStructName { name, .. } => Some(name.as_str()),
ValidationError::DuplicateStructField { field, .. } => Some(field.as_str()),
ValidationError::EmptyStruct { name, .. } => Some(name.as_str()),
ValidationError::DuplicateEnumName { name, .. } => Some(name.as_str()),
ValidationError::EmptyEnum { name, .. } => Some(name.as_str()),
ValidationError::DuplicateEnumVariant { variant, .. } => Some(variant.as_str()),
ValidationError::UnknownTypeRef { name } => Some(name.as_str()),
ValidationError::DuplicateCallbackName { name, .. } => Some(name.as_str()),
ValidationError::ListenerCallbackNotFound { callback, .. } => Some(callback.as_str()),
ValidationError::DuplicateListenerName { name, .. } => Some(name.as_str()),
ValidationError::BuilderStructEmpty { name, .. } => Some(name.as_str()),
ValidationError::UnsupportedSchemaVersion { version, .. } => Some(version.as_str()),
_ => None,
}?;
let quoted = format!("\"{needle}\"");
if let Some(pos) = src.find("ed) {
return Some(SourceSpan::new(pos.into(), quoted.len()));
}
src.find(needle)
.map(|pos| SourceSpan::new(pos.into(), needle.len()))
}
const RESERVED: &[&str] = &[
"if", "else", "for", "while", "loop", "match", "type", "return", "async", "await", "break",
"continue", "fn", "struct", "enum", "mod", "use",
];
fn is_valid_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
None => false,
Some(c) if !(c.is_ascii_alphabetic() || c == '_') => false,
_ => chars.all(|c| c.is_ascii_alphanumeric() || c == '_'),
}
}
fn check_identifier(name: &str) -> Result<(), ValidationError> {
if !is_valid_identifier(name) {
return Err(ValidationError::InvalidIdentifier(
name.to_string(),
"must start with a letter or underscore and contain only alphanumeric characters or underscores",
));
}
if RESERVED.contains(&name) {
return Err(ValidationError::ReservedKeyword(name.to_string()));
}
Ok(())
}
#[allow(clippy::result_large_err)]
pub fn validate_api(
api: &mut Api,
source: Option<(&str, &str)>,
) -> Result<(), ValidationDiagnostic> {
validate_api_inner(api).map_err(|e| ValidationDiagnostic::new(e, source))
}
fn validate_api_inner(api: &mut Api) -> Result<(), ValidationError> {
if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
return Err(ValidationError::UnsupportedSchemaVersion {
version: api.version.clone(),
supported: SUPPORTED_VERSIONS.join(", "),
});
}
let mut module_names = BTreeSet::new();
for m in &api.modules {
if !module_names.insert(m.name.clone()) {
return Err(ValidationError::DuplicateModuleName(m.name.clone()));
}
validate_module(m, &api.modules)?;
}
resolve_type_refs(api);
Ok(())
}
pub fn resolve_type_refs(api: &mut Api) {
let mut global_types: BTreeMap<String, (String, bool)> = BTreeMap::new();
for module in &api.modules {
for s in &module.structs {
global_types
.entry(s.name.clone())
.or_insert((module.name.clone(), false));
}
for e in &module.enums {
global_types
.entry(e.name.clone())
.or_insert((module.name.clone(), true));
}
}
for module in &mut api.modules {
let local_enum_names: BTreeSet<String> =
module.enums.iter().map(|e| e.name.clone()).collect();
let local_struct_names: BTreeSet<String> =
module.structs.iter().map(|s| s.name.clone()).collect();
let module_name = module.name.clone();
for f in &mut module.functions {
for p in &mut f.params {
resolve_single_type_ref(
&mut p.ty,
&local_enum_names,
&local_struct_names,
&module_name,
&global_types,
);
}
if let Some(ret) = &mut f.returns {
resolve_single_type_ref(
ret,
&local_enum_names,
&local_struct_names,
&module_name,
&global_types,
);
}
}
for s in &mut module.structs {
for field in &mut s.fields {
resolve_single_type_ref(
&mut field.ty,
&local_enum_names,
&local_struct_names,
&module_name,
&global_types,
);
}
}
}
}
fn resolve_single_type_ref(
ty: &mut TypeRef,
local_enum_names: &BTreeSet<String>,
local_struct_names: &BTreeSet<String>,
current_module: &str,
global_types: &BTreeMap<String, (String, bool)>,
) {
match ty {
TypeRef::Struct(name) if local_enum_names.contains(name.as_str()) => {
let name = std::mem::take(name);
*ty = TypeRef::Enum(name);
}
TypeRef::Struct(name) if !local_struct_names.contains(name.as_str()) => {
if let Some((mod_name, is_enum)) = global_types.get(name.as_str()) {
if mod_name != current_module {
let qualified = format!("{mod_name}.{name}");
if *is_enum {
*ty = TypeRef::Enum(qualified);
} else {
*name = qualified;
}
}
}
}
TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
resolve_single_type_ref(
inner,
local_enum_names,
local_struct_names,
current_module,
global_types,
);
}
TypeRef::Map(k, v) => {
resolve_single_type_ref(
k,
local_enum_names,
local_struct_names,
current_module,
global_types,
);
resolve_single_type_ref(
v,
local_enum_names,
local_struct_names,
current_module,
global_types,
);
}
_ => {}
}
}
pub fn find_type_in_api(api: &Api, name: &str) -> Option<(String, bool)> {
for module in &api.modules {
if module.structs.iter().any(|s| s.name == name) {
return Some((module.name.clone(), false));
}
if module.enums.iter().any(|e| e.name == name) {
return Some((module.name.clone(), true));
}
}
None
}
fn validate_module(module: &Module, all_modules: &[Module]) -> Result<(), ValidationError> {
if module.name.trim().is_empty() {
return Err(ValidationError::NoModuleName);
}
check_identifier(&module.name).map_err(|e| match e {
ValidationError::ReservedKeyword(_) => {
ValidationError::InvalidModuleName(module.name.clone(), "reserved word")
}
ValidationError::InvalidIdentifier(_, reason) => {
ValidationError::InvalidModuleName(module.name.clone(), reason)
}
other => other,
})?;
let mut function_names = BTreeSet::new();
for f in &module.functions {
if !function_names.insert(f.name.clone()) {
return Err(ValidationError::DuplicateFunctionName {
module: module.name.clone(),
function: f.name.clone(),
});
}
validate_function(module, f)?;
}
let mut struct_names = BTreeSet::new();
for s in &module.structs {
check_identifier(&s.name)?;
if !struct_names.insert(s.name.clone()) {
return Err(ValidationError::DuplicateStructName {
module: module.name.clone(),
name: s.name.clone(),
});
}
if s.fields.is_empty() {
if s.builder {
return Err(ValidationError::BuilderStructEmpty {
module: module.name.clone(),
name: s.name.clone(),
});
}
return Err(ValidationError::EmptyStruct {
module: module.name.clone(),
name: s.name.clone(),
});
}
let mut field_names = BTreeSet::new();
for f in &s.fields {
check_identifier(&f.name)?;
if !field_names.insert(f.name.clone()) {
return Err(ValidationError::DuplicateStructField {
struct_name: s.name.clone(),
field: f.name.clone(),
});
}
}
}
let mut enum_names = BTreeSet::new();
for e in &module.enums {
check_identifier(&e.name)?;
if !enum_names.insert(e.name.clone()) {
return Err(ValidationError::DuplicateEnumName {
module: module.name.clone(),
name: e.name.clone(),
});
}
if e.variants.is_empty() {
return Err(ValidationError::EmptyEnum {
module: module.name.clone(),
name: e.name.clone(),
});
}
let mut variant_names = BTreeSet::new();
let mut variant_values = BTreeMap::new();
for v in &e.variants {
check_identifier(&v.name)?;
if !variant_names.insert(v.name.clone()) {
return Err(ValidationError::DuplicateEnumVariant {
enum_name: e.name.clone(),
variant: v.name.clone(),
});
}
if variant_values.insert(v.value, v.name.clone()).is_some() {
return Err(ValidationError::DuplicateEnumValue {
enum_name: e.name.clone(),
value: v.value,
});
}
}
}
let known_types: BTreeSet<&str> = struct_names
.iter()
.map(|s| s.as_str())
.chain(enum_names.iter().map(|s| s.as_str()))
.collect();
for s in &module.structs {
for f in &s.fields {
if let Some(ty) = contains_borrowed(&f.ty) {
return Err(ValidationError::BorrowedTypeInInvalidPosition {
ty: ty.to_string(),
location: format!("field '{}' of struct '{}'", f.name, s.name),
});
}
if contains_iterator(&f.ty) {
return Err(ValidationError::IteratorInInvalidPosition {
location: format!("field '{}' of struct '{}'", f.name, s.name),
});
}
validate_type_ref(&f.ty, &known_types, all_modules, &module.name)?;
}
}
for f in &module.functions {
for p in &f.params {
if contains_iterator(&p.ty) {
return Err(ValidationError::IteratorInInvalidPosition {
location: format!(
"param '{}' of function '{}::{}'",
p.name, module.name, f.name
),
});
}
validate_type_ref(&p.ty, &known_types, all_modules, &module.name)?;
}
if let Some(ret) = &f.returns {
if let Some(ty) = contains_borrowed(ret) {
return Err(ValidationError::BorrowedTypeInInvalidPosition {
ty: ty.to_string(),
location: format!("return type of {}::{}", module.name, f.name),
});
}
validate_type_ref(ret, &known_types, all_modules, &module.name)?;
}
}
let mut callback_names = BTreeSet::new();
for cb in &module.callbacks {
check_identifier(&cb.name)?;
if !callback_names.insert(cb.name.clone()) {
return Err(ValidationError::DuplicateCallbackName {
module: module.name.clone(),
name: cb.name.clone(),
});
}
for p in &cb.params {
validate_param(p)?;
}
}
let mut listener_names = BTreeSet::new();
for l in &module.listeners {
check_identifier(&l.name)?;
if !listener_names.insert(l.name.clone()) {
return Err(ValidationError::DuplicateListenerName {
module: module.name.clone(),
name: l.name.clone(),
});
}
if !callback_names.contains(&l.event_callback) {
return Err(ValidationError::ListenerCallbackNotFound {
module: module.name.clone(),
listener: l.name.clone(),
callback: l.event_callback.clone(),
});
}
}
if let Some(errors) = &module.errors {
validate_error_domain(module, errors, &function_names)?;
}
let mut sub_module_names = BTreeSet::new();
for sub in &module.modules {
if !sub_module_names.insert(sub.name.clone()) {
return Err(ValidationError::DuplicateModuleName(sub.name.clone()));
}
validate_module(sub, all_modules)?;
}
Ok(())
}
fn validate_function(module: &Module, f: &Function) -> Result<(), ValidationError> {
check_identifier(&f.name)?;
let mut param_names = BTreeSet::new();
for p in &f.params {
validate_param(p)?;
if !param_names.insert(p.name.clone()) {
return Err(ValidationError::DuplicateParamName {
module: module.name.clone(),
function: f.name.clone(),
param: p.name.clone(),
});
}
}
Ok(())
}
fn validate_param(p: &Param) -> Result<(), ValidationError> {
check_identifier(&p.name)?;
Ok(())
}
fn contains_borrowed(ty: &TypeRef) -> Option<&'static str> {
match ty {
TypeRef::BorrowedStr => Some("&str"),
TypeRef::BorrowedBytes => Some("&[u8]"),
TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
contains_borrowed(inner)
}
TypeRef::Map(k, v) => contains_borrowed(k).or_else(|| contains_borrowed(v)),
_ => None,
}
}
fn contains_iterator(ty: &TypeRef) -> bool {
match ty {
TypeRef::Iterator(_) => true,
TypeRef::Optional(inner) | TypeRef::List(inner) => contains_iterator(inner),
TypeRef::Map(k, v) => contains_iterator(k) || contains_iterator(v),
_ => false,
}
}
fn validate_type_ref(
ty: &TypeRef,
known: &BTreeSet<&str>,
all_modules: &[Module],
current_module: &str,
) -> Result<(), ValidationError> {
match ty {
TypeRef::Struct(name) | TypeRef::Enum(name) | TypeRef::TypedHandle(name) => {
if !known.contains(name.as_str()) {
let found_elsewhere = all_modules.iter().any(|m| {
m.name != current_module
&& (m.structs.iter().any(|s| s.name == *name)
|| m.enums.iter().any(|e| e.name == *name))
});
if !found_elsewhere {
return Err(ValidationError::UnknownTypeRef { name: name.clone() });
}
}
Ok(())
}
TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
validate_type_ref(inner, known, all_modules, current_module)
}
TypeRef::Map(k, v) => {
let bad_key = match k.as_ref() {
TypeRef::Struct(name) => Some(format!("struct {name}")),
TypeRef::List(_) => Some("list".to_string()),
TypeRef::Map(_, _) => Some("map".to_string()),
_ => None,
};
if let Some(key_type) = bad_key {
return Err(ValidationError::InvalidMapKey { key_type });
}
validate_type_ref(k, known, all_modules, current_module)?;
validate_type_ref(v, known, all_modules, current_module)
}
_ => Ok(()),
}
}
fn validate_error_domain(
module: &Module,
errors: &ErrorDomain,
function_names: &BTreeSet<String>,
) -> Result<(), ValidationError> {
if errors.name.trim().is_empty() {
return Err(ValidationError::ErrorDomainMissingName(module.name.clone()));
}
if function_names.contains(&errors.name) {
return Err(ValidationError::NameCollisionWithErrorDomain {
module: module.name.clone(),
name: errors.name.clone(),
});
}
let mut by_name: BTreeSet<String> = BTreeSet::new();
let mut by_code: BTreeMap<i32, String> = BTreeMap::new();
for c in &errors.codes {
if c.code == 0 {
return Err(ValidationError::InvalidErrorCode {
module: module.name.clone(),
name: c.name.clone(),
});
}
if !by_name.insert(c.name.clone()) {
return Err(ValidationError::DuplicateErrorName {
module: module.name.clone(),
name: c.name.clone(),
});
}
if by_code.insert(c.code, c.name.clone()).is_some() {
return Err(ValidationError::DuplicateErrorCode {
module: module.name.clone(),
code: c.code,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use weaveffi_ir::ir::{
Api, CallbackDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, ListenerDef,
Module, Param, StructDef, StructField, TypeRef,
};
fn simple_function(name: &str) -> Function {
Function {
name: name.to_string(),
params: vec![Param {
name: "x".to_string(),
ty: TypeRef::I32,
mutable: false,
doc: None,
}],
returns: Some(TypeRef::I32),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}
}
fn simple_module(name: &str) -> Module {
Module {
name: name.to_string(),
functions: vec![simple_function("do_stuff")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}
}
fn simple_api() -> Api {
Api {
version: "0.1.0".to_string(),
modules: vec![simple_module("mymod")],
generators: None,
}
}
#[test]
fn valid_api_passes() {
let mut api = simple_api();
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn duplicate_module_names_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![simple_module("dup"), simple_module("dup")],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateModuleName(n) if n == "dup"
));
}
#[test]
fn duplicate_function_names_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("same"), simple_function("same")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateFunctionName { .. }
));
}
#[test]
fn reserved_keywords_rejected() {
for kw in ["type", "async"] {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: kw.to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(
validate_api(&mut api, None).is_err(),
"Expected reserved keyword '{kw}' to be rejected"
);
}
}
#[test]
fn invalid_identifiers_rejected() {
for bad in ["123", "has spaces", ""] {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: bad.to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(
validate_api(&mut api, None).is_err(),
"Expected invalid identifier '{bad}' to be rejected"
);
}
}
#[test]
fn async_function_passes_validation() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "do_async".to_string(),
params: vec![],
returns: None,
doc: None,
r#async: true,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn async_function_with_return_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "fetch_data".to_string(),
params: vec![Param {
name: "url".to_string(),
ty: TypeRef::StringUtf8,
mutable: false,
doc: None,
}],
returns: Some(TypeRef::StringUtf8),
doc: None,
r#async: true,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn async_void_function_emits_warning() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "fire_and_forget".to_string(),
params: vec![],
returns: None,
doc: Some("documented".to_string()),
r#async: true,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.iter().any(|w| matches!(
w,
ValidationWarning::AsyncVoidFunction { module, function }
if module == "mymod" && function == "fire_and_forget"
)));
}
#[test]
fn async_function_with_return_no_void_warning() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "fetch".to_string(),
params: vec![],
returns: Some(TypeRef::StringUtf8),
doc: Some("documented".to_string()),
r#async: true,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(!warnings
.iter()
.any(|w| matches!(w, ValidationWarning::AsyncVoidFunction { .. })));
}
#[test]
fn empty_module_name_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::NoModuleName
));
}
#[test]
fn doc_example_error_domain_validates() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "contacts".to_string(),
functions: vec![
Function {
name: "create_contact".to_string(),
params: vec![
Param {
name: "name".to_string(),
ty: TypeRef::StringUtf8,
mutable: false,
doc: None,
},
Param {
name: "email".to_string(),
ty: TypeRef::StringUtf8,
mutable: false,
doc: None,
},
],
returns: Some(TypeRef::Handle),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
},
Function {
name: "get_contact".to_string(),
params: vec![Param {
name: "id".to_string(),
ty: TypeRef::Handle,
mutable: false,
doc: None,
}],
returns: Some(TypeRef::StringUtf8),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
},
],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: Some(ErrorDomain {
name: "ContactErrors".to_string(),
codes: vec![
ErrorCode {
name: "not_found".to_string(),
code: 1,
message: "Contact not found".to_string(),
doc: None,
},
ErrorCode {
name: "duplicate".to_string(),
code: 2,
message: "Contact already exists".to_string(),
doc: None,
},
ErrorCode {
name: "invalid_email".to_string(),
code: 3,
message: "Email address is invalid".to_string(),
doc: None,
},
],
}),
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn error_code_zero_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: Some(ErrorDomain {
name: "MyErrors".to_string(),
codes: vec![ErrorCode {
name: "success".to_string(),
code: 0,
message: "should fail".to_string(),
doc: None,
}],
}),
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::InvalidErrorCode { module, name }
if module == "mymod" && name == "success"
));
}
#[test]
fn error_domain_name_collision_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("do_stuff")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: Some(ErrorDomain {
name: "do_stuff".to_string(),
codes: vec![ErrorCode {
name: "fail".to_string(),
code: 1,
message: "failed".to_string(),
doc: None,
}],
}),
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::NameCollisionWithErrorDomain { module, name }
if module == "mymod" && name == "do_stuff"
));
}
#[test]
fn duplicate_error_names_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: Some(ErrorDomain {
name: "MyErrors".to_string(),
codes: vec![
ErrorCode {
name: "fail".to_string(),
code: 1,
message: "failed".to_string(),
doc: None,
},
ErrorCode {
name: "fail".to_string(),
code: 2,
message: "also failed".to_string(),
doc: None,
},
],
}),
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateErrorName { module, name }
if module == "mymod" && name == "fail"
));
}
#[test]
fn duplicate_error_codes_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: Some(ErrorDomain {
name: "MyErrors".to_string(),
codes: vec![
ErrorCode {
name: "not_found".to_string(),
code: 1,
message: "not found".to_string(),
doc: None,
},
ErrorCode {
name: "timeout".to_string(),
code: 1,
message: "timed out".to_string(),
doc: None,
},
],
}),
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateErrorCode { .. }
));
}
fn simple_struct(name: &str) -> StructDef {
StructDef {
name: name.to_string(),
doc: None,
fields: vec![StructField {
name: "x".to_string(),
ty: TypeRef::I32,
doc: None,
default: None,
}],
builder: false,
}
}
#[test]
fn duplicate_struct_names_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![simple_struct("Point"), simple_struct("Point")],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateStructName { module, name }
if module == "mymod" && name == "Point"
));
}
#[test]
fn empty_struct_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![StructDef {
name: "Empty".to_string(),
doc: None,
fields: vec![],
builder: false,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::EmptyStruct { module, name }
if module == "mymod" && name == "Empty"
));
}
#[test]
fn duplicate_struct_field_names_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![StructDef {
name: "Point".to_string(),
doc: None,
fields: vec![
StructField {
name: "x".to_string(),
ty: TypeRef::I32,
doc: None,
default: None,
},
StructField {
name: "x".to_string(),
ty: TypeRef::F64,
doc: None,
default: None,
},
],
builder: false,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateStructField { struct_name, field }
if struct_name == "Point" && field == "x"
));
}
fn simple_enum(name: &str) -> EnumDef {
EnumDef {
name: name.to_string(),
doc: None,
variants: vec![
EnumVariant {
name: "A".to_string(),
value: 0,
doc: None,
},
EnumVariant {
name: "B".to_string(),
value: 1,
doc: None,
},
],
}
}
#[test]
fn duplicate_enum_names_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![simple_enum("Color"), simple_enum("Color")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateEnumName { module, name }
if module == "mymod" && name == "Color"
));
}
#[test]
fn empty_enum_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![EnumDef {
name: "Empty".to_string(),
doc: None,
variants: vec![],
}],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::EmptyEnum { module, name }
if module == "mymod" && name == "Empty"
));
}
#[test]
fn duplicate_enum_variant_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![EnumDef {
name: "Color".to_string(),
doc: None,
variants: vec![
EnumVariant {
name: "Red".to_string(),
value: 0,
doc: None,
},
EnumVariant {
name: "Red".to_string(),
value: 1,
doc: None,
},
],
}],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateEnumVariant { enum_name, variant }
if enum_name == "Color" && variant == "Red"
));
}
#[test]
fn duplicate_enum_value_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![EnumDef {
name: "Color".to_string(),
doc: None,
variants: vec![
EnumVariant {
name: "Red".to_string(),
value: 0,
doc: None,
},
EnumVariant {
name: "Green".to_string(),
value: 0,
doc: None,
},
],
}],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateEnumValue { enum_name, value }
if enum_name == "Color" && value == 0
));
}
#[test]
fn unknown_type_ref_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "do_stuff".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: TypeRef::Struct("Foo".to_string()),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::UnknownTypeRef { name } if name == "Foo"
));
}
#[test]
fn valid_struct_ref_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "do_stuff".to_string(),
params: vec![Param {
name: "p".to_string(),
ty: TypeRef::Struct("Point".to_string()),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![simple_struct("Point")],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn unknown_type_ref_in_optional_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "do_stuff".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::Struct("Bar".to_string()))),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::UnknownTypeRef { name } if name == "Bar"
));
}
#[test]
fn unknown_type_ref_in_list_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "do_stuff".to_string(),
params: vec![],
returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Baz".to_string())))),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::UnknownTypeRef { name } if name == "Baz"
));
}
#[test]
fn struct_field_referencing_unknown_type() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![StructDef {
name: "Wrapper".to_string(),
doc: None,
fields: vec![StructField {
name: "inner".to_string(),
ty: TypeRef::Struct("Nonexistent".to_string()),
doc: None,
default: None,
}],
builder: false,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
));
}
#[test]
fn function_param_with_optional_struct() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "save".to_string(),
params: vec![Param {
name: "c".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::Struct("Contact".to_string()))),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![StructDef {
name: "Contact".to_string(),
doc: None,
fields: vec![StructField {
name: "name".to_string(),
ty: TypeRef::StringUtf8,
doc: None,
default: None,
}],
builder: false,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn function_param_with_list_of_enums() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "paint".to_string(),
params: vec![Param {
name: "colors".to_string(),
ty: TypeRef::List(Box::new(TypeRef::Enum("Color".to_string()))),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![simple_enum("Color")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn nested_optional_list_validates() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "list_contacts".to_string(),
params: vec![],
returns: Some(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
TypeRef::Struct("Contact".to_string()),
))))),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![StructDef {
name: "Contact".to_string(),
doc: None,
fields: vec![StructField {
name: "name".to_string(),
ty: TypeRef::StringUtf8,
doc: None,
default: None,
}],
builder: false,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn enum_variant_value_zero_allowed() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![EnumDef {
name: "Status".to_string(),
doc: None,
variants: vec![
EnumVariant {
name: "Unknown".to_string(),
value: 0,
doc: None,
},
EnumVariant {
name: "Active".to_string(),
value: 1,
doc: None,
},
],
}],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn valid_enum_ref_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "get_color".to_string(),
params: vec![],
returns: Some(TypeRef::Enum("Color".to_string())),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![simple_enum("Color")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn resolve_enum_ref_in_function_param() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "paint".to_string(),
params: vec![Param {
name: "color".to_string(),
ty: TypeRef::Struct("Color".to_string()),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![simple_enum("Color")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
validate_api(&mut api, None).unwrap();
assert_eq!(
api.modules[0].functions[0].params[0].ty,
TypeRef::Enum("Color".to_string())
);
}
#[test]
fn resolve_enum_ref_in_optional() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "paint".to_string(),
params: vec![Param {
name: "color".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::Struct("Color".to_string()))),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![simple_enum("Color")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
validate_api(&mut api, None).unwrap();
assert_eq!(
api.modules[0].functions[0].params[0].ty,
TypeRef::Optional(Box::new(TypeRef::Enum("Color".to_string())))
);
}
#[test]
fn struct_ref_not_changed() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "save".to_string(),
params: vec![Param {
name: "c".to_string(),
ty: TypeRef::Struct("Contact".to_string()),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![simple_struct("Contact")],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
validate_api(&mut api, None).unwrap();
assert_eq!(
api.modules[0].functions[0].params[0].ty,
TypeRef::Struct("Contact".to_string())
);
}
#[test]
fn map_with_string_key_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "get_map".to_string(),
params: vec![],
returns: Some(TypeRef::Map(
Box::new(TypeRef::StringUtf8),
Box::new(TypeRef::I32),
)),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn map_with_struct_key_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "get_map".to_string(),
params: vec![],
returns: Some(TypeRef::Map(
Box::new(TypeRef::Struct("Point".to_string())),
Box::new(TypeRef::I32),
)),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![simple_struct("Point")],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::InvalidMapKey { key_type } if key_type == "struct Point"
));
}
#[test]
fn map_with_enum_key_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "get_map".to_string(),
params: vec![],
returns: Some(TypeRef::Map(
Box::new(TypeRef::Enum("Color".to_string())),
Box::new(TypeRef::StringUtf8),
)),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![simple_enum("Color")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn warning_large_enum_variant_count() {
let variants: Vec<EnumVariant> = (0..101)
.map(|i| EnumVariant {
name: format!("V{i}"),
value: i,
doc: None,
})
.collect();
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![EnumDef {
name: "BigEnum".to_string(),
doc: None,
variants,
}],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.iter().any(|w| matches!(
w,
ValidationWarning::LargeEnumVariantCount { enum_name, count }
if enum_name == "BigEnum" && *count == 101
)));
}
#[test]
fn warning_enum_at_100_no_warning() {
let variants: Vec<EnumVariant> = (0..100)
.map(|i| EnumVariant {
name: format!("V{i}"),
value: i,
doc: None,
})
.collect();
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![],
enums: vec![EnumDef {
name: "BigEnum".to_string(),
doc: None,
variants,
}],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(!warnings
.iter()
.any(|w| matches!(w, ValidationWarning::LargeEnumVariantCount { .. })));
}
#[test]
fn warning_deep_nesting_in_param() {
let deep = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
Box::new(TypeRef::List(Box::new(TypeRef::I32))),
)))));
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "nested_fn".to_string(),
params: vec![Param {
name: "data".to_string(),
ty: deep,
mutable: false,
doc: None,
}],
returns: None,
doc: Some("documented".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.iter().any(|w| matches!(
w,
ValidationWarning::DeepNesting { location, depth }
if location == "mymod::nested_fn::data" && *depth == 4
)));
}
#[test]
fn warning_nesting_at_3_no_warning() {
let nested = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
Box::new(TypeRef::I32),
)))));
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "ok_fn".to_string(),
params: vec![Param {
name: "data".to_string(),
ty: nested,
mutable: false,
doc: None,
}],
returns: None,
doc: Some("documented".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(!warnings
.iter()
.any(|w| matches!(w, ValidationWarning::DeepNesting { .. })));
}
#[test]
fn warning_deep_nesting_in_struct_field() {
let deep = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
Box::new(TypeRef::List(Box::new(TypeRef::I32))),
)))));
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![StructDef {
name: "Widget".to_string(),
doc: None,
fields: vec![StructField {
name: "data".to_string(),
ty: deep,
doc: None,
default: None,
}],
builder: false,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.iter().any(|w| matches!(
w,
ValidationWarning::DeepNesting { location, .. }
if location == "mymod::Widget::data"
)));
}
#[test]
fn warning_empty_module_doc() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "undocumented".to_string(),
functions: vec![
Function {
name: "a".to_string(),
params: vec![],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
},
Function {
name: "b".to_string(),
params: vec![],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
},
],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.iter().any(|w| matches!(
w,
ValidationWarning::EmptyModuleDoc { module } if module == "undocumented"
)));
}
#[test]
fn warning_partial_docs_no_warning() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "partial".to_string(),
functions: vec![
Function {
name: "a".to_string(),
params: vec![],
returns: None,
doc: Some("has doc".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
},
Function {
name: "b".to_string(),
params: vec![],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
},
],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(!warnings
.iter()
.any(|w| matches!(w, ValidationWarning::EmptyModuleDoc { .. })));
}
#[test]
fn warning_no_functions_no_empty_doc_warning() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "empty".to_string(),
functions: vec![],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(!warnings
.iter()
.any(|w| matches!(w, ValidationWarning::EmptyModuleDoc { .. })));
}
#[test]
fn warning_clean_api_no_warnings() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "clean".to_string(),
functions: vec![Function {
name: "add".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: TypeRef::I32,
mutable: false,
doc: None,
}],
returns: Some(TypeRef::I32),
doc: Some("Adds numbers".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![simple_enum("Color")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.is_empty());
}
#[test]
fn resolve_enum_ref_in_struct_field() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![simple_function("ok_fn")],
structs: vec![StructDef {
name: "Widget".to_string(),
doc: None,
fields: vec![StructField {
name: "color".to_string(),
ty: TypeRef::Struct("Color".to_string()),
doc: None,
default: None,
}],
builder: false,
}],
enums: vec![simple_enum("Color")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
validate_api(&mut api, None).unwrap();
assert_eq!(
api.modules[0].structs[0].fields[0].ty,
TypeRef::Enum("Color".to_string())
);
}
#[test]
fn typed_handle_valid_struct_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "get_session".to_string(),
params: vec![Param {
name: "h".to_string(),
ty: TypeRef::TypedHandle("Session".to_string()),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![simple_struct("Session")],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn typed_handle_unknown_struct_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "mymod".to_string(),
functions: vec![Function {
name: "get_session".to_string(),
params: vec![Param {
name: "h".to_string(),
ty: TypeRef::TypedHandle("Nonexistent".to_string()),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
));
}
#[test]
fn borrowed_str_param_accepted() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "io".to_string(),
functions: vec![Function {
name: "write".to_string(),
params: vec![Param {
name: "data".to_string(),
ty: TypeRef::BorrowedStr,
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn borrowed_bytes_param_accepted() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "io".to_string(),
functions: vec![Function {
name: "upload".to_string(),
params: vec![Param {
name: "raw".to_string(),
ty: TypeRef::BorrowedBytes,
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn borrowed_str_in_return_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "io".to_string(),
functions: vec![Function {
name: "read".to_string(),
params: vec![],
returns: Some(TypeRef::BorrowedStr),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::BorrowedTypeInInvalidPosition { ty, location }
if ty == "&str" && location.contains("return type")
));
}
#[test]
fn borrowed_bytes_in_return_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "io".to_string(),
functions: vec![Function {
name: "read_raw".to_string(),
params: vec![],
returns: Some(TypeRef::BorrowedBytes),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::BorrowedTypeInInvalidPosition { ty, location }
if ty == "&[u8]" && location.contains("return type")
));
}
#[test]
fn borrowed_str_in_struct_field_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "data".to_string(),
functions: vec![],
structs: vec![StructDef {
name: "Msg".to_string(),
fields: vec![StructField {
name: "text".to_string(),
ty: TypeRef::BorrowedStr,
doc: None,
default: None,
}],
builder: false,
doc: None,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::BorrowedTypeInInvalidPosition { ty, location }
if ty == "&str" && location.contains("struct")
));
}
#[test]
fn borrowed_bytes_in_struct_field_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "data".to_string(),
functions: vec![],
structs: vec![StructDef {
name: "Blob".to_string(),
fields: vec![StructField {
name: "content".to_string(),
ty: TypeRef::BorrowedBytes,
doc: None,
default: None,
}],
builder: false,
doc: None,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::BorrowedTypeInInvalidPosition { ty, location }
if ty == "&[u8]" && location.contains("struct")
));
}
#[test]
fn borrowed_str_nested_in_optional_return_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "io".to_string(),
functions: vec![Function {
name: "maybe_read".to_string(),
params: vec![],
returns: Some(TypeRef::Optional(Box::new(TypeRef::BorrowedStr))),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::BorrowedTypeInInvalidPosition { ty, .. }
if ty == "&str"
));
}
#[test]
fn cross_module_struct_ref_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![
Module {
name: "orders".to_string(),
functions: vec![Function {
name: "place_order".to_string(),
params: vec![Param {
name: "item".to_string(),
ty: TypeRef::Struct("Product".to_string()),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
},
Module {
name: "catalog".to_string(),
functions: vec![simple_function("list_products")],
structs: vec![simple_struct("Product")],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
},
],
generators: None,
};
validate_api(&mut api, None).unwrap();
assert_eq!(
api.modules[0].functions[0].params[0].ty,
TypeRef::Struct("catalog.Product".to_string())
);
}
#[test]
fn cross_module_enum_ref_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![
Module {
name: "orders".to_string(),
functions: vec![Function {
name: "get_status".to_string(),
params: vec![],
returns: Some(TypeRef::Struct("Status".to_string())),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
},
Module {
name: "shared".to_string(),
functions: vec![simple_function("noop")],
structs: vec![],
enums: vec![simple_enum("Status")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
},
],
generators: None,
};
validate_api(&mut api, None).unwrap();
assert_eq!(
api.modules[0].functions[0].returns,
Some(TypeRef::Enum("shared.Status".to_string()))
);
}
#[test]
fn cross_module_unknown_still_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![
Module {
name: "orders".to_string(),
functions: vec![Function {
name: "do_stuff".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: TypeRef::Struct("Nonexistent".to_string()),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
},
Module {
name: "catalog".to_string(),
functions: vec![simple_function("list_products")],
structs: vec![simple_struct("Product")],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
},
],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
));
}
#[test]
fn find_type_in_api_finds_struct() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "catalog".to_string(),
functions: vec![],
structs: vec![simple_struct("Product")],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let result = find_type_in_api(&api, "Product");
assert_eq!(result, Some(("catalog".to_string(), false)));
}
#[test]
fn find_type_in_api_finds_enum() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "shared".to_string(),
functions: vec![],
structs: vec![],
enums: vec![simple_enum("Status")],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let result = find_type_in_api(&api, "Status");
assert_eq!(result, Some(("shared".to_string(), true)));
}
#[test]
fn find_type_in_api_returns_none_for_unknown() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![simple_module("mymod")],
generators: None,
};
assert_eq!(find_type_in_api(&api, "Nonexistent"), None);
}
#[test]
fn validate_nested_module_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "parent".to_string(),
functions: vec![simple_function("top_fn")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![Module {
name: "child".to_string(),
functions: vec![simple_function("inner_fn")],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn duplicate_callback_names_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "events".to_string(),
functions: vec![],
structs: vec![],
enums: vec![],
callbacks: vec![
CallbackDef {
name: "on_data".to_string(),
params: vec![],
doc: None,
},
CallbackDef {
name: "on_data".to_string(),
params: vec![],
doc: None,
},
],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateCallbackName { module, name }
if module == "events" && name == "on_data"
));
}
#[test]
fn listener_referencing_undefined_callback_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "events".to_string(),
functions: vec![],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![ListenerDef {
name: "watcher".to_string(),
event_callback: "nonexistent".to_string(),
doc: None,
}],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::ListenerCallbackNotFound { module, listener, callback }
if module == "events" && listener == "watcher" && callback == "nonexistent"
));
}
#[test]
fn listener_referencing_defined_callback_passes() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "events".to_string(),
functions: vec![],
structs: vec![],
enums: vec![],
callbacks: vec![CallbackDef {
name: "on_data".to_string(),
params: vec![Param {
name: "payload".to_string(),
ty: TypeRef::StringUtf8,
mutable: false,
doc: None,
}],
doc: None,
}],
listeners: vec![ListenerDef {
name: "data_stream".to_string(),
event_callback: "on_data".to_string(),
doc: Some("Subscribe to data".to_string()),
}],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn duplicate_listener_names_rejected() {
let mut api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "events".to_string(),
functions: vec![],
structs: vec![],
enums: vec![],
callbacks: vec![CallbackDef {
name: "on_data".to_string(),
params: vec![],
doc: None,
}],
listeners: vec![
ListenerDef {
name: "watcher".to_string(),
event_callback: "on_data".to_string(),
doc: None,
},
ListenerDef {
name: "watcher".to_string(),
event_callback: "on_data".to_string(),
doc: None,
},
],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::DuplicateListenerName { module, name }
if module == "events" && name == "watcher"
));
}
#[test]
fn iterator_valid_as_return_type() {
let mut api = Api {
version: "0.2.0".to_string(),
modules: vec![Module {
name: "data".to_string(),
functions: vec![Function {
name: "list_items".to_string(),
params: vec![],
returns: Some(TypeRef::Iterator(Box::new(TypeRef::I32))),
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(validate_api(&mut api, None).is_ok());
}
#[test]
fn iterator_rejected_as_param() {
let mut api = Api {
version: "0.2.0".to_string(),
modules: vec![Module {
name: "data".to_string(),
functions: vec![Function {
name: "consume".to_string(),
params: vec![Param {
name: "items".to_string(),
ty: TypeRef::Iterator(Box::new(TypeRef::I32)),
mutable: false,
doc: None,
}],
returns: None,
doc: None,
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::IteratorInInvalidPosition { .. }
));
}
#[test]
fn iterator_rejected_in_struct_field() {
let mut api = Api {
version: "0.2.0".to_string(),
modules: vec![Module {
name: "data".to_string(),
functions: vec![],
structs: vec![StructDef {
name: "Container".to_string(),
doc: None,
fields: vec![StructField {
name: "items".to_string(),
ty: TypeRef::Iterator(Box::new(TypeRef::I32)),
doc: None,
default: None,
}],
builder: false,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
assert!(matches!(
validate_api(&mut api, None).unwrap_err().error,
ValidationError::IteratorInInvalidPosition { .. }
));
}
#[test]
fn builder_struct_empty_is_error() {
let mut api = Api {
version: "0.2.0".to_string(),
modules: vec![Module {
name: "m".into(),
functions: vec![],
structs: vec![StructDef {
name: "Empty".into(),
doc: None,
fields: vec![],
builder: true,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let err = validate_api(&mut api, None).unwrap_err();
assert!(
matches!(err.error, ValidationError::BuilderStructEmpty { .. }),
"expected BuilderStructEmpty, got: {err}"
);
}
#[test]
fn warning_mutable_on_value_type() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "math".to_string(),
functions: vec![Function {
name: "add".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: TypeRef::I32,
mutable: true,
doc: None,
}],
returns: Some(TypeRef::I32),
doc: Some("add".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.iter().any(|w| matches!(
w,
ValidationWarning::MutableOnValueType {
param,
..
} if param == "x"
)));
}
#[test]
fn no_warning_mutable_on_pointer_type() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "io".to_string(),
functions: vec![Function {
name: "fill".to_string(),
params: vec![
Param {
name: "buf".to_string(),
ty: TypeRef::Bytes,
mutable: true,
doc: None,
},
Param {
name: "msg".to_string(),
ty: TypeRef::StringUtf8,
mutable: true,
doc: None,
},
Param {
name: "obj".to_string(),
ty: TypeRef::Struct("Thing".into()),
mutable: true,
doc: None,
},
],
returns: None,
doc: Some("fill".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(
!warnings
.iter()
.any(|w| matches!(w, ValidationWarning::MutableOnValueType { .. })),
"pointer types should not trigger mutable warning"
);
}
#[test]
fn no_warning_mutable_false_on_value_type() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "math".to_string(),
functions: vec![Function {
name: "add".to_string(),
params: vec![Param {
name: "x".to_string(),
ty: TypeRef::I32,
mutable: false,
doc: None,
}],
returns: Some(TypeRef::I32),
doc: Some("add".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(
!warnings
.iter()
.any(|w| matches!(w, ValidationWarning::MutableOnValueType { .. })),
"mutable=false should not trigger warning"
);
}
#[test]
fn warning_mutable_on_enum_type() {
let api = Api {
version: "0.1.0".to_string(),
modules: vec![Module {
name: "paint".to_string(),
functions: vec![Function {
name: "set_color".to_string(),
params: vec![Param {
name: "color".to_string(),
ty: TypeRef::Enum("Color".into()),
mutable: true,
doc: None,
}],
returns: None,
doc: Some("set".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.iter().any(|w| matches!(
w,
ValidationWarning::MutableOnValueType { param, .. } if param == "color"
)));
}
#[test]
fn warning_deprecated_function() {
let api = Api {
version: "0.2.0".to_string(),
modules: vec![Module {
name: "math".to_string(),
functions: vec![Function {
name: "add_old".to_string(),
params: vec![],
returns: Some(TypeRef::I32),
doc: Some("old add".to_string()),
r#async: false,
cancellable: false,
deprecated: Some("Use add_v2 instead".to_string()),
since: Some("0.1.0".to_string()),
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(warnings.iter().any(|w| matches!(
w,
ValidationWarning::DeprecatedFunction { function, message, .. }
if function == "add_old" && message == "Use add_v2 instead"
)));
}
#[test]
fn no_warning_for_non_deprecated_function() {
let api = Api {
version: "0.2.0".to_string(),
modules: vec![Module {
name: "math".to_string(),
functions: vec![Function {
name: "add".to_string(),
params: vec![],
returns: Some(TypeRef::I32),
doc: Some("add things".to_string()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let warnings = collect_warnings(&api);
assert!(!warnings
.iter()
.any(|w| matches!(w, ValidationWarning::DeprecatedFunction { .. })));
}
}