use crate::{
PipelineKind,
deserialization_impl::generate_custom_deserialize,
imports::generate_struct_imports,
surreal_value_impl::{Mode, struct_surreal_value},
validate_impl::{CheckedField, struct_validate},
};
use convert_case::{Case, Casing};
use evenframe_core::{
derive::{
attributes::{
find_duplicate_index_name, indexable_fields, parse_event_attributes,
parse_field_index_attributes, parse_format_attribute, parse_index_attributes,
parse_mock_data_attribute, parse_relation_attribute, parse_rust_derives,
},
naming,
schemasync_attributes::{parse_validator_overrides, refuse_container_validators},
typesync_attributes::{Position, TypesyncAttributes},
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 wire = match naming::resolve(&input) {
Ok(wire) => wire,
Err(err) => return err.to_compile_error(),
};
if wire.shape != naming::ItemShape::Named {
return crate::newtype_impl::generate_newtype_impl(&input, pipeline, &wire);
}
let Fields::Named(ref fields_named) = data_struct.fields else {
return syn::Error::new(
ident.span(),
"a struct written as an object has named fields",
)
.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{}\nstruct MyStruct {{ ... }}", err, PermissionsConfig::EXAMPLE)
)
.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 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();
}
};
if let Err(err) = refuse_container_validators(&input.attrs) {
return err.to_compile_error();
}
let TypesyncAttributes {
macroforge_derives,
annotations: struct_annotations,
..
} = match TypesyncAttributes::parse(&input.attrs, Position::Container) {
Ok(typesync) => typesync,
Err(err) => return err.to_compile_error(),
};
let rust_derives = parse_rust_derives(&input.attrs);
let known_field_names = indexable_fields(&fields_named.named);
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()
.zip(&wire.fields)
.any(|(field, field_wire)| {
field_wire.surreal.as_deref().map_or_else(
|| {
field
.ident
.as_ref()
.is_some_and(|ident| naming::unraw(ident) == "id")
},
|name| name == "id",
)
});
let mut table_field_tokens = Vec::new();
let mut fields_validators = Vec::new();
for (field, field_wire) in fields_named.named.iter().zip(&wire.fields) {
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 = naming::unraw(field_ident);
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(
err.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(validators) => validators,
Err(err) => {
return syn::Error::new(
err.span(),
format!("Failed to parse validators for field '{field_name}': {err}"),
)
.to_compile_error();
}
};
let validator_overrides = match parse_validator_overrides(&field.attrs) {
Ok(overrides) => overrides,
Err(err) => return 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 TypesyncAttributes::parse(&field.attrs, Position::Field) {
Ok(typesync) => typesync.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, &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 morph_tokens = field_validators.morph_tokens();
let morphs_tokens = quote! { vec![#(#morph_tokens),*] };
let config_tokens = field_validators.config_tokens();
let validators_tokens = quote! { vec![#(#config_tokens),*] };
fields_validators.push(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.to_string(),
field_type: #field_type,
wire: #field_wire,
edge_config: #edge_config_tokens,
define_config: #define_config_tokens,
format: #format_tokens,
morphs: #morphs_tokens,
validators: #validators_tokens,
validator_overrides: #validator_overrides,
always_regenerate: false,
doccom: None,
annotations: #field_annotations_tokens,
unique: #is_unique,
output_override: None,
raw_attributes: std::collections::BTreeMap::new(),
}
});
}
let struct_name = ident.to_string();
let table_name = ident.to_string().to_case(Case::Snake);
let permissions_config_tokens = if let Some(ref config) = permissions_config {
quote! { Some(#config) }
} else {
quote! { None }
};
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 (struct_pipeline, written_as) = match &wire.wire_as {
Some(written) => (
pipeline.schemasync_part(),
pipeline.typesync_part().map(|typesync| {
crate::newtype_impl::written_as_metadata(&input, written, typesync)
}),
),
None => (Some(pipeline), None),
};
let pipeline_tokens = struct_pipeline.unwrap_or(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_owned(),
struct_config: ::evenframe::types::StructConfig {
struct_name: #struct_name.to_owned(),
fields: vec![ #(#table_field_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_validators
.iter()
.any(|validators| !validators.is_empty());
let checked_fields: Vec<CheckedField> = fields_named
.named
.iter()
.zip(&wire.fields)
.zip(&fields_validators)
.enumerate()
.map(|(position, ((field, field_wire), validators))| {
let path = field_wire
.serde
.clone()
.unwrap_or_else(|| field.ident.as_ref().map(naming::unraw).unwrap_or_default());
CheckedField::new(field, position, path, validators)
})
.collect();
let validate_impl = match struct_validate(&input, &checked_fields) {
Ok(tokens) => tokens,
Err(err) => return err.to_compile_error(),
};
let surreal_value_impl = if pipeline.reaches_database() {
match struct_surreal_value(&input, &wire, Mode::Stored, &checked_fields) {
Ok(tokens) => tokens,
Err(err) => return err.to_compile_error(),
}
} else {
TokenStream::new()
};
let deserialize_impl = if has_field_validators {
match generate_custom_deserialize(&input, &checked_fields) {
Ok(tokens) => tokens,
Err(err) => return err.to_compile_error(),
}
} 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 = struct_pipeline.is_some().then(|| quote! {
#[::evenframe::linkme::distributed_slice(::evenframe::registry::TABLE_REGISTRY_ENTRIES)]
#[linkme(crate = ::evenframe::linkme)]
static #registry_var_name: ::evenframe::registry::TableRegistryEntry = ::evenframe::registry::TableRegistryEntry {
type_name: #struct_name,
table_config_fn: || #ident::static_table_config(),
pipeline: #pipeline_tokens,
};
});
let metadata_gate = crate::metadata::gate(
&input,
quote! {
const _: () = {
#imports
#evenframe_persistable_struct_impl
#registry_submission
};
},
);
quote! {
impl ::evenframe::traits::EvenframeTable for #ident {}
#metadata_gate
#written_as
#validate_impl
#deserialize_impl
#surreal_value_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),* ],
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 = struct_pipeline.is_some().then(|| quote! {
#[::evenframe::linkme::distributed_slice(::evenframe::registry::OBJECT_REGISTRY_ENTRIES)]
#[linkme(crate = ::evenframe::linkme)]
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;
};
let metadata_gate = crate::metadata::gate(
&input,
quote! {
const _: () = {
#app_imports
#struct_config_impl
#registry_submission
};
},
);
quote! {
#metadata_gate
#written_as
#validate_impl
#deserialize_impl
#surreal_value_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,
}
}
}
}
}