#![forbid(unsafe_code)]
#![warn(missing_docs)]
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{Ident, ItemStruct, LitStr, Token};
#[proc_macro_attribute]
pub fn controller(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as ItemStruct);
let struct_name = &input.ident;
let expanded = quote! {
#input
impl ::sz_rust_core::controller::SzController for #struct_name {}
};
expanded.into()
}
#[proc_macro_attribute]
pub fn model(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as ItemStruct);
let struct_name = &input.ident;
let args = match parse_model_attr(attr) {
Ok(args) => args,
Err(msg) => {
return syn::Error::new_spanned(&input, msg)
.to_compile_error()
.into();
}
};
let table_name = args.table;
let pk_name = args.pk.unwrap_or_else(|| "id".to_string());
let fields = match collect_fields(&input, &pk_name) {
Ok(fields) => fields,
Err(msg) => {
return syn::Error::new_spanned(&input, msg)
.to_compile_error()
.into();
}
};
let pk_field = fields
.iter()
.find(|f| f.column_name == pk_name)
.ok_or_else(|| {
format!(
"primary key field '{}' not found in struct '{}'",
pk_name, struct_name
)
});
let pk_field = match pk_field {
Ok(f) => f,
Err(msg) => {
return syn::Error::new_spanned(&input, msg)
.to_compile_error()
.into();
}
};
let pk_ty = pk_field.ty_token.clone();
let pk_ident = pk_field.ident.clone();
let column_names: Vec<&str> = fields.iter().map(|f| f.column_name.as_str()).collect();
let fillable_names: Vec<&str> = fields
.iter()
.filter(|f| f.column_name != pk_name)
.map(|f| f.column_name.as_str())
.collect();
let get_column_value_arms = fields.iter().map(|f| {
let col = &f.column_name;
let ident = &f.ident;
let ty = &f.ty;
if ty == "i64" {
quote! { #col => Some(::sz_orm_core::Value::I64(self.#ident)) }
} else if ty == "i32" {
quote! { #col => Some(::sz_orm_core::Value::I32(self.#ident)) }
} else if ty == "f64" {
quote! { #col => Some(::sz_orm_core::Value::F64(self.#ident)) }
} else if ty == "String" {
quote! { #col => Some(::sz_orm_core::Value::String(self.#ident.clone())) }
} else if ty == "bool" {
quote! { #col => Some(::sz_orm_core::Value::Bool(self.#ident)) }
} else {
quote! { #col => None }
}
});
let from_value_stmts = fields.iter().filter_map(|f| {
let col = &f.column_name;
let ident = &f.ident;
let ty = &f.ty;
if ty == "i64" {
Some(quote! {
if let Some(::sz_orm_core::Value::I64(v)) = map.get(#col) {
self.#ident = *v;
}
})
} else if ty == "i32" {
Some(quote! {
if let Some(::sz_orm_core::Value::I32(v)) = map.get(#col) {
self.#ident = *v;
}
})
} else if ty == "f64" {
Some(quote! {
if let Some(::sz_orm_core::Value::F64(v)) = map.get(#col) {
self.#ident = *v;
}
})
} else if ty == "String" {
Some(quote! {
if let Some(::sz_orm_core::Value::String(v)) = map.get(#col) {
self.#ident = v.clone();
}
})
} else if ty == "bool" {
Some(quote! {
if let Some(::sz_orm_core::Value::Bool(v)) = map.get(#col) {
self.#ident = *v;
}
})
} else {
None
}
});
let table_name_lit = LitStr::new(&table_name, proc_macro2::Span::call_site());
let pk_name_lit = LitStr::new(&pk_name, proc_macro2::Span::call_site());
let expanded = quote! {
#input
impl ::sz_orm_core::Model for #struct_name {
type PrimaryKey = #pk_ty;
fn table_name() -> &'static str {
#table_name_lit
}
fn pk_name() -> &'static str {
#pk_name_lit
}
fn pk(&self) -> Self::PrimaryKey {
self.#pk_ident.clone()
}
fn set_pk(&mut self, pk: Self::PrimaryKey) {
self.#pk_ident = pk;
}
}
impl ::sz_orm_core::ModelExt for #struct_name {
fn columns() -> Vec<&'static str> {
vec![#(#column_names),*]
}
fn fillable() -> Vec<&'static str> {
vec![#(#fillable_names),*]
}
fn guarded() -> Vec<&'static str> {
vec![#pk_name_lit]
}
fn get_column_value(&self, column: &str) -> Option<::sz_orm_core::Value> {
match column {
#(#get_column_value_arms,)*
_ => None,
}
}
fn from_value(&mut self, map: std::collections::HashMap<String, ::sz_orm_core::Value>) {
#(#from_value_stmts)*
}
}
};
expanded.into()
}
struct ModelAttr {
table: String,
pk: Option<String>,
}
fn parse_model_attr(attr: TokenStream) -> Result<ModelAttr, String> {
if attr.is_empty() {
return Err("missing required 'table' attribute: #[model(table = \"xxx\")]".to_string());
}
let attr2: proc_macro2::TokenStream = attr.into();
let meta_list = Punctuated::<MetaNameValueStr, Token![,]>::parse_terminated
.parse2(attr2)
.map_err(|e| format!("failed to parse model attributes: {e}"))?;
let mut table = None;
let mut pk = None;
for nv in meta_list {
let key = nv.key.to_string();
let value = nv.value;
match key.as_str() {
"table" => table = Some(value),
"pk" => pk = Some(value),
_ => return Err(format!("unknown model attribute '{}'", key)),
}
}
let table = table.ok_or_else(|| {
"missing required 'table' attribute: #[model(table = \"xxx\")]".to_string()
})?;
Ok(ModelAttr { table, pk })
}
struct MetaNameValueStr {
key: Ident,
value: String,
}
impl syn::parse::Parse for MetaNameValueStr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let key: Ident = input.parse()?;
let _: Token![=] = input.parse()?;
let value: LitStr = input.parse()?;
Ok(Self {
key,
value: value.value(),
})
}
}
struct FieldInfo {
ident: Ident,
ty: String,
ty_token: syn::Type,
column_name: String,
}
fn collect_fields(input: &ItemStruct, _pk_name: &str) -> Result<Vec<FieldInfo>, String> {
let fields = match &input.fields {
syn::Fields::Named(named) => &named.named,
_ => {
return Err("#[model] only supports structs with named fields".to_string());
}
};
let mut result = Vec::new();
for field in fields {
let ident = field
.ident
.clone()
.ok_or_else(|| "#[model] requires all fields to be named".to_string())?;
if field.attrs.iter().any(|attr| {
attr.path().is_ident("model")
&& attr
.parse_args::<syn::Ident>()
.ok()
.map(|i| i == "skip")
.unwrap_or(false)
}) {
continue;
}
let ty_str = extract_type_string(&field.ty);
let ty_token = field.ty.clone();
let column_name = ident.to_string();
result.push(FieldInfo {
ident,
ty: ty_str,
ty_token,
column_name,
});
}
if result.is_empty() {
return Err("#[model] struct must have at least one field".to_string());
}
Ok(result)
}
fn extract_type_string(ty: &syn::Type) -> String {
let s = quote!(#ty).to_string();
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[proc_macro]
pub fn compact(input: TokenStream) -> TokenStream {
let names =
syn::parse_macro_input!(input with Punctuated::<Ident, Token![,]>::parse_terminated);
let inserts = names.iter().map(|name| {
let name_str = name.to_string();
quote! {
map.insert(
#name_str.to_string(),
serde_json::to_value(&#name).unwrap_or(serde_json::Value::Null),
);
}
});
quote! {
{
let mut map: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
#(#inserts)*
map
}
}
.into()
}