use crate::{
PipelineKind, deserialization_impl::generate_custom_deserialize,
imports::generate_struct_imports,
};
use convert_case::{Case, Casing};
use evenframe_core::{
derive::{
attributes::{
find_duplicate_index_name, parse_annotation_attributes, parse_event_attributes,
parse_field_index_attributes, parse_format_attribute, parse_index_attributes,
parse_macroforge_derive_attribute, parse_mock_data_attribute, parse_relation_attribute,
parse_rust_derives,
},
validator_parser::parse_field_validators,
},
schemasync::{
Bm25, DefineConfig, EdgeConfig, IndexKind, PermissionsConfig, VectorDistance, VectorType,
},
types::FieldType,
};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Fields, LitStr};
pub fn generate_struct_impl(input: DeriveInput, pipeline: PipelineKind) -> TokenStream {
let ident = input.ident.clone();
let imports = generate_struct_imports();
if let Data::Struct(ref data_struct) = input.data {
let fields_named = if let Fields::Named(ref fields_named) = data_struct.fields {
fields_named
} else {
return syn::Error::new(
ident.span(),
format!("Evenframe derive macro only supports structs with named fields.\n\nExample of a valid struct:\n\nstruct {} {{\n id: String,\n name: String,\n}}", ident),
)
.to_compile_error();
};
let permissions_config = match PermissionsConfig::parse(&input.attrs) {
Ok(config) => config,
Err(err) => {
return syn::Error::new(
input.span(),
format!("Failed to parse permissions configuration: {}\n\nExample usage:\n#[permissions(\n select = \"true\",\n create = \"auth.role == 'admin'\",\n update = \"$auth.id == id\",\n delete = \"false\"\n)]\nstruct MyStruct {{ ... }}", err)
)
.to_compile_error();
}
};
let mock_data_config = match parse_mock_data_attribute(&input.attrs) {
Ok(config) => config,
Err(err) => return err.to_compile_error(),
};
let table_validators = match parse_field_validators(&input.attrs) {
Ok(validators) => validators,
Err(err) => {
return syn::Error::new(
input.span(),
format!("Failed to parse table validators: {}\n\nExample usage:\n#[validators(StringValidator::MinLength(5))]\nstruct MyStruct {{ ... }}", err)
)
.to_compile_error();
}
};
let relation_config = match parse_relation_attribute(&input.attrs) {
Ok(Some(mut config)) => {
if config.edge_name.is_empty() {
config.edge_name = {
let name = ident.to_string();
let mut snake = String::new();
for (i, ch) in name.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
snake.push('_');
}
snake.push(ch.to_lowercase().next().unwrap());
}
snake
};
}
Some(config)
}
Ok(None) => None,
Err(err) => return err.to_compile_error(),
};
let events = match parse_event_attributes(&input.attrs) {
Ok(events) => events,
Err(err) => {
return syn::Error::new(
input.span(),
format!(
"Failed to parse event attribute: {}\n\nExample usage:\n#[event(\"DEFINE EVENT my_event ON TABLE my_table WHEN $before != $after THEN ...\")]",
err
),
)
.to_compile_error();
}
};
let macroforge_derives = match parse_macroforge_derive_attribute(&input.attrs) {
Ok(derives) => derives,
Err(err) => return err.to_compile_error(),
};
let struct_annotations = match parse_annotation_attributes(&input.attrs) {
Ok(annotations) => annotations,
Err(err) => return err.to_compile_error(),
};
let rust_derives = parse_rust_derives(&input.attrs);
let known_field_names: std::collections::BTreeSet<String> = fields_named
.named
.iter()
.filter_map(|f| {
f.ident
.as_ref()
.map(|i| i.to_string().trim_start_matches("r#").to_string())
})
.collect();
let (mut indexes, mut index_spans): (Vec<_>, Vec<Span>) =
match parse_index_attributes(&input.attrs, &known_field_names) {
Ok(v) => v.into_iter().unzip(),
Err(err) => return err.to_compile_error(),
};
let has_id = fields_named.named.iter().any(|field| {
field.ident.as_ref().map(|id| id == "id").unwrap_or(false)
});
let mut table_field_tokens = Vec::new();
let mut json_assignments = Vec::new();
for field in fields_named.named.iter() {
let field_ident = match field.ident.as_ref() {
Some(ident) => ident,
None => {
return syn::Error::new(
field.span(),
"Internal error: Field identifier is missing. This should not happen with named fields."
)
.to_compile_error();
}
};
let field_name = field_ident.to_string();
let field_name_trim = field_name.trim_start_matches("r#");
let ty = &field.ty;
let field_type = FieldType::parse_syn_ty(ty);
let edge_config = match EdgeConfig::parse(field) {
Ok(details) => details,
Err(err) => {
return syn::Error::new(
field.span(),
format!("Failed to parse edge configuration for field '{}': {}\n\nExample usage:\n#[edge(name = \"has_user\", direction = \"from\", to = \"User\")]\npub user: RecordLink<User>", field_name, err)
)
.to_compile_error();
}
};
let define_config = match DefineConfig::parse(field) {
Ok(details) => details,
Err(err) => {
return syn::Error::new(
field.span(),
format!("Failed to parse define configuration for field '{}': {}\n\nExample usage:\n#[define(default = \"0\", readonly = true)]\npub count: u32", field_name, err)
)
.to_compile_error();
}
};
let format = match parse_format_attribute(&field.attrs) {
Ok(fmt) => fmt,
Err(err) => {
return syn::Error::new(
field.span(),
format!(
"Failed to parse format attribute for field '{}': {}",
field_name, err
),
)
.to_compile_error();
}
};
let field_validators = match parse_field_validators(&field.attrs) {
Ok(v) => v,
Err(err) => {
return syn::Error::new(
field.span(),
format!("Failed to parse validators for field '{}': {}\n\nExample usage:\n#[validate(min_length = 3, max_length = 50)]\npub name: String\n\n#[validate(email)]\npub email: String", field_name, err)
)
.to_compile_error();
}
};
let edge_config_tokens = if let Some(ref details) = edge_config {
quote! {
Some(#details)
}
} else {
quote! { None }
};
let define_config_tokens = if let Some(ref define) = define_config {
quote! {
Some(#define)
}
} else {
quote! { None }
};
let format_tokens = if let Some(ref fmt) = format {
quote! { Some(#fmt) }
} else {
quote! { None }
};
let field_annotations = match parse_annotation_attributes(&field.attrs) {
Ok(annotations) => annotations,
Err(err) => return err.to_compile_error(),
};
let is_unique = field
.attrs
.iter()
.any(|attr| attr.path().is_ident("unique"));
match parse_field_index_attributes(field_name_trim, &field.attrs) {
Ok(field_indexes) => {
for (index, span) in field_indexes {
indexes.push(index);
index_spans.push(span);
}
}
Err(err) => return err.to_compile_error(),
}
let validators_tokens = if field_validators.is_empty() {
quote! { vec![] }
} else {
quote! { vec![#(#field_validators),*] }
};
let field_annotations_tokens = if field_annotations.is_empty() {
quote! { vec![] }
} else {
quote! { vec![#(#field_annotations.to_string()),*] }
};
table_field_tokens.push(quote! {
StructField {
field_name: #field_name_trim.to_string(),
field_type: #field_type,
edge_config: #edge_config_tokens,
define_config: #define_config_tokens,
format: #format_tokens,
validators: #validators_tokens,
always_regenerate: false,
doccom: None,
annotations: #field_annotations_tokens,
unique: #is_unique,
output_override: None,
raw_attributes: std::collections::BTreeMap::new(),
}
});
if field_name != "id" && edge_config.is_none() {
json_assignments.push(quote! {
#field_name: payload.#field_ident,
});
}
}
let struct_name = ident.to_string();
let table_name = ident.to_string();
let permissions_config_tokens = if let Some(ref config) = permissions_config {
quote! { Some(#config) }
} else {
quote! { None }
};
let table_validators_tokens = if !table_validators.is_empty() {
quote! { vec![#(#table_validators),*] }
} else {
quote! { vec![] }
};
let mock_data_tokens = if let Some(config) = mock_data_config {
quote! { Some(#config) }
} else {
quote! { None }
};
let relation_tokens = if let Some(ref rel) = relation_config {
quote! { Some(#rel) }
} else {
quote! { None }
};
let event_tokens = if events.is_empty() {
quote! { Vec::new() }
} else {
let event_configs = events.iter().map(|statement| {
let lit = LitStr::new(statement, Span::call_site());
quote! {
::evenframe::schemasync::EventConfig {
statement: String::from(#lit),
}
}
});
quote! { vec![#(#event_configs),*] }
};
let macroforge_derives_tokens = if macroforge_derives.is_empty() {
quote! { vec![] }
} else {
quote! { vec![#(#macroforge_derives.to_string()),*] }
};
let struct_annotations_tokens = if struct_annotations.is_empty() {
quote! { vec![] }
} else {
quote! { vec![#(#struct_annotations.to_string()),*] }
};
let rust_derives_tokens = if rust_derives.is_empty() {
quote! { vec![] }
} else {
quote! { vec![#(#rust_derives.to_string()),*] }
};
let pipeline_tokens = pipeline.to_tokens();
if let Some((position, name)) =
find_duplicate_index_name(&ident.to_string().to_case(Case::Snake), &indexes)
{
return syn::Error::new(
index_spans
.get(position)
.copied()
.unwrap_or_else(|| ident.span()),
format!(
"another index on `{ident}` already uses the name `{name}`; \
give one of them `name = \"...\"`"
),
)
.to_compile_error();
}
let indexes_tokens = if indexes.is_empty() {
quote! { vec![] }
} else {
let entries = indexes.iter().map(|idx| {
let field_strs = idx.fields.iter().map(|s| quote! { #s.to_string() });
let name = option_tokens(&idx.name, |s| quote! { #s.to_string() });
let kind = index_kind_tokens(&idx.kind);
let comment = option_tokens(&idx.comment, |s| quote! { #s.to_string() });
let concurrently = idx.concurrently;
quote! {
::evenframe::schemasync::IndexConfig {
fields: vec![ #(#field_strs),* ],
name: #name,
kind: #kind,
comment: #comment,
concurrently: #concurrently,
}
}
});
quote! { vec![ #(#entries),* ] }
};
let evenframe_persistable_struct_impl = {
quote! {
impl EvenframePersistableStruct for #ident {
fn static_table_config() -> TableConfig {
TableConfig {
table_name: #table_name.to_case(Case::Snake),
struct_config: ::evenframe::types::StructConfig {
struct_name: #struct_name.to_owned(),
fields: vec![ #(#table_field_tokens),* ],
validators: #table_validators_tokens,
doccom: None,
macroforge_derives: #macroforge_derives_tokens,
annotations: #struct_annotations_tokens,
pipeline: #pipeline_tokens,
rust_derives: #rust_derives_tokens,
output_override: None,
resolve_only: false,
raw_attributes: std::collections::BTreeMap::new(),
},
relation: #relation_tokens,
permissions: #permissions_config_tokens,
mock_generation_config: #mock_data_tokens,
events: #event_tokens,
indexes: #indexes_tokens,
output_override: None,
}
}
}
}
};
let has_field_validators = fields_named.named.iter().any(|field| {
match parse_field_validators(&field.attrs) {
Ok(validators) => !validators.is_empty(),
Err(_) => true, }
});
let deserialize_impl = if has_field_validators || !table_validators.is_empty() {
generate_custom_deserialize(&input)
} else {
quote! {}
};
if has_id {
let registry_var_name = syn::Ident::new(
&format!("{}_REGISTRY_ENTRY", ident.to_string().to_uppercase()),
ident.span(),
);
let registry_submission = quote! {
#[::evenframe::linkme::distributed_slice(::evenframe::registry::TABLE_REGISTRY_ENTRIES)]
static #registry_var_name: ::evenframe::registry::TableRegistryEntry = ::evenframe::registry::TableRegistryEntry {
type_name: #struct_name,
table_config_fn: || #ident::static_table_config(),
pipeline: #pipeline_tokens,
};
};
quote! {
const _: () = {
#imports
#evenframe_persistable_struct_impl
#registry_submission
};
#deserialize_impl
}
} else {
let struct_config_impl = quote! {
impl #ident {
pub fn static_struct_config() -> ::evenframe::types::StructConfig {
::evenframe::types::StructConfig {
struct_name: #struct_name.to_owned(),
fields: vec![ #(#table_field_tokens),* ],
validators: #table_validators_tokens,
doccom: None,
macroforge_derives: #macroforge_derives_tokens,
annotations: #struct_annotations_tokens,
pipeline: #pipeline_tokens,
rust_derives: #rust_derives_tokens,
output_override: None,
resolve_only: false,
raw_attributes: std::collections::BTreeMap::new(),
}
}
}
};
let registry_var_name = syn::Ident::new(
&format!("{}_OBJECT_REGISTRY_ENTRY", ident.to_string().to_uppercase()),
ident.span(),
);
let registry_submission = quote! {
#[::evenframe::linkme::distributed_slice(::evenframe::registry::OBJECT_REGISTRY_ENTRIES)]
static #registry_var_name: ::evenframe::registry::ObjectRegistryEntry = ::evenframe::registry::ObjectRegistryEntry {
type_name: #struct_name,
struct_config_fn: || #ident::static_struct_config(),
pipeline: #pipeline_tokens,
};
};
let app_imports = quote! {
use ::evenframe::types::{StructConfig, StructField, FieldType};
use ::evenframe::registry;
use ::evenframe::prelude::linkme;
};
quote! {
const _: () = {
#app_imports
#struct_config_impl
#registry_submission
};
#deserialize_impl
}
}
} else {
syn::Error::new(
ident.span(),
format!("The Evenframe derive macro can only be applied to structs.\n\nYou tried to apply it to: {}\n\nExample of correct usage:\n#[derive(Evenframe)]\nstruct MyStruct {{\n id: String,\n // ... other fields\n}}", ident)
)
.to_compile_error()
}
}
fn option_tokens<T>(value: &Option<T>, some: impl Fn(&T) -> TokenStream) -> TokenStream {
match value {
Some(v) => {
let inner = some(v);
quote! { ::core::option::Option::Some(#inner) }
}
None => quote! { ::core::option::Option::None },
}
}
fn vector_distance_tokens(dist: &VectorDistance) -> TokenStream {
let path = quote! { ::evenframe::schemasync::VectorDistance };
match dist {
VectorDistance::Euclidean => quote! { #path::Euclidean },
VectorDistance::Cosine => quote! { #path::Cosine },
VectorDistance::CosineNormalized => quote! { #path::CosineNormalized },
VectorDistance::InnerProduct => quote! { #path::InnerProduct },
VectorDistance::Manhattan => quote! { #path::Manhattan },
VectorDistance::Chebyshev => quote! { #path::Chebyshev },
VectorDistance::Hamming => quote! { #path::Hamming },
VectorDistance::Jaccard => quote! { #path::Jaccard },
VectorDistance::Pearson => quote! { #path::Pearson },
VectorDistance::Minkowski(order) => quote! { #path::Minkowski(#order) },
}
}
fn vector_type_tokens(vector_type: &VectorType) -> TokenStream {
let path = quote! { ::evenframe::schemasync::VectorType };
match vector_type {
VectorType::F64 => quote! { #path::F64 },
VectorType::F32 => quote! { #path::F32 },
VectorType::F16 => quote! { #path::F16 },
VectorType::I64 => quote! { #path::I64 },
VectorType::I32 => quote! { #path::I32 },
VectorType::I16 => quote! { #path::I16 },
VectorType::I8 => quote! { #path::I8 },
VectorType::U8 => quote! { #path::U8 },
}
}
fn index_kind_tokens(kind: &IndexKind) -> TokenStream {
let path = quote! { ::evenframe::schemasync::IndexKind };
match kind {
IndexKind::Standard => quote! { #path::Standard },
IndexKind::Unique => quote! { #path::Unique },
IndexKind::Count { where_clause } => {
let where_clause = option_tokens(where_clause, |s| quote! { #s.to_string() });
quote! { #path::Count { where_clause: #where_clause } }
}
IndexKind::FullText {
analyzer,
bm25,
highlights,
} => {
let analyzer = option_tokens(analyzer, |s| quote! { #s.to_string() });
let bm25 = option_tokens(bm25, |b| match b {
Bm25::Default => quote! { ::evenframe::schemasync::Bm25::Default },
Bm25::Params { k1, b } => {
quote! { ::evenframe::schemasync::Bm25::Params { k1: #k1, b: #b } }
}
});
quote! {
#path::FullText {
analyzer: #analyzer,
bm25: #bm25,
highlights: #highlights,
}
}
}
IndexKind::Hnsw {
dimension,
dist,
vector_type,
efc,
m,
m0,
lm,
extend_candidates,
keep_pruned_connections,
hashed_vector,
} => {
let dist = option_tokens(dist, vector_distance_tokens);
let vector_type = option_tokens(vector_type, vector_type_tokens);
let efc = option_tokens(efc, |v| quote! { #v });
let m = option_tokens(m, |v| quote! { #v });
let m0 = option_tokens(m0, |v| quote! { #v });
let lm = option_tokens(lm, |v| quote! { #v });
quote! {
#path::Hnsw {
dimension: #dimension,
dist: #dist,
vector_type: #vector_type,
efc: #efc,
m: #m,
m0: #m0,
lm: #lm,
extend_candidates: #extend_candidates,
keep_pruned_connections: #keep_pruned_connections,
hashed_vector: #hashed_vector,
}
}
}
IndexKind::DiskAnn {
dimension,
dist,
vector_type,
degree,
l_build,
alpha,
hashed_vector,
} => {
let dist = option_tokens(dist, vector_distance_tokens);
let vector_type = option_tokens(vector_type, vector_type_tokens);
let degree = option_tokens(degree, |v| quote! { #v });
let l_build = option_tokens(l_build, |v| quote! { #v });
let alpha = option_tokens(alpha, |v| quote! { #v });
quote! {
#path::DiskAnn {
dimension: #dimension,
dist: #dist,
vector_type: #vector_type,
degree: #degree,
l_build: #l_build,
alpha: #alpha,
hashed_vector: #hashed_vector,
}
}
}
}
}