diff --git a/Cargo.toml b/Cargo.toml index a0375aa810..861ef7dbc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ members = [ "identity_ecdsa_verifier", "identity_eddsa_verifier", "examples", + "identity_new_resolver", + "compound_resolver", ] exclude = ["bindings/wasm", "bindings/grpc"] diff --git a/compound_resolver/Cargo.toml b/compound_resolver/Cargo.toml new file mode 100644 index 0000000000..af45437b9c --- /dev/null +++ b/compound_resolver/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "compound_resolver" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +proc-macro2 = "1.0.85" +quote = "1.0.36" +syn = { versin = "2.0.66", features = ["parsing"] } + +[lints] +workspace = true + +[lib] +proc-macro = true diff --git a/compound_resolver/src/lib.rs b/compound_resolver/src/lib.rs new file mode 100644 index 0000000000..02e6368752 --- /dev/null +++ b/compound_resolver/src/lib.rs @@ -0,0 +1,106 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse::Parse, punctuated::Punctuated, Attribute, Data, DeriveInput, Field, Ident, Token}; + +#[proc_macro_derive(CompoundResolver, attributes(resolver))] +pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { + let DeriveInput { + ident: struct_ident, + data, + generics, + .. + } = syn::parse_macro_input!(input); + + let Data::Struct(data) = data else { + panic!("Derive macro \"CompoundResolver\" only works on structs"); + }; + + data + .fields + .into_iter() + // parse all the fields that are annoted with #[resolver(..)] + .filter_map(ResolverField::from_field) + // create an iterator over (field_name, Resolver::I, Resolver::T) + .flat_map(|ResolverField { ident, impls }| { + impls + .into_iter() + .map(move |(input_ty, target_ty)| (ident.clone(), input_ty, target_ty)) + }) + // generates code that forward the implementation of Resolver to field_name. + .map(|(field_name, input_ty, target_ty)| { + quote! { + impl ::identity_new_resolver::Resolver<#target_ty, #input_ty> for #struct_ident #generics { + async fn resolve(&self, input: &#input_ty) -> std::result::Result<#target_ty, ::identity_new_resolver::Error> { + self.#field_name.resolve(input).await + } + } + } + }) + .collect::() + .into() +} + +/// A field annotated with `#[resolver(Input -> Target, ..)]` +struct ResolverField { + ident: Ident, + impls: Vec<(Ident, Ident)>, +} + +impl ResolverField { + pub fn from_field(field: Field) -> Option { + let Field { attrs, ident, .. } = field; + let Some(ident) = ident else { + panic!("Derive macro \"CompoundResolver\" only works on struct with named fields"); + }; + + let impls: Vec<(Ident, Ident)> = attrs + .into_iter() + .flat_map(|attr| parse_resolver_attribute(attr).into_iter()) + .collect(); + + if !impls.is_empty() { + Some(ResolverField { ident, impls }) + } else { + None + } + } +} + +fn parse_resolver_attribute(attr: Attribute) -> Vec<(Ident, Ident)> { + if attr.path().is_ident("resolver") { + attr + .parse_args_with(Punctuated::::parse_terminated) + .expect("invalid resolver annotation") + .into_iter() + .map(Into::into) + .collect() + } else { + vec![] + } +} + +struct ResolverTy { + pub input: Ident, + pub target: Ident, +} + +impl From for (Ident, Ident) { + fn from(value: ResolverTy) -> Self { + (value.input, value.target) + } +} + +impl Parse for ResolverTy { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut tys = Punctuated::]>::parse_separated_nonempty(input)? + .into_iter() + .take(2); + + Ok({ + ResolverTy { + input: tys.next().unwrap(), + target: tys.next().unwrap(), + } + }) + } +} diff --git a/identity_new_resolver/Cargo.toml b/identity_new_resolver/Cargo.toml new file mode 100644 index 0000000000..aadc8c7b0c --- /dev/null +++ b/identity_new_resolver/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "identity_new_resolver" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = "1.0.86" +thiserror.workspace = true +compound_resolver = { path = "../compound_resolver" } + +[lints] +workspace = true + +[dev-dependencies] +tokio = { version = "1.38.0", features = ["macros", "rt"] } diff --git a/identity_new_resolver/src/error.rs b/identity_new_resolver/src/error.rs new file mode 100644 index 0000000000..d9d9b984a9 --- /dev/null +++ b/identity_new_resolver/src/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("The requested item \"{0}\" was not found.")] + NotFound(String), + #[error("Failed to parse the provided input into a resolvable type: {0}")] + ParsingFailure(#[source] anyhow::Error), + #[error(transparent)] + Generic(anyhow::Error), +} diff --git a/identity_new_resolver/src/lib.rs b/identity_new_resolver/src/lib.rs new file mode 100644 index 0000000000..6238ab24db --- /dev/null +++ b/identity_new_resolver/src/lib.rs @@ -0,0 +1,7 @@ +mod resolver; +mod error; + +pub use error::{Result, Error}; +pub use resolver::Resolver; + +pub use compound_resolver::CompoundResolver; \ No newline at end of file diff --git a/identity_new_resolver/src/resolver.rs b/identity_new_resolver/src/resolver.rs new file mode 100644 index 0000000000..1a95ad8537 --- /dev/null +++ b/identity_new_resolver/src/resolver.rs @@ -0,0 +1,7 @@ +#![allow(async_fn_in_trait)] + +use crate::Result; + +pub trait Resolver { + async fn resolve(&self, input: &I) -> Result; +} diff --git a/identity_new_resolver/tests/compound.rs b/identity_new_resolver/tests/compound.rs new file mode 100644 index 0000000000..602a750c2b --- /dev/null +++ b/identity_new_resolver/tests/compound.rs @@ -0,0 +1,47 @@ +use identity_new_resolver::{CompoundResolver, Result, Resolver}; + +struct DidKey; +struct DidJwk; +struct DidWeb; + +struct CoreDoc; + +struct DidKeyResolver; +impl Resolver for DidKeyResolver { + async fn resolve(&self, _input: &DidKey) -> Result { + Ok(CoreDoc {}) + } +} +struct DidJwkResolver; +impl Resolver for DidJwkResolver { + async fn resolve(&self, _input: &DidJwk) -> Result { + Ok(CoreDoc {}) + } +} +struct DidWebResolver; +impl Resolver for DidWebResolver { + async fn resolve(&self, _input: &DidWeb) -> Result { + Ok(CoreDoc {}) + } +} + +#[derive(CompoundResolver)] +struct SuperDidResolver { + #[resolver(DidKey -> CoreDoc)] + did_key: DidKeyResolver, + #[resolver(DidJwk -> CoreDoc)] + did_jwk: DidJwkResolver, + #[resolver(DidWeb -> CoreDoc)] + did_web: DidWebResolver, +} + +#[tokio::test] +async fn test_compound_resolver() { + let super_resolver = SuperDidResolver { + did_key: DidKeyResolver {}, + did_jwk: DidJwkResolver {}, + did_web: DidWebResolver {}, + }; + + assert!(super_resolver.resolve(&DidJwk {}).await.is_ok()); +}