use darling::FromMeta;
use super::ScopeOpts;
#[derive(Debug, Clone)]
pub enum IdSourceOpt {
Repo { generator: Option<syn::Path> },
Caller,
}
impl Default for IdSourceOpt {
fn default() -> Self {
Self::Repo { generator: None }
}
}
impl IdSourceOpt {
pub fn is_repo(&self) -> bool {
matches!(self, Self::Repo { .. })
}
pub fn marker(&self) -> proc_macro2::TokenStream {
match self {
Self::Repo { .. } => quote::quote!(es_entity::FromRepo),
Self::Caller => quote::quote!(es_entity::FromCaller),
}
}
pub fn builder(&self, id: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
match self {
Self::Repo { .. } => {
quote::quote!(es_entity::EntityEventsBuilder::__from_repo(#id))
}
Self::Caller => quote::quote!(es_entity::EntityEventsBuilder::__from_caller()),
}
}
pub fn resolve(&self, id_ty: &syn::Ident) -> proc_macro2::TokenStream {
match self {
Self::Repo {
generator: Some(path),
} => {
quote::quote!(#path(self, &mut *op).await?)
}
_ => quote::quote!(#id_ty::new()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SourceKind {
Repo,
Caller,
}
impl FromMeta for SourceKind {
fn from_string(value: &str) -> darling::Result<Self> {
match value {
"repo" => Ok(Self::Repo),
"caller" => Ok(Self::Caller),
other => Err(darling::Error::unknown_value(other)),
}
}
fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
match expr {
syn::Expr::Path(path) => match path.path.get_ident() {
Some(ident) => {
Self::from_string(&ident.to_string()).map_err(|e| e.with_span(ident))
}
None => Err(darling::Error::unexpected_expr_type(expr)),
},
syn::Expr::Lit(lit) => Self::from_value(&lit.lit),
syn::Expr::Group(group) => Self::from_expr(&group.expr),
_ => Err(darling::Error::unexpected_expr_type(expr)),
}
}
}
#[derive(Debug, Default)]
pub struct IdOpts {
pub ty: Option<syn::Ident>,
pub source: IdSourceOpt,
pub scope: Option<ScopeOpts>,
}
impl FromMeta for IdOpts {
fn from_string(value: &str) -> darling::Result<Self> {
Ok(Self {
ty: Some(syn::Ident::from_string(value)?),
..Self::default()
})
}
fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
#[derive(FromMeta)]
struct Inner {
#[darling(default)]
ty: Option<syn::Ident>,
#[darling(default)]
source: Option<darling::util::SpannedValue<SourceKind>>,
#[darling(default)]
generator: Option<darling::util::SpannedValue<syn::Path>>,
#[darling(default)]
scope: Option<ScopeOpts>,
}
let inner = Inner::from_list(items)?;
let source = match (inner.source.as_deref(), inner.generator) {
(Some(SourceKind::Caller), Some(generator)) => {
return Err(darling::Error::custom(
"`generator` needs `source = repo`: a caller-sourced id is not generated",
)
.with_span(&generator.span()));
}
(Some(SourceKind::Caller), None) => IdSourceOpt::Caller,
(_, generator) => IdSourceOpt::Repo {
generator: generator.map(|g| g.into_inner()),
},
};
Ok(Self {
ty: inner.ty,
source,
scope: inner.scope,
})
}
}
#[cfg(test)]
mod tests {
use darling::FromMeta;
use syn::parse_quote;
use super::*;
fn parse(meta: syn::Meta) -> darling::Result<IdOpts> {
IdOpts::from_meta(&meta)
}
fn err(meta: syn::Meta) -> String {
match parse(meta) {
Err(err) => err.to_string(),
Ok(opts) => panic!("expected an error, got {opts:?}"),
}
}
#[test]
fn short_form_names_the_type_and_keeps_the_defaults() {
let opts = parse(parse_quote!(id = "CustomId")).unwrap();
assert_eq!(opts.ty.unwrap().to_string(), "CustomId");
assert!(matches!(opts.source, IdSourceOpt::Repo { generator: None }));
assert!(opts.scope.is_none());
}
#[test]
fn empty_long_form_is_the_default() {
let opts = parse(parse_quote!(id())).unwrap();
assert!(opts.ty.is_none());
assert!(matches!(opts.source, IdSourceOpt::Repo { generator: None }));
assert!(opts.scope.is_none());
}
#[test]
fn long_form_takes_ty_source_and_scope() {
let opts = parse(parse_quote!(id(ty = "CustomId", source = caller, scope))).unwrap();
assert_eq!(opts.ty.unwrap().to_string(), "CustomId");
assert!(matches!(opts.source, IdSourceOpt::Caller));
assert!(opts.scope.is_some());
}
#[test]
fn source_takes_a_bare_word_or_a_string() {
let opts = parse(parse_quote!(id(source = "caller"))).unwrap();
assert!(matches!(opts.source, IdSourceOpt::Caller));
let opts = parse(parse_quote!(id(source = "repo"))).unwrap();
assert!(matches!(opts.source, IdSourceOpt::Repo { generator: None }));
}
#[test]
fn repo_source_is_explicit_or_takes_a_generator() {
let opts = parse(parse_quote!(id(source = repo))).unwrap();
assert!(matches!(opts.source, IdSourceOpt::Repo { generator: None }));
for meta in [
parse_quote!(id(source = repo, generator = "Self::next_id")),
parse_quote!(id(generator = "Self::next_id")),
] {
let opts = parse(meta).unwrap();
let IdSourceOpt::Repo {
generator: Some(generator),
} = opts.source
else {
panic!("expected a generator");
};
assert_eq!(quote::quote!(#generator).to_string(), "Self :: next_id");
}
}
#[test]
fn generator_needs_the_repo_source() {
let err = err(parse_quote!(id(
source = caller,
generator = "Self::next_id"
)));
assert!(
err.contains("`generator` needs `source = repo`"),
"unexpected error: {err}"
);
}
#[test]
fn source_rejects_an_unknown_value() {
let err = err(parse_quote!(id(source = database)));
assert!(err.contains("database"), "unexpected error: {err}");
}
#[test]
fn scope_variant_override_is_parsed() {
let opts = parse(parse_quote!(id(scope(variant = "Tenant")))).unwrap();
assert!(opts.scope.is_some());
}
#[test]
fn scope_variant_override_must_be_valid_identifier() {
let err = err(parse_quote!(id(scope(variant = "not an ident"))));
assert!(
err.contains("not a valid Rust identifier"),
"unexpected error: {err}"
);
}
#[test]
fn scope_rejects_unknown_scope_option() {
let err = err(parse_quote!(id(scope(unknown = "x"))));
assert!(
err.contains("unknown") || err.contains("Unknown"),
"unexpected error: {err}"
);
}
#[test]
fn scope_rejects_name_value_form() {
assert!(parse(parse_quote!(id(scope = true))).is_err());
}
#[test]
fn rejects_column_options_the_id_does_not_take() {
for meta in [
parse_quote!(id(find_by = false)),
parse_quote!(id(list_by = false)),
parse_quote!(id(nullable)),
parse_quote!(id(list_for)),
parse_quote!(id(create(persist = false))),
parse_quote!(id(update(persist = true))),
parse_quote!(id(parent)),
parse_quote!(id(supplied)),
parse_quote!(id(generated)),
] {
let err = err(meta);
assert!(err.contains("Unknown field"), "unexpected error: {err}");
}
}
}