#[cfg(test)]
mod tests;
use crate::prelude::*;
#[derive(Clone, Debug, Default, FromMeta)]
pub struct Item {
#[darling(default)]
pub(crate) is: Option<Path>,
#[darling(default, rename = "prim")]
pub(crate) primitive: Option<Primitive>,
#[darling(default)]
pub(crate) scale: Option<u32>,
#[darling(default)]
pub(crate) max_len: Option<u32>,
#[darling(default)]
pub(crate) max_bytes: Option<u32>,
#[darling(default)]
pub(crate) unbounded: bool,
#[darling(default, rename = "rel")]
pub(crate) relation: Option<Path>,
#[darling(multiple, rename = "normalizer")]
pub(crate) normalizers: Vec<TypeNormalizer>,
#[darling(multiple, rename = "validator")]
pub(crate) validators: Vec<TypeValidator>,
#[darling(default)]
pub(crate) indirect: bool,
}
impl Item {
pub fn validate(&self) -> Result<(), DarlingError> {
if self.is.is_some() && self.primitive.is_some() {
return Err(DarlingError::custom(
"item may not specify both is and prim",
));
}
if let Some(relation) = &self.relation
&& self.primitive.is_none()
{
return Err(
DarlingError::custom(
"rel fields must explicitly declare prim = \"...\"; implicit identity types are forbidden",
)
.with_span(relation),
);
}
if let Some(relation) = &self.relation
&& self.indirect
{
return Err(
DarlingError::custom("relations cannot be set to indirect").with_span(relation)
);
}
if self.scale.is_some() && !matches!(self.primitive, Some(Primitive::Decimal)) {
return Err(DarlingError::custom(
"scale may only be used with prim = \"Decimal\"",
));
}
if matches!(self.primitive, Some(Primitive::Decimal)) && self.scale.is_none() {
return Err(DarlingError::custom(
"prim = \"Decimal\" requires item(scale = N)",
));
}
if self.max_len.is_some()
&& !matches!(self.primitive, Some(Primitive::Text | Primitive::Blob))
{
return Err(DarlingError::custom(
"max_len may only be used with prim = \"Text\" or prim = \"Blob\"",
));
}
if self.max_len.is_some_and(|max_len| max_len == 0) {
return Err(DarlingError::custom(
"item(max_len = N) requires a positive value",
));
}
if self.max_bytes.is_some()
&& !matches!(self.primitive, Some(Primitive::IntBig | Primitive::NatBig))
{
return Err(DarlingError::custom(
"max_bytes may only be used with prim = \"IntBig\" or prim = \"NatBig\"",
));
}
if self.max_bytes.is_some_and(|max_bytes| max_bytes == 0) {
return Err(DarlingError::custom(
"item(max_bytes = N) requires a positive value",
));
}
if self.unbounded && !matches!(self.primitive, Some(Primitive::Text | Primitive::Blob)) {
return Err(DarlingError::custom(
"unbounded may only be used with prim = \"Text\" or prim = \"Blob\"",
));
}
if self.unbounded && self.max_len.is_some() {
return Err(DarlingError::custom(
"unbounded cannot be combined with max_len",
));
}
if self.max_len.is_none() && !self.unbounded {
match self.primitive {
Some(Primitive::Text) => {
return Err(DarlingError::custom(
"prim = \"Text\" requires either item(max_len = N) or item(unbounded)",
));
}
Some(Primitive::Blob) => {
return Err(DarlingError::custom(
"prim = \"Blob\" requires either item(max_len = N) or item(unbounded)",
));
}
_ => {}
}
}
Ok(())
}
pub fn target(&self) -> ItemTarget {
debug_assert!(
!(self.is.is_some() && self.primitive.is_some()),
"item 'is' cannot be combined with 'prim'",
);
if let Some(path) = &self.is {
return ItemTarget::Is(path.clone());
}
if let Some(prim) = &self.primitive {
return ItemTarget::Primitive(*prim);
}
ItemTarget::Primitive(Primitive::Unit)
}
pub const fn is_relation(&self) -> bool {
self.relation.is_some()
}
}
impl HasSchemaPart for Item {
fn schema_part(&self) -> TokenStream {
let target = self.target().schema_part();
let relation = quote_option(self.relation.as_ref(), to_path);
let scale = quote_option(self.scale.as_ref(), |scale| quote!(#scale));
let max_len = quote_option(self.max_len.as_ref(), |max_len| quote!(#max_len));
let max_bytes = quote_option(self.max_bytes.as_ref(), |max_bytes| quote!(#max_bytes));
let validators = quote_slice(&self.validators, TypeValidator::schema_part);
let normalizers = quote_slice(&self.normalizers, TypeNormalizer::schema_part);
let indirect = self.indirect;
quote! {
::icydb_model::node::Item::new(
#target,
#relation,
#scale,
#max_len,
#max_bytes,
#validators,
#normalizers,
#indirect,
)
}
}
}
impl HasTypeExpr for Item {
fn type_expr(&self) -> TokenStream {
let ty = self.target().type_expr();
if self.indirect {
quote!(Box<#ty>)
} else {
quote!(#ty)
}
}
}
pub enum ItemTarget {
Is(Path),
Primitive(Primitive),
}
impl HasSchemaPart for ItemTarget {
fn schema_part(&self) -> TokenStream {
match self {
Self::Is(path) => {
let path = quote_one(path, to_path);
quote! {
::icydb_model::node::ItemTarget::Is(#path)
}
}
Self::Primitive(prim) => {
quote! {
::icydb_model::node::ItemTarget::Primitive(#prim)
}
}
}
}
}
impl HasTypeExpr for ItemTarget {
fn type_expr(&self) -> TokenStream {
match self {
Self::Is(path) => quote!(#path),
Self::Primitive(prim) => {
let ty = crate::types::primitive_type_tokens(*prim);
quote!(#ty)
}
}
}
}