use crate::{
node::field_list_arg::{
field_or_fields_duplicate_message, parse_field_list_arg, parse_scalar_field_arg,
reject_duplicate_fields,
},
prelude::*,
};
use darling::ast::NestedMeta;
#[derive(Clone, Debug)]
pub struct Relation {
pub(crate) name: LitStr,
pub(crate) target: Path,
pub(crate) fields: Vec<LitStr>,
}
impl FromMeta for Relation {
fn from_list(items: &[NestedMeta]) -> Result<Self, DarlingError> {
let mut name = None;
let mut target = None;
let mut fields = None;
for item in items {
let NestedMeta::Meta(syn::Meta::NameValue(name_value)) = item else {
return Err(DarlingError::custom(
"relation(...) supports name = \"...\", rel = \"...\", field = \"...\", and fields = [...]",
));
};
if name_value.path.is_ident("name") {
set_relation_arg_once(
&mut name,
parse_relation_name(&name_value.value)?,
"relation(...) accepts only one name = \"...\" argument",
&name_value.path,
)?;
continue;
}
if name_value.path.is_ident("rel") {
set_relation_arg_once(
&mut target,
parse_relation_target(&name_value.value)?,
"relation(...) accepts only one rel = \"...\" argument",
&name_value.path,
)?;
continue;
}
if name_value.path.is_ident("field") {
let field = parse_scalar_field_arg("relation", &name_value.value)?;
if fields.replace(vec![field]).is_some() {
return Err(DarlingError::custom(field_or_fields_duplicate_message(
"relation",
))
.with_span(&name_value.path));
}
continue;
}
if name_value.path.is_ident("fields") {
if fields
.replace(parse_field_list_arg("relation", &name_value.value)?)
.is_some()
{
return Err(DarlingError::custom(field_or_fields_duplicate_message(
"relation",
))
.with_span(&name_value.path));
}
continue;
}
return Err(DarlingError::custom(
"relation(...) supports name = \"...\", rel = \"...\", field = \"...\", and fields = [...]",
)
.with_span(&name_value.path));
}
let Some(name) = name else {
return Err(DarlingError::custom(
"relation(...) requires name = \"...\"",
));
};
let Some(target) = target else {
return Err(DarlingError::custom("relation(...) requires rel = \"...\""));
};
let Some(fields) = fields else {
return Err(DarlingError::custom(
"relation(...) requires field = \"...\" or fields = [...]",
));
};
if fields.is_empty() {
return Err(DarlingError::custom(
"relation(fields = []) must contain at least one field",
));
}
reject_duplicate_fields(fields.as_slice(), LitStr::value, "relation")?;
Ok(Self {
name,
target,
fields,
})
}
}
fn set_relation_arg_once<T>(
target: &mut Option<T>,
value: T,
duplicate_message: &str,
span: &syn::Path,
) -> Result<(), DarlingError> {
if target.replace(value).is_some() {
return Err(DarlingError::custom(duplicate_message).with_span(span));
}
Ok(())
}
impl Relation {
pub(crate) fn validate(
&self,
fields: &FieldList,
__paths: &CratePaths,
) -> Result<(), DarlingError> {
let mut local_component_cardinality = None;
for field in &self.fields {
let field_ident = relation_field_ident(field)?;
let Some(local_field) = fields.get(&field_ident) else {
return Err(DarlingError::custom(format!(
"relation field '{}' not found",
field.value()
))
.with_span(field));
};
let local_cardinality = local_field.value.cardinality();
if local_cardinality == Cardinality::Many {
return Err(DarlingError::custom(
"relation tuple component fields cannot have many cardinality",
)
.with_span(field));
}
match local_component_cardinality {
Some(expected) if expected != local_cardinality => {
return Err(DarlingError::custom(
"relation tuple component fields must be all required or all optional",
)
.with_span(field));
}
Some(_) => {}
None => local_component_cardinality = Some(local_cardinality),
}
if local_field.generated.is_some() {
return Err(DarlingError::custom(
"relation tuple component fields cannot be generated",
)
.with_span(field));
}
}
Ok(())
}
}
impl HasSchemaPart for Relation {
fn schema_part(&self, paths: &CratePaths) -> TokenStream {
let model = &paths.model;
let name = quote_one(&self.name, to_str_lit);
let target = quote_one(&self.target, |value| to_path(value, paths));
let fields = quote_slice(&self.fields, to_str_lit);
quote! {
#model::node::RelationEdge::new(#name, #target, #fields)
}
}
}
fn parse_relation_name(expr: &syn::Expr) -> Result<LitStr, DarlingError> {
let literal = parse_relation_string_arg("name", expr)?;
if literal.value().is_empty() {
return Err(DarlingError::custom("relation name cannot be empty").with_span(&literal));
}
Ok(literal)
}
fn parse_relation_target(expr: &syn::Expr) -> Result<Path, DarlingError> {
let literal = parse_relation_string_arg("rel", expr)?;
syn::parse_str::<Path>(literal.value().as_str()).map_err(|_| {
DarlingError::custom(format!(
"relation target '{}' is not a valid Rust path",
literal.value()
))
.with_span(&literal)
})
}
fn parse_relation_string_arg(name: &str, expr: &syn::Expr) -> Result<LitStr, DarlingError> {
let syn::Expr::Lit(expr_lit) = expr else {
return Err(DarlingError::custom(format!(
"relation({name} = ...) requires a string literal"
))
.with_span(expr));
};
let syn::Lit::Str(literal) = &expr_lit.lit else {
return Err(DarlingError::custom(format!(
"relation({name} = ...) requires a string literal"
))
.with_span(expr));
};
Ok(literal.clone())
}
fn relation_field_ident(field: &LitStr) -> Result<Ident, DarlingError> {
syn::parse_str::<Ident>(field.value().as_str()).map_err(|_| {
DarlingError::custom(format!(
"relation field '{}' is not a valid Rust field identifier",
field.value()
))
.with_span(field)
})
}
#[cfg(test)]
mod tests;