use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Expr, ExprLit, Lit, Meta};
pub fn derive_model(input: DeriveInput) -> TokenStream {
let name = &input.ident;
let where_name = syn::Ident::new(&format!("{name}Where"), name.span());
let table_name = extract_table_name(&input);
let is_tuple_struct = matches!(&input.data, syn::Data::Struct(data) if matches!(&data.fields, syn::Fields::Unnamed(_)));
if is_tuple_struct {
return derive_model_tuple_wrapper(&input, name, &where_name, table_name);
}
let fields = match &input.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Named(fields) => &fields.named,
_ => panic!("Model must have named fields or be a tuple struct wrapper"),
},
_ => panic!("Model must be a struct"),
};
let primary_keys: Vec<_> = fields
.iter()
.filter_map(|f| {
for attr in &f.attrs {
if attr.path().is_ident("primary") {
let field_name = f.ident.as_ref().unwrap().clone();
let is_auto = if let Meta::List(list) = &attr.meta {
list.tokens.to_string().contains("auto")
} else {
false
};
return Some((field_name, is_auto));
}
}
None
})
.collect();
if primary_keys.is_empty() {
panic!("Model must have at least one #[primary] field");
}
let auto_count = primary_keys.iter().filter(|(_, is_auto)| *is_auto).count();
if auto_count > 1 {
panic!("Only one primary key field can have #[primary(auto)]");
}
let primary_key_field = &primary_keys[0].0;
let is_auto_increment = primary_keys[0].1;
let auto_increment_key_type = if is_auto_increment {
let pk_type = &fields
.iter()
.find(|f| f.ident.as_ref().unwrap() == primary_key_field)
.map(|f| &f.ty)
.expect("Primary key field not found");
quote! { #pk_type }
} else {
quote! { () }
};
let primary_key_field_names: Vec<_> = primary_keys
.iter()
.map(|(field_name, _)| {
quote! { stringify!(#field_name) }
})
.collect();
let primary_key_values: Vec<_> = primary_keys
.iter()
.map(|(field_name, _)| {
let field = fields
.iter()
.find(|f| f.ident.as_ref().unwrap() == field_name)
.expect("Primary key field not found");
field_to_value_expr(field)
})
.collect();
let primary_key_value_expr = {
let field = fields
.iter()
.find(|f| f.ident.as_ref().unwrap() == primary_key_field)
.expect("Primary key field not found");
field_to_value_expr(field)
};
let field_names: Vec<String> = fields
.iter()
.map(|f| f.ident.as_ref().unwrap().to_string())
.collect();
let field_names_lit = field_names.iter().map(|name| {
quote! { #name }
});
let column_schema_entries = fields.iter().map(|f| {
let field_name = f.ident.as_ref().unwrap();
let field_type = &f.ty;
let type_str = normalize_type_string(quote! { #field_type }.to_string());
let is_primary = f.attrs.iter().any(|attr| attr.path().is_ident("primary"));
let field_is_auto_increment = if is_primary { is_auto_increment } else { false };
let is_nullable = type_str.starts_with("Option<");
let rust_type = if is_nullable {
type_str
.strip_prefix("Option<")
.and_then(|ty| ty.strip_suffix('>'))
.unwrap_or(&type_str)
.trim()
.to_string()
} else {
type_str
};
let unique_group = extract_unique_group(f);
let is_indexed = f.attrs.iter().any(|attr| attr.path().is_ident("index"));
let foreign_key = extract_foreign_key(f);
let data_type = extract_data_type(f);
let has_data_type = has_data_type(f);
let hypertable = extract_hypertable(f);
let compress = f.attrs.iter().any(|attr| attr.path().is_ident("compress"));
let enum_variants = if has_data_type {
quote! { None }
} else {
quote! { <#field_type as ::ormer::model::ModelEnumProvider>::ENUM_VARIANTS }
};
quote! {
::ormer::model::ColumnSchema {
name: stringify!(#field_name),
rust_type: #rust_type,
is_primary: #is_primary,
is_auto_increment: #field_is_auto_increment,
is_nullable: #is_nullable,
unique_group: #unique_group,
is_indexed: #is_indexed,
foreign_key: #foreign_key,
enum_variants: #enum_variants,
data_type: #data_type,
hypertable: #hypertable,
compress: #compress,
}
}
});
let from_row_fields = fields.iter().map(|f| {
let field_name = f.ident.as_ref().unwrap();
if has_i32_data_type(f) {
field_from_i32_expr(
f,
quote! { row.get::<i32>(stringify!(#field_name))? },
quote! { row.get::<Option<i32>>(stringify!(#field_name))? },
)
} else if has_vec_i32_data_type(f) {
field_from_vec_i32_expr(f, quote! { row.get::<Vec<i32>>(stringify!(#field_name))? })
} else {
quote! {
#field_name: row.get(stringify!(#field_name))?
}
}
});
let from_row_values_fields = fields.iter().enumerate().map(|(i, f)| {
let field_name = f.ident.as_ref().unwrap();
if has_i32_data_type(f) {
field_from_i32_expr(
f,
quote! {
<i32 as ::ormer::FromRowValues>::from_row_values(&values[#i..#i+1])?
},
quote! {
<Option<i32> as ::ormer::FromRowValues>::from_row_values(
&values[#i..#i+1]
)?
},
)
} else if has_vec_i32_data_type(f) {
field_from_vec_i32_expr(
f,
quote! {
<Vec<i32> as ::ormer::FromRowValues>::from_row_values(
&values[#i..#i+1]
)?
},
)
} else {
let field_type = &f.ty;
quote! {
#field_name: <#field_type as ::ormer::FromRowValues>::from_row_values(
&values[#i..#i+1]
)?
}
}
});
let field_names_for_values = fields.iter().map(field_to_value_expr);
let where_fields = fields.iter().map(|f| {
let field_name = f.ident.as_ref().unwrap();
let field_type = &f.ty;
quote! {
pub #field_name: ::ormer::query::builder::TypedColumn<#field_type>
}
});
let where_default_fields = fields.iter().map(|f| {
let field_name = f.ident.as_ref().unwrap();
quote! {
#field_name: ::ormer::query::builder::TypedColumn::new(stringify!(#field_name))
}
});
quote! {
pub struct #where_name {
#(#where_fields),*
}
impl Default for #where_name {
fn default() -> Self {
Self {
#(#where_default_fields),*
}
}
}
impl ::ormer::Model for #name {
const TABLE_NAME: &'static str = #table_name;
const COLUMNS: &'static [&'static str] = &[#(#field_names_lit),*];
const COLUMN_SCHEMA: &'static [::ormer::model::ColumnSchema] = &[#(#column_schema_entries),*];
type AutoIncrementKeyType = #auto_increment_key_type;
type QueryBuilder = ::ormer::Select<Self>;
type Where = #where_name;
fn query() -> Self::QueryBuilder {
::ormer::Select::new()
}
fn select() -> Self::QueryBuilder {
::ormer::Select::new()
}
fn from_row(row: &::ormer::Row) -> anyhow::Result<Self> {
Ok(Self {
#(#from_row_fields),*
})
}
fn from_row_values(values: &[::ormer::Value]) -> anyhow::Result<Self> {
if values.len() < Self::COLUMNS.len() {
return Err(anyhow::anyhow!(
"Expected {} values for {}", Self::COLUMNS.len(), stringify!(#name)
));
}
Ok(Self {
#(#from_row_values_fields),*
})
}
fn field_values(&self) -> Vec<::ormer::Value> {
vec![
#(#field_names_for_values),*
]
}
fn primary_key_columns() -> &'static [&'static str] {
&[#(#primary_key_field_names),*]
}
fn primary_key_values(&self) -> Vec<::ormer::Value> {
vec![#(#primary_key_values),*]
}
fn primary_key_column() -> &'static str {
stringify!(#primary_key_field)
}
fn primary_key_value(&self) -> ::ormer::Value {
#primary_key_value_expr
}
}
impl #name {
pub fn select() -> ::ormer::Select<Self> {
::ormer::Select::new()
}
pub fn query() -> ::ormer::Select<Self> {
::ormer::Select::new()
}
}
}
}
fn normalize_type_string(type_str: String) -> String {
type_str
.replace(" :: ", "::")
.replace(" < ", "<")
.replace(" >", ">")
.replace(" , ", ",")
}
fn derive_model_tuple_wrapper(
input: &DeriveInput,
name: &syn::Ident,
_where_name: &syn::Ident,
table_name: String,
) -> TokenStream {
let inner_type = match &input.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Unnamed(fields) => {
if fields.unnamed.len() != 1 {
panic!("Tuple struct wrapper must have exactly one field");
}
&fields.unnamed[0].ty
}
_ => panic!("Expected unnamed fields"),
},
_ => panic!("Expected struct"),
};
quote! {
impl ::ormer::Model for #name {
const TABLE_NAME: &'static str = #table_name;
const COLUMNS: &'static [&'static str] = <#inner_type as ::ormer::Model>::COLUMNS;
const COLUMN_SCHEMA: &'static [::ormer::model::ColumnSchema] = <#inner_type as ::ormer::Model>::COLUMN_SCHEMA;
type AutoIncrementKeyType = <#inner_type as ::ormer::Model>::AutoIncrementKeyType;
type QueryBuilder = ::ormer::Select<Self>;
type Where = <#inner_type as ::ormer::Model>::Where;
fn query() -> Self::QueryBuilder {
::ormer::Select::new()
}
fn select() -> Self::QueryBuilder {
::ormer::Select::new()
}
fn from_row(row: &::ormer::Row) -> anyhow::Result<Self> {
let inner = <#inner_type as ::ormer::Model>::from_row(row)?;
Ok(#name(inner))
}
fn from_row_values(values: &[::ormer::Value]) -> anyhow::Result<Self> {
let inner = <#inner_type as ::ormer::Model>::from_row_values(values)?;
Ok(#name(inner))
}
fn field_values(&self) -> Vec<::ormer::Value> {
self.0.field_values()
}
fn primary_key_columns() -> &'static [&'static str] {
<#inner_type as ::ormer::Model>::primary_key_columns()
}
fn primary_key_values(&self) -> Vec<::ormer::Value> {
self.0.primary_key_values()
}
fn primary_key_column() -> &'static str {
<#inner_type as ::ormer::Model>::primary_key_column()
}
fn primary_key_value(&self) -> ::ormer::Value {
self.0.primary_key_value()
}
}
impl #name {
pub fn select() -> ::ormer::Select<Self> {
::ormer::Select::new()
}
pub fn query() -> ::ormer::Select<Self> {
::ormer::Select::new()
}
}
impl From<#inner_type> for #name {
fn from(inner: #inner_type) -> Self {
#name(inner)
}
}
impl #name {
pub fn into_inner(self) -> #inner_type {
self.0
}
pub fn inner(&self) -> &#inner_type {
&self.0
}
}
}
}
fn extract_table_name(input: &DeriveInput) -> String {
for attr in &input.attrs {
if attr.path().is_ident("table") {
if let Meta::NameValue(meta) = &attr.meta {
if let syn::Expr::Lit(expr) = &meta.value {
if let Lit::Str(lit) = &expr.lit {
return lit.value();
}
}
}
}
}
to_snake_case(&input.ident.to_string())
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
} else {
result.push(c);
}
}
result
}
fn extract_unique_group(field: &syn::Field) -> proc_macro2::TokenStream {
for attr in &field.attrs {
if attr.path().is_ident("unique") {
if let Meta::List(list) = &attr.meta {
let tokens_str = list.tokens.to_string();
if tokens_str.contains("group") {
if let Ok(Meta::NameValue(meta)) = syn::parse2(list.tokens.clone()) {
if let Expr::Lit(ExprLit {
lit: Lit::Int(lit_int),
..
}) = &meta.value
{
let group_value: i32 = lit_int.base10_parse().unwrap_or(0);
return quote! { Some(#group_value) };
}
}
}
}
return quote! { Some(0) };
}
}
quote! { None }
}
fn extract_data_type(field: &syn::Field) -> proc_macro2::TokenStream {
for attr in &field.attrs {
if attr.path().is_ident("data_type") {
if let Meta::List(list) = &attr.meta {
let tokens_str = list.tokens.to_string().replace('"', "");
return quote! { Some(#tokens_str) };
}
}
}
quote! { None }
}
fn has_data_type(field: &syn::Field) -> bool {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("data_type"))
}
fn has_i32_data_type(field: &syn::Field) -> bool {
extract_data_type_type(field)
.as_ref()
.map(is_i32_type)
.unwrap_or(false)
}
fn has_vec_i32_data_type(field: &syn::Field) -> bool {
extract_data_type_type(field)
.as_ref()
.map(is_vec_i32_type)
.unwrap_or(false)
}
fn extract_data_type_type(field: &syn::Field) -> Option<syn::Type> {
for attr in &field.attrs {
if attr.path().is_ident("data_type") {
if let Meta::List(list) = &attr.meta {
if let Ok(data_type) = syn::parse2::<syn::Type>(list.tokens.clone()) {
return Some(data_type);
}
}
}
}
None
}
fn is_i32_type(ty: &syn::Type) -> bool {
match ty {
syn::Type::Path(type_path) if type_path.qself.is_none() => type_path
.path
.segments
.last()
.map(|segment| segment.ident == "i32")
.unwrap_or(false),
_ => false,
}
}
fn is_vec_i32_type(ty: &syn::Type) -> bool {
vec_inner_type(ty).map(is_i32_type).unwrap_or(false)
}
fn option_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
match ty {
syn::Type::Path(type_path) if type_path.qself.is_none() => {
let segment = type_path.path.segments.last()?;
if segment.ident != "Option" {
return None;
}
match &segment.arguments {
syn::PathArguments::AngleBracketed(args) => args.args.first().and_then(|arg| {
if let syn::GenericArgument::Type(inner) = arg {
Some(inner)
} else {
None
}
}),
_ => None,
}
}
_ => None,
}
}
fn vec_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
match ty {
syn::Type::Path(type_path) if type_path.qself.is_none() => {
let segment = type_path.path.segments.last()?;
if segment.ident != "Vec" {
return None;
}
match &segment.arguments {
syn::PathArguments::AngleBracketed(args) => args.args.first().and_then(|arg| {
if let syn::GenericArgument::Type(inner) = arg {
Some(inner)
} else {
None
}
}),
_ => None,
}
}
_ => None,
}
}
fn field_to_value_expr(field: &syn::Field) -> proc_macro2::TokenStream {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;
if has_i32_data_type(field) {
if option_inner_type(field_type).is_some() {
quote! {
match self.#field_name.clone() {
Some(value) => ::ormer::Value::from(value as i32),
None => ::ormer::Value::Null,
}
}
} else {
quote! {
::ormer::Value::from(self.#field_name.clone() as i32)
}
}
} else if has_vec_i32_data_type(field) {
let Some(_) = vec_inner_type(field_type) else {
panic!("#[data_type(Vec<i32>)] requires a Vec<T> field");
};
quote! {
::ormer::Value::from(
self.#field_name
.clone()
.into_iter()
.map(|value| value as i32)
.collect::<Vec<i32>>()
)
}
} else {
quote! {
::ormer::Value::from(self.#field_name.clone())
}
}
}
fn field_from_i32_expr(
field: &syn::Field,
value_expr: proc_macro2::TokenStream,
optional_value_expr: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;
if let Some(inner_type) = option_inner_type(field_type) {
let decode_expr = data_type_i32_decode_expr(inner_type, field_name, quote! { value });
quote! {
#field_name: {
let value = #optional_value_expr;
match value {
Some(value) => Some(#decode_expr),
None => None,
}
}
}
} else {
let decode_expr = data_type_i32_decode_expr(field_type, field_name, value_expr);
quote! {
#field_name: #decode_expr
}
}
}
fn field_from_vec_i32_expr(
field: &syn::Field,
value_expr: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;
let inner_type =
vec_inner_type(field_type).expect("#[data_type(Vec<i32>)] requires a Vec<T> field");
quote! {
#field_name: {
let values = #value_expr;
use ::ormer::model::I32DataTypeDecode as _;
values
.into_iter()
.map(|value| {
::ormer::model::I32DataTypeDecoder::<#inner_type>::new()
.decode(value, stringify!(#field_name), stringify!(#inner_type))
})
.collect::<anyhow::Result<Vec<#inner_type>>>()?
}
}
}
fn data_type_i32_decode_expr(
target_type: &syn::Type,
field_name: &syn::Ident,
value_expr: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
quote! {
{
let value = #value_expr;
use ::ormer::model::I32DataTypeDecode as _;
::ormer::model::I32DataTypeDecoder::<#target_type>::new()
.decode(value, stringify!(#field_name), stringify!(#target_type))?
}
}
}
fn extract_hypertable(field: &syn::Field) -> proc_macro2::TokenStream {
for attr in &field.attrs {
if attr.path().is_ident("hypertable") {
if let Meta::List(list) = &attr.meta {
let tokens = &list.tokens;
return quote! { Some(#tokens) };
}
}
}
quote! { None }
}
fn extract_foreign_key(field: &syn::Field) -> proc_macro2::TokenStream {
for attr in &field.attrs {
if attr.path().is_ident("foreign") {
if let Meta::List(list) = &attr.meta {
let tokens_str = list.tokens.to_string();
let parts: Vec<&str> = tokens_str.split('.').collect();
if parts.len() == 2 {
let ref_type = parts[0].trim();
let ref_field = parts[1].trim();
let ref_type_ident = syn::Ident::new(ref_type, proc_macro2::Span::call_site());
return quote! {
Some(::ormer::model::ForeignKeyInfo {
ref_table: <#ref_type_ident as ::ormer::Model>::TABLE_NAME,
ref_column: #ref_field,
ref_column_fn: None,
})
};
} else if parts.len() == 1 {
let ref_type = parts[0].trim();
let ref_type_ident = syn::Ident::new(ref_type, proc_macro2::Span::call_site());
let pk_fn_name = syn::Ident::new(
&format!("__{}_primary_key_column", ref_type),
proc_macro2::Span::call_site(),
);
return quote! {
{
fn #pk_fn_name() -> &'static str {
<#ref_type_ident as ::ormer::Model>::primary_key_columns()[0]
}
Some(::ormer::model::ForeignKeyInfo {
ref_table: <#ref_type_ident as ::ormer::Model>::TABLE_NAME,
ref_column: "",
ref_column_fn: Some(#pk_fn_name),
})
}
};
}
}
}
}
quote! { None }
}