use std::collections::BTreeMap;
use std::path::Path;
use crate::error::{Error, Result};
use crate::spec::{
ApiSpec, ApiSpecLeaf, CompilerPass, FunctionArgsSpec, FunctionResultSpec, SelectedFamily,
};
pub(crate) struct SelectedValidationPass;
impl CompilerPass<SelectedFamily, SelectedFamily> for SelectedValidationPass {
type Error = Error;
fn transform_leaf(
&mut self,
leaf: ApiSpecLeaf<SelectedFamily>,
) -> Result<ApiSpecLeaf<SelectedFamily>> {
validate_generic_model_semantics(&leaf.spec, &leaf.source_path)?;
Ok(leaf)
}
}
fn validate_generic_model_semantics(spec: &ApiSpec<SelectedFamily>, path: &Path) -> Result<()> {
let invalid = |reason: String| Error::InvalidWit {
path: path.to_path_buf(),
reason,
};
for (_, record) in spec.records() {
let parameters = spec.record_type_parameters(&record.full_name);
let mut generated_names = BTreeMap::<String, String>::new();
for usage in ¶meters {
if let Some(previous) = generated_names.insert(
usage.parameter.name.clone(),
usage.parameter.full_name.clone(),
) && previous != usage.parameter.full_name
{
return Err(invalid(format!(
"type parameters `{previous}` and `{}` both generate the name `{}`",
usage.parameter.full_name, usage.parameter.name
)));
}
}
for (field_name, function) in record.functions() {
let uses_parameter = match &function.result {
FunctionResultSpec::Authored(result) => !spec.type_parameters(result).is_empty(),
FunctionResultSpec::Annotation(_) => false,
} || match &function.args {
FunctionArgsSpec::Varargs { prefix, .. } | FunctionArgsSpec::Fixed(prefix) => {
prefix
.iter()
.any(|arg| !spec.type_parameters(&arg.field_type).is_empty())
}
} || function
.alternate_type
.as_ref()
.is_some_and(|alternate| !spec.type_parameters(alternate).is_empty());
if uses_parameter {
return Err(invalid(format!(
"record `{}` field `{field_name}` uses a type parameter in function-signature metadata, which is not supported",
record.full_name
)));
}
}
}
for (_, variant) in spec.variants() {
let parameters = spec.variant_type_parameters(&variant.full_name);
let mut generated_names = BTreeMap::<String, String>::new();
for usage in parameters {
if let Some(previous) = generated_names.insert(
usage.parameter.name.clone(),
usage.parameter.full_name.clone(),
) && previous != usage.parameter.full_name
{
return Err(invalid(format!(
"variant `{}` uses type parameters `{previous}` and `{}` that both generate the name `{}`",
variant.full_name, usage.parameter.full_name, usage.parameter.name
)));
}
}
}
for service in &spec.services {
for operation in &service.operations {
let mut generated_names = BTreeMap::<String, String>::new();
let operation_parameters = operation
.input_type()
.into_iter()
.chain(operation.output_type())
.flat_map(|value| spec.type_parameters(value));
for usage in operation_parameters {
if let Some(previous) = generated_names.insert(
usage.parameter.name.clone(),
usage.parameter.full_name.clone(),
) && previous != usage.parameter.full_name
{
return Err(invalid(format!(
"operation `{}` uses type parameters `{previous}` and `{}` that both generate the name `{}`",
operation.name, usage.parameter.full_name, usage.parameter.name
)));
}
}
}
for resource in &service.resources {
let resource_is_generic = resource
.fields
.iter()
.any(|field| !spec.type_parameters(&field.field_type).is_empty())
|| resource.methods.iter().any(|method| {
method
.params
.iter()
.any(|param| !spec.type_parameters(¶m.field_type).is_empty())
|| method.result.as_ref().is_some_and(|result| {
!spec.type_parameters(&result.result_type).is_empty()
})
});
if resource_is_generic {
return Err(invalid(format!(
"resource `{}` cannot contain generic type parameters",
resource.name
)));
}
for method in &resource.methods {
let Some(operation_name) = &method.operation_name else {
continue;
};
let Some(operation) = service.operation(operation_name) else {
continue;
};
let operation_is_generic = operation
.input_type()
.is_some_and(|input| !spec.type_parameters(input).is_empty())
|| operation
.output_type()
.is_some_and(|output| !spec.type_parameters(output).is_empty());
if operation_is_generic {
return Err(invalid(format!(
"resource-bound operation `{operation_name}` cannot use generic type parameters"
)));
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::descriptors::DescriptorIndex;
use crate::language::Language;
use crate::planning::{AuthoredValidationPass, LanguageSelectionPass};
use crate::spec::ApiSpecTree;
fn validate(language: Language, wit: &str) -> Result<()> {
let spec = crate::parser::parse_api_spec_from_wit_for_language_with_inputs(
language,
wit,
PathBuf::from("inline.wit"),
&[PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("advanced/samples/inputs/deps")],
)?;
let descriptors = DescriptorIndex::load_many(&[])?;
let authored =
AuthoredValidationPass::new(&descriptors, language).apply(ApiSpecTree::single(spec))?;
let selected = LanguageSelectionPass::new(language)
.apply(authored)
.expect("language selection is infallible");
SelectedValidationPass.apply(selected).map(|_| ())
}
const GENERIC_WIT: &str = r#"
package temporal:nexus@1.0.0;
world system { export generic-service; }
interface generic-service {
use nexus:temporal-types/model@1.0.0.{placeholder};
/// @nexus.type-parameter
type context-t = placeholder;
/// @nexus.type-parameter
type output-t = placeholder;
record inner { value: context-t, }
record request { nested: inner, values: list<context-t>, }
record response { context: context-t, output: output-t, }
complete: func(request: request) -> response;
}
"#;
#[test]
fn accepts_generic_models() {
validate(Language::Dotnet, GENERIC_WIT).unwrap();
}
#[test]
fn rejects_generic_resources() {
let resource = GENERIC_WIT.replace(
"record inner { value: context-t, }",
"resource holder { constructor(value: context-t); }\n record inner { value: context-t, }",
);
assert!(
validate(Language::Dotnet, &resource)
.unwrap_err()
.to_string()
.contains("resource")
);
}
#[test]
fn rejects_generated_type_parameter_name_collisions() {
let base = r#"
package temporal:nexus@1.0.0;
world system { export generic-service; }
interface left {
use nexus:temporal-types/model@1.0.0.{placeholder};
/// @nexus.type-parameter
type value-t = placeholder;
record payload { value: value-t, }
}
interface right {
use nexus:temporal-types/model@1.0.0.{placeholder};
/// @nexus.type-parameter
type value-t = placeholder;
record payload { value: value-t, }
}
interface generic-service {
use left.{payload as left-payload};
use right.{payload as right-payload};
record request { value: left-payload, }
record response { value: right-payload, }
execute: func(request: request) -> response;
}
"#;
assert!(
validate(Language::TypeScript, base)
.unwrap_err()
.to_string()
.contains("operation `Execute`")
);
let variant = base.replace(
"record request { value: left-payload, }",
"variant collision { left(left-payload), right(right-payload), }\n record request { value: left-payload, }",
);
assert!(
validate(Language::TypeScript, &variant)
.unwrap_err()
.to_string()
.contains("variant `generic-service.collision`")
);
}
}