use std::collections::HashSet;
use heck::{ToKebabCase, ToUpperCamelCase};
use proc_macro2::Span;
use syn::{
Error, Token,
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
};
use crate::wit_world::{ManifestPackage, SelectedDependency};
#[derive(Debug)]
pub(crate) struct DependencyRef {
pub(crate) package: String,
pub(crate) package_ident: syn::Ident,
pub(crate) interface: String,
pub(crate) interface_ident: syn::Ident,
pub(crate) span: Span,
}
#[derive(Debug)]
pub(crate) struct DependencyRefArgs {
pub(crate) refs: Vec<DependencyRef>,
}
impl Parse for DependencyRefArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let paths = Punctuated::<syn::Path, Token![,]>::parse_terminated(input)?;
let mut refs = Vec::with_capacity(paths.len());
let mut seen = HashSet::new();
for path in &paths {
let dependency_ref = parse_dependency_ref(path)?;
if !seen.insert((dependency_ref.package.clone(), dependency_ref.interface.clone())) {
return Err(Error::new(
dependency_ref.span,
format!(
"duplicate dependency reference `{}::{}`",
dependency_ref.package_ident, dependency_ref.interface_ident
),
));
}
refs.push(dependency_ref);
}
Ok(Self { refs })
}
}
fn parse_dependency_ref(path: &syn::Path) -> syn::Result<DependencyRef> {
if path.leading_colon.is_some() {
return Err(Error::new(
path.span(),
"dependency references cannot start with `::`; write `package::Interface`",
));
}
if let Some(segment) = path.segments.iter().find(|segment| !segment.arguments.is_none()) {
return Err(Error::new(
segment.arguments.span(),
"dependency references cannot use generic arguments; write `package::Interface`",
));
}
match path.segments.len() {
2 => {
let package_ident = path.segments[0].ident.clone();
let interface_ident = path.segments[1].ident.clone();
Ok(DependencyRef {
package: package_ident.to_string().to_kebab_case(),
package_ident,
interface: interface_ident.to_string().to_kebab_case(),
interface_ident,
span: path.span(),
})
}
1 => {
let package_ident = &path.segments[0].ident;
let package = package_ident.to_string().to_kebab_case();
let suggested_interface = package_ident.to_string().to_upper_camel_case();
Err(Error::new(
path.span(),
format!(
"dependency reference `{package_ident}` is missing the WIT interface name; \
write `{package_ident}::{suggested_interface}` to select the `{}` interface \
exported by package `{package}`",
suggested_interface.to_kebab_case(),
),
))
}
_ => Err(Error::new(
path.span(),
"dependency references must have exactly two segments; write `package::Interface`",
)),
}
}
pub(crate) fn select_dependencies(
manifest: &ManifestPackage,
requested: &[DependencyRef],
error_span: Span,
) -> syn::Result<Vec<SelectedDependency>> {
let dependencies = manifest.collect_miden_dependencies(error_span)?;
let available = dependencies
.iter()
.map(|dependency| dependency.name.clone())
.collect::<Vec<_>>();
requested
.iter()
.map(|reference| {
let dependency = dependencies
.iter()
.find(|dependency| dependency.name == reference.package)
.ok_or_else(|| {
Error::new(
reference.span,
format!(
"dependency `{}` is not declared in miden-project.toml; available \
dependencies: {}",
reference.package_ident,
format_available_dependencies(&available)
),
)
})?;
dependency.select(&reference.interface).ok_or_else(|| {
Error::new(
reference.span,
format!(
"dependency `{}` does not export a WIT interface named `{}` (from `{}`); \
exported interfaces: {}",
reference.package_ident,
reference.interface,
reference.interface_ident,
format_available_dependencies(
&dependency
.interface_names()
.iter()
.map(|name| name.to_string())
.collect::<Vec<_>>()
)
),
)
})
})
.collect()
}
pub(crate) fn format_available_dependencies(available: &[String]) -> String {
if available.is_empty() {
"none".to_string()
} else {
available.join(", ")
}
}
#[cfg(test)]
mod tests {
use quote::quote;
use super::*;
#[test]
fn parses_two_segment_references() {
let args = syn::parse2::<DependencyRefArgs>(quote! {
counter_contract::CounterContract, pausable::Pausable
})
.unwrap();
assert_eq!(args.refs.len(), 2);
assert_eq!(args.refs[0].package, "counter-contract");
assert_eq!(args.refs[0].interface, "counter-contract");
assert_eq!(args.refs[0].interface_ident, "CounterContract");
assert_eq!(args.refs[1].package, "pausable");
assert_eq!(args.refs[1].interface, "pausable");
}
#[test]
fn rejects_bare_package_reference_with_migration_message() {
let err = syn::parse2::<DependencyRefArgs>(quote!(counter_contract)).unwrap_err();
let message = err.to_string();
assert!(message.contains("missing the WIT interface name"));
assert!(message.contains("counter_contract::CounterContract"));
assert!(message.contains("package `counter-contract`"));
}
#[test]
fn rejects_overlong_references() {
let err = syn::parse2::<DependencyRefArgs>(quote!(a::b::C)).unwrap_err();
assert!(err.to_string().contains("exactly two segments"));
}
#[test]
fn rejects_leading_colons_and_generics() {
let err = syn::parse2::<DependencyRefArgs>(quote!(::a::B)).unwrap_err();
assert!(err.to_string().contains("cannot start with `::`"));
let err = syn::parse2::<DependencyRefArgs>(quote!(a::B<u32>)).unwrap_err();
assert!(err.to_string().contains("cannot use generic arguments"));
}
#[test]
fn rejects_duplicate_references() {
let err = syn::parse2::<DependencyRefArgs>(quote! {
counter_contract::CounterContract, counter_contract::CounterContract
})
.unwrap_err();
assert!(err.to_string().contains("duplicate dependency reference"));
}
#[test]
fn allows_distinct_interfaces_of_one_package() {
let args = syn::parse2::<DependencyRefArgs>(quote! {
counter_contract::CounterContract, counter_contract::Pausable
})
.unwrap();
assert_eq!(args.refs.len(), 2);
}
}