use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
PathArguments, Type, TypePath,
};
fn rustango_root() -> TokenStream2 {
use proc_macro_crate::{crate_name, FoundCrate};
match crate_name("rustango") {
Ok(FoundCrate::Itself) => quote!(::rustango),
Ok(FoundCrate::Name(name)) => {
let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
quote!(::#ident)
}
Err(_) => quote!(::rustango),
}
}
#[proc_macro_derive(Model, attributes(rustango))]
pub fn derive_model(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(ViewSet, attributes(viewset))]
pub fn derive_viewset(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_viewset(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(Form, attributes(form))]
pub fn derive_form(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_form(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(Serializer, attributes(serializer))]
pub fn derive_serializer(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_serializer(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro]
pub fn embed_migrations(input: TokenStream) -> TokenStream {
expand_embed_migrations(input.into())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[allow(non_snake_case)]
#[proc_macro]
pub fn Q(input: TokenStream) -> TokenStream {
expand_q(input.into())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_attribute]
pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
expand_main(args.into(), item.into())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
let mut input: syn::ItemFn = syn::parse2(item)?;
if input.sig.asyncness.is_none() {
return Err(syn::Error::new(
input.sig.ident.span(),
"`#[rustango::main]` must wrap an `async fn`",
));
}
let root = rustango_root();
let flavor = parse_flavor(&args);
let builder_call = match flavor {
Flavor::CurrentThread => quote! {
#root::__private_runtime::tokio::runtime::Builder::new_current_thread()
},
Flavor::MultiThread => quote! {
#root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
},
};
let user_body = input.block.clone();
input.sig.asyncness = None;
input.block = syn::parse2(quote! {{
{
use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
let _ = tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
)
.try_init();
}
let __rt = #builder_call
.enable_all()
.build()
.expect("failed to build tokio runtime");
__rt.block_on(async move #user_body)
}})?;
Ok(quote! {
#input
})
}
enum Flavor {
MultiThread,
CurrentThread,
}
fn parse_flavor(args: &TokenStream2) -> Flavor {
let s = args.to_string();
if s.contains("current_thread") {
Flavor::CurrentThread
} else {
Flavor::MultiThread
}
}
struct QInput {
base_path: syn::Path,
field: syn::Ident,
value: syn::Expr,
}
impl syn::parse::Parse for QInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let base_path: syn::Path = input.parse()?;
input.parse::<syn::Token![.]>()?;
let field: syn::Ident = input.parse()?;
input.parse::<syn::Token![=]>()?;
let value: syn::Expr = input.parse()?;
Ok(QInput {
base_path,
field,
value,
})
}
}
fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
let q: QInput = syn::parse2(input)?;
let root = rustango_root();
let field_str = q.field.to_string();
let field_span = q.field.span();
let (base, suffix) = match field_str.find("__") {
Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
None => (field_str.as_str(), ""),
};
if base.is_empty() {
return Err(syn::Error::new(
field_span,
"Q!(): field name is empty before `__` suffix",
));
}
let base_ident = syn::Ident::new(base, field_span);
let value = &q.value;
let path = &q.base_path;
let expanded = match suffix {
"" | "exact" => quote! {
#root::core::Column::eq(#path::#base_ident, #value)
},
"ne" => quote! {
#root::core::Column::ne(#path::#base_ident, #value)
},
"gt" => quote! {
#root::core::Column::gt(#path::#base_ident, #value)
},
"gte" => quote! {
#root::core::Column::gte(#path::#base_ident, #value)
},
"lt" => quote! {
#root::core::Column::lt(#path::#base_ident, #value)
},
"lte" => quote! {
#root::core::Column::lte(#path::#base_ident, #value)
},
"iexact" => quote! {
#root::core::Column::ilike(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
},
"contains" => quote! {
#root::core::Column::like(
#path::#base_ident,
::std::format!("%{}%", #value),
)
},
"icontains" => quote! {
#root::core::Column::ilike(
#path::#base_ident,
::std::format!("%{}%", #value),
)
},
"startswith" => quote! {
#root::core::Column::like(
#path::#base_ident,
::std::format!("{}%", #value),
)
},
"istartswith" => quote! {
#root::core::Column::ilike(
#path::#base_ident,
::std::format!("{}%", #value),
)
},
"endswith" => quote! {
#root::core::Column::like(
#path::#base_ident,
::std::format!("%{}", #value),
)
},
"iendswith" => quote! {
#root::core::Column::ilike(
#path::#base_ident,
::std::format!("%{}", #value),
)
},
"in" => quote! {
#root::core::Column::is_in(#path::#base_ident, #value)
},
"not_in" => quote! {
#root::core::Column::not_in(#path::#base_ident, #value)
},
"isnull" => {
let b = match value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Bool(b),
..
}) => b.value(),
_ => {
return Err(syn::Error::new_spanned(
value,
"Q!(): `__isnull` requires a `true` or `false` literal",
));
}
};
if b {
quote! { #root::core::Column::is_null(#path::#base_ident) }
} else {
quote! { #root::core::Column::is_not_null(#path::#base_ident) }
}
}
"between" => {
let tuple = match value {
syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
_ => {
return Err(syn::Error::new_spanned(
value,
"Q!(): `__between` requires a tuple literal `(lo, hi)`",
));
}
};
let lo = &tuple.elems[0];
let hi = &tuple.elems[1];
quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
}
"regex" => quote! {
#root::core::Column::regex(#path::#base_ident, #value)
},
"iregex" => quote! {
#root::core::Column::iregex(#path::#base_ident, #value)
},
_ => {
return Err(syn::Error::new(
field_span,
format!(
"Q!(): unknown lookup suffix `__{}`. Supported: __exact / __iexact / __ne / __gt / __gte / __lt / __lte / __contains / __icontains / __startswith / __istartswith / __endswith / __iendswith / __in / __not_in / __isnull / __between / __regex / __iregex",
suffix
),
));
}
};
Ok(expanded)
}
fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
let path_str = if input.is_empty() {
"./migrations".to_string()
} else {
let lit: LitStr = syn::parse2(input)?;
lit.value()
};
let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
syn::Error::new(
proc_macro2::Span::call_site(),
"embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
)
})?;
let abs = std::path::Path::new(&manifest).join(&path_str);
let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
if abs.is_dir() {
let read = std::fs::read_dir(&abs).map_err(|e| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("embed_migrations!: cannot read {}: {e}", abs.display()),
)
})?;
for entry in read.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
if path.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
entries.push((stem.to_owned(), path));
}
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
for (stem, path) in &entries {
let raw = std::fs::read_to_string(path).map_err(|e| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"embed_migrations!: cannot read {} for chain validation: {e}",
path.display()
),
)
})?;
let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"embed_migrations!: {} is not valid JSON: {e}",
path.display()
),
)
})?;
let name = json
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"embed_migrations!: {} is missing the `name` field",
path.display()
),
)
})?
.to_owned();
if name != *stem {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"embed_migrations!: file stem `{stem}` does not match the migration's \
`name` field `{name}` — rename the file or fix the JSON",
),
));
}
let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
chain_names.push(name.clone());
prev_refs.push((name, prev));
}
let name_set: std::collections::HashSet<&str> =
chain_names.iter().map(String::as_str).collect();
for (name, prev) in &prev_refs {
if let Some(p) = prev {
if !name_set.contains(p.as_str()) {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"embed_migrations!: broken migration chain — `{name}` declares \
prev=`{p}` but no migration with that name exists in {}",
abs.display()
),
));
}
}
}
let pairs: Vec<TokenStream2> = entries
.iter()
.map(|(name, path)| {
let path_lit = path.display().to_string();
quote! { (#name, ::core::include_str!(#path_lit)) }
})
.collect();
Ok(quote! {
{
const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
__RUSTANGO_EMBEDDED
}
})
}
fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
let root = rustango_root();
let struct_name = &input.ident;
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new_spanned(
struct_name,
"Model can only be derived on structs",
));
};
let Fields::Named(named) = &data.fields else {
return Err(syn::Error::new_spanned(
struct_name,
"Model requires a struct with named fields",
));
};
let container = parse_container_attrs(input)?;
let table = container
.table
.unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
let model_name = struct_name.to_string();
let collected = collect_fields(named, &table)?;
if let Some((ref display, span)) = container.display {
if !collected.field_names.iter().any(|n| n == display) {
return Err(syn::Error::new(
span,
format!("`display = \"{display}\"` does not match any field on this struct"),
));
}
}
let display = container.display.map(|(name, _)| name);
let app_label = container.app.clone();
if let Some(admin) = &container.admin {
for (label, list) in [
("search_fields", &admin.search_fields),
("readonly_fields", &admin.readonly_fields),
("list_filter", &admin.list_filter),
] {
if let Some((names, span)) = list {
for name in names {
if !collected.field_names.iter().any(|n| n == name) {
return Err(syn::Error::new(
*span,
format!(
"`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
),
));
}
}
}
}
if let Some((pairs, span)) = &admin.ordering {
for (name, _) in pairs {
if !collected.field_names.iter().any(|n| n == name) {
return Err(syn::Error::new(
*span,
format!(
"`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
),
));
}
}
}
if let Some((groups, span)) = &admin.fieldsets {
for (_, fields) in groups {
for name in fields {
if !collected.field_names.iter().any(|n| n == name) {
return Err(syn::Error::new(
*span,
format!(
"`fieldsets`: \"{name}\" is not a declared field on this struct"
),
));
}
}
}
}
}
if let Some(audit) = &container.audit {
if let Some((names, span)) = &audit.track {
for name in names {
if !collected.field_names.iter().any(|n| n == name) {
return Err(syn::Error::new(
*span,
format!(
"`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
),
));
}
}
}
}
for (col, _desc, span) in &container.default_order {
if !collected.field_names.iter().any(|n| n == col) {
return Err(syn::Error::new(
*span,
format!(
"`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
),
));
}
}
let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
audit
.track
.as_ref()
.map(|(names, _)| names.clone())
.unwrap_or_default()
});
let mut all_indexes: Vec<IndexAttr> = container.indexes;
for field in &named.named {
let ident = field.ident.as_ref().expect("named");
let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
if fa.index {
let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
let auto_name = if fa.index_unique {
format!("{table}_{col_name}_uq_idx")
} else {
format!("{table}_{col_name}_idx")
};
all_indexes.push(IndexAttr {
name: fa.index_name.or(Some(auto_name)),
columns: vec![col_name],
unique: fa.index_unique,
method: fa.index_method,
where_clause: None,
include: Vec::new(),
});
}
}
}
let model_impl = model_impl_tokens(
struct_name,
&model_name,
&table,
display.as_deref(),
app_label.as_deref(),
container.admin.as_ref(),
&container.default_order,
&collected.field_schemas,
collected.soft_delete_column.as_deref(),
container.permissions,
audit_track_names.as_deref(),
&container.m2m,
&all_indexes,
&container.checks,
&container.excludes,
&container.composite_fks,
&container.generic_fks,
container.scope.as_deref(),
container.is_view,
container.verbose_name.as_deref(),
container.verbose_name_plural.as_deref(),
container.managed,
container.base_manager_name.as_deref(),
container.order_with_respect_to.as_deref(),
container.proxy,
&container.required_db_features,
container.required_db_vendor.as_deref(),
container.default_related_name.as_deref(),
container.db_table_comment.as_deref(),
container
.get_latest_by
.as_ref()
.map(|(c, d)| (c.as_str(), *d)),
&container.extra_permissions,
&container.default_permissions,
&container.global_scopes,
&container.reverse_has_relations,
&container.generic_has_relations,
);
let module_ident = column_module_ident(struct_name);
let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
let track_set: Option<std::collections::HashSet<&str>> = audit
.track
.as_ref()
.map(|(names, _)| names.iter().map(String::as_str).collect());
collected
.column_entries
.iter()
.filter(|c| {
track_set
.as_ref()
.map_or(true, |s| s.contains(c.name.as_str()))
})
.collect()
});
let inherent_impl = inherent_impl_tokens(
struct_name,
&collected,
collected.primary_key.as_ref(),
&column_consts,
audited_fields.as_deref(),
&all_indexes,
&container.manager_fns,
);
let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
let reverse_helpers = reverse_helper_tokens(
struct_name,
&collected.fk_relations,
container.default_related_name.as_deref(),
);
let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
let reverse_has_accessors =
reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
let generic_fk_accessors = generic_fk_accessor_tokens(
struct_name,
&container.generic_fks,
&collected.column_entries,
);
let manager_trait = container.manager_ext.as_ref().map(|name| {
let model_name_str = struct_name.to_string();
let doc = format!(
"Custom-Manager extension trait for [`{model_name_str}`]. \
Generated by `#[rustango(manager(ext = ...))]`. Add methods \
via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
);
quote! {
#[doc = #doc]
pub trait #name: ::core::marker::Sized {}
}
});
Ok(quote! {
#model_impl
#inherent_impl
#from_row_impl
#column_module
#reverse_helpers
#m2m_accessors
#generic_m2m_accessors
#through_accessors
#reverse_has_accessors
#generic_fk_accessors
#manager_trait
#root::core::inventory::submit! {
#root::core::ModelEntry {
schema: <#struct_name as #root::core::Model>::SCHEMA,
module_path: ::core::module_path!(),
}
}
})
}
fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
let root = rustango_root();
let arms = fk_relations.iter().map(|rel| {
let parent_ty = &rel.parent_type;
let fk_col = rel.fk_column.as_str();
let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
let assign = if rel.nullable {
quote! {
self.#field_ident = ::core::option::Option::Some(
#root::sql::ForeignKey::loaded(_pk, _parent),
);
}
} else {
quote! {
self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
}
};
quote! {
#fk_col => {
let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
if let ::core::option::Option::Some(__r) = __rest {
let _ = #root::sql::LoadRelated::__rustango_load_related(
&mut _parent, row, __r, &__next_alias,
)?;
}
let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
#root::core::SqlValue::#variant_ident(v) => v,
_other => {
::core::debug_assert!(
false,
"rustango macro bug: load_related on FK `{}` expected \
SqlValue::{} from parent's __rustango_pk_value but got \
{:?} — file a bug at https://github.com/ujeenet/rustango",
#fk_col,
::core::stringify!(#variant_ident),
_other,
);
#default_expr
}
};
#assign
::core::result::Result::Ok(true)
}
}
});
quote! {
#[cfg(feature = "postgres")]
impl #root::sql::LoadRelated for #struct_name {
#[allow(unused_variables)]
fn __rustango_load_related(
&mut self,
row: &#root::sql::sqlx::postgres::PgRow,
field_name: &str,
alias: &str,
) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
let (__base, __rest): (&str, ::core::option::Option<&str>) =
match field_name.split_once("__") {
::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
::core::option::Option::None => (field_name, ::core::option::Option::None),
};
let __next_alias: ::std::string::String = match __rest {
::core::option::Option::Some(__r) => {
let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
::std::format!("{}__{}", alias, __rb)
}
::core::option::Option::None => ::std::string::String::new(),
};
match __base {
#( #arms )*
_ => ::core::result::Result::Ok(false),
}
}
}
}
}
fn load_related_impl_my_tokens(
struct_name: &syn::Ident,
fk_relations: &[FkRelation],
) -> TokenStream2 {
let root = rustango_root();
let arms = fk_relations.iter().map(|rel| {
let parent_ty = &rel.parent_type;
let fk_col = rel.fk_column.as_str();
let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
let assign = if rel.nullable {
quote! {
__self.#field_ident = ::core::option::Option::Some(
#root::sql::ForeignKey::loaded(_pk, _parent),
);
}
} else {
quote! {
__self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
}
};
quote! {
#fk_col => {
let mut _parent: #parent_ty =
<#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
if let ::core::option::Option::Some(__r) = __rest {
let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
&mut _parent, row, __r, &__next_alias,
)?;
}
let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
#root::core::SqlValue::#variant_ident(v) => v,
_other => {
::core::debug_assert!(
false,
"rustango macro bug: load_related on FK `{}` expected \
SqlValue::{} from parent's __rustango_pk_value but got \
{:?} — file a bug at https://github.com/ujeenet/rustango",
#fk_col,
::core::stringify!(#variant_ident),
_other,
);
#default_expr
}
};
#assign
::core::result::Result::Ok(true)
}
}
});
quote! {
#root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
#( #arms )*
});
}
}
fn load_related_impl_sqlite_tokens(
struct_name: &syn::Ident,
fk_relations: &[FkRelation],
) -> TokenStream2 {
let root = rustango_root();
let arms = fk_relations.iter().map(|rel| {
let parent_ty = &rel.parent_type;
let fk_col = rel.fk_column.as_str();
let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
let assign = if rel.nullable {
quote! {
__self.#field_ident = ::core::option::Option::Some(
#root::sql::ForeignKey::loaded(_pk, _parent),
);
}
} else {
quote! {
__self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
}
};
quote! {
#fk_col => {
let mut _parent: #parent_ty =
<#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
if let ::core::option::Option::Some(__r) = __rest {
let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
&mut _parent, row, __r, &__next_alias,
)?;
}
let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
#root::core::SqlValue::#variant_ident(v) => v,
_other => {
::core::debug_assert!(
false,
"rustango macro bug: load_related on FK `{}` expected \
SqlValue::{} from parent's __rustango_pk_value but got \
{:?} — file a bug at https://github.com/ujeenet/rustango",
#fk_col,
::core::stringify!(#variant_ident),
_other,
);
#default_expr
}
};
#assign
::core::result::Result::Ok(true)
}
}
});
quote! {
#root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
#( #arms )*
});
}
}
fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
let root = rustango_root();
let arms = fk_relations.iter().map(|rel| {
let fk_col = rel.fk_column.as_str();
let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
if rel.pk_kind == DetectedKind::I64 {
if rel.nullable {
quote! {
#fk_col => self.#field_ident
.as_ref()
.map(|fk| #root::sql::ForeignKey::pk(fk)),
}
} else {
quote! {
#fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
}
}
} else {
quote! {
#fk_col => ::core::option::Option::None,
}
}
});
let value_arms = fk_relations.iter().map(|rel| {
let fk_col = rel.fk_column.as_str();
let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
if rel.nullable {
quote! {
#fk_col => self.#field_ident
.as_ref()
.map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
#root::sql::ForeignKey::pk(fk)
)),
}
} else {
quote! {
#fk_col => ::core::option::Option::Some(
::core::convert::Into::<#root::core::SqlValue>::into(
self.#field_ident.pk()
)
),
}
}
});
quote! {
impl #root::sql::FkPkAccess for #struct_name {
#[allow(unused_variables)]
fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
match field_name {
#( #arms )*
_ => ::core::option::Option::None,
}
}
#[allow(unused_variables)]
fn __rustango_fk_pk_value(
&self,
field_name: &str,
) -> ::core::option::Option<#root::core::SqlValue> {
match field_name {
#( #value_arms )*
_ => ::core::option::Option::None,
}
}
}
}
}
fn reverse_helper_tokens(
child_ident: &syn::Ident,
fk_relations: &[FkRelation],
default_related_name: Option<&str>,
) -> TokenStream2 {
let root = rustango_root();
if fk_relations.is_empty() {
return TokenStream2::new();
}
let default_pg_suffix = default_related_name
.map(str::to_owned)
.unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
let impls = fk_relations.iter().map(|rel| {
let pg_suffix = rel
.related_name
.clone()
.unwrap_or_else(|| default_pg_suffix.clone());
let pool_suffix = format!("{}_pool", pg_suffix);
let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
let parent_ty = &rel.parent_type;
let fk_col = rel.fk_column.as_str();
let doc = format!(
"Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
further `{child_ident}::objects()` filters via direct queryset use."
);
let pool_doc = format!(
"Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
[`#root::sql::Pool`] and dispatches per backend so the \
reverse-FK fetch works on PG / MySQL / SQLite under one method. \
Use this from framework code that holds a `&Pool` (admin, \
tenancy resolver, viewset handlers); reach for the executor- \
bound variant when you already have a typed `sqlx::Executor`."
);
quote! {
#[cfg(feature = "postgres")]
impl #parent_ty {
#[doc = #doc]
pub async fn #pg_method_ident<'_c, _E>(
&self,
_executor: _E,
) -> ::core::result::Result<
::std::vec::Vec<#child_ident>,
#root::sql::ExecError,
>
where
_E: #root::sql::sqlx::Executor<
'_c,
Database = #root::sql::sqlx::Postgres,
>,
{
let _pk: #root::core::SqlValue = self.__rustango_pk_value();
#root::query::QuerySet::<#child_ident>::new()
.filter_op(#fk_col, #root::core::Op::Eq, _pk)
.fetch_on(_executor)
.await
}
}
impl #parent_ty {
#[doc = #pool_doc]
pub async fn #pool_method_ident(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<#child_ident>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _pk: #root::core::SqlValue = self.__rustango_pk_value();
#root::query::QuerySet::<#child_ident>::new()
.filter_op(#fk_col, #root::core::Op::Eq, _pk)
.fetch(pool)
.await
}
}
}
});
quote! { #( #impls )* }
}
fn generic_fk_accessor_tokens(
struct_name: &syn::Ident,
generic_fks: &[GenericFkAttr],
column_entries: &[ColumnEntry],
) -> TokenStream2 {
let root = rustango_root();
if generic_fks.is_empty() {
return TokenStream2::new();
}
let methods = generic_fks.iter().filter_map(|gfk| {
let ct_ident = column_entries
.iter()
.find(|c| c.column == gfk.ct_column)
.map(|c| c.ident.clone())?;
let pk_ident = column_entries
.iter()
.find(|c| c.column == gfk.pk_column)
.map(|c| c.ident.clone())?;
let accessor_ident =
syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
let setter_ident =
syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
let name_literal = gfk.name.as_str();
Some(quote! {
#[doc = concat!(
"Resolve the polymorphic `",
#name_literal,
"` relation. Reads `self.",
stringify!(#ct_ident),
"` + `self.",
stringify!(#pk_ident),
"`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
"Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
#name_literal,
"\", ...))]`."
)]
pub async fn #accessor_ident(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<#root::__serde_json::Value>,
#root::sql::ExecError,
> {
let gfk = #root::contenttypes::GenericForeignKey::new(
self.#ct_ident as i64,
self.#pk_ident as i64,
);
gfk.get_object(pool).await
}
#[doc = concat!(
"Set the polymorphic `",
#name_literal,
"` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
stringify!(#ct_ident),
"` and `self.",
stringify!(#pk_ident),
"`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
#name_literal,
"\", ...))]`."
)]
pub async fn #setter_ident<T: #root::core::Model>(
&mut self,
pool: &#root::sql::Pool,
target_pk: i64,
) -> ::core::result::Result<(), #root::sql::ExecError> {
let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
pool,
target_pk,
).await?;
self.#ct_ident = gfk.content_type_id as _;
self.#pk_ident = gfk.object_pk as _;
::core::result::Result::Ok(())
}
})
});
quote! {
impl #struct_name {
#( #methods )*
}
}
}
fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
let root = rustango_root();
if m2m_relations.is_empty() {
return TokenStream2::new();
}
let methods = m2m_relations.iter().map(|rel| {
let method_name = format!("{}_m2m", rel.name);
let method_ident = syn::Ident::new(&method_name, struct_name.span());
let through = rel.through.as_str();
let src_col = rel.src.as_str();
let dst_col = rel.dst.as_str();
quote! {
pub fn #method_ident(&self) -> #root::sql::M2MManager {
#root::sql::M2MManager {
src_pk: self.__rustango_pk_value(),
through: #through,
src_col: #src_col,
dst_col: #dst_col,
}
}
}
});
quote! {
impl #struct_name {
#( #methods )*
}
}
}
fn generic_m2m_accessor_tokens(
struct_name: &syn::Ident,
relations: &[GenericM2MAttr],
) -> TokenStream2 {
let root = rustango_root();
if relations.is_empty() {
return TokenStream2::new();
}
let methods = relations.iter().map(|rel| {
let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
let through = rel.through.as_str();
let pk_col = rel.pk_column.as_str();
let ct_col = rel.ct_column.as_str();
let related_col = rel.related_column.as_str();
quote! {
pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
#root::sql::GenericM2MManager {
src_pk: self.__rustango_pk_value(),
src_schema: <Self as #root::core::Model>::SCHEMA,
through: #through,
pk_col: #pk_col,
ct_col: #ct_col,
dst_col: #related_col,
}
}
}
});
quote! {
impl #struct_name {
#( #methods )*
}
}
}
fn reverse_has_accessor_tokens(
struct_name: &syn::Ident,
reverse_has_relations: &[ReverseHasAttr],
) -> TokenStream2 {
let root = rustango_root();
if reverse_has_relations.is_empty() {
return TokenStream2::new();
}
let methods = reverse_has_relations.iter().map(|rel| {
let exists_name = format!("{}_exists_expr", rel.name);
let not_exists_name = format!("{}_not_exists_expr", rel.name);
let count_name = format!("{}_count", rel.name);
let fetch_name = format!("{}_fetch", rel.name);
let first_name = format!("{}_first", rel.name);
let pluck_name = format!("{}_pluck", rel.name);
let accessor_name = rel.name.as_str();
let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
let count_ident = syn::Ident::new(&count_name, struct_name.span());
let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
let first_ident = syn::Ident::new(&first_name, struct_name.span());
let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
let child = &rel.child;
let child_fk_column = rel.child_fk_column.as_str();
let self_pk_column = rel.self_pk_column.as_str();
let exists_doc = format!(
"Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
filter to {struct_name}s with at least one matching child.",
);
let not_exists_doc = format!(
"Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
(SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
<outer>.{self_pk_column})`. Drop into \
`QuerySet::<{struct_name}>::where_raw(...)` to filter to \
{struct_name}s with **no** matching child.",
);
let count_doc = format!(
"Eloquent `$model->{name}->count()` analog — returns \
the number of `{child}` rows whose `{child_fk_column}` \
matches this `{struct_name}` instance's primary key. \
Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
{child_fk_column} = <self.pk>`.",
name = rel.name,
);
let accessor_doc = format!(
"Eloquent `$model->{name}` accessor — returns a \
`QuerySet<{child}>` filtered to rows whose \
`{child_fk_column}` matches this `{struct_name}` \
instance's primary key. **Chainable**: compose `.filter()` \
/ `.order_by()` / `.limit()` etc. on top, then call \
`.fetch(&pool)` (the QuerySet trait method) when \
done. For the simple \"fetch all\" hot path with no \
further composition, prefer the bare-name \
`{name}_fetch(&pool)` companion.",
name = rel.name,
);
let fetch_doc = format!(
"Eloquent `$model->{name}->get()` — bare-name hot-path \
over `{name}(&self).fetch(&pool)`. Use this when \
you don't need further `.filter()` / `.order_by()` \
composition; falls back to the chainable accessor when \
you do. Avoids the `_pool` suffix on the most common \
call-site shape.",
name = rel.name,
);
quote! {
#[doc = #accessor_doc]
pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
#root::query::QuerySet::<#child>::new()
.filter(#child_fk_column, self.__rustango_pk_value())
}
#[doc = #fetch_doc]
pub async fn #fetch_ident(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<#child>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
self.#accessor_ident().fetch(pool).await
}
pub async fn #first_ident(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<#child>,
#root::sql::ExecError,
> {
self.#accessor_ident().first(pool).await
}
pub async fn #pluck_ident<U>(
&self,
col: &'static str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<U>,
#root::sql::ExecError,
>
where
U: #root::sql::MaybePgScalar
+ #root::sql::MaybeMyScalar
+ #root::sql::MaybeSqliteScalar
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
self.#accessor_ident()
.values_list_flat(col)
.fetch::<U>(pool)
.await
}
#[doc = #exists_doc]
pub fn #exists_ident() -> #root::core::WhereExpr {
use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
let child_schema =
<#child as #root::core::Model>::SCHEMA;
let inner = SelectQuery {
where_clause: WhereExpr::ExprCompare {
lhs: Expr::Column(#child_fk_column),
op: Op::Eq,
rhs: Expr::OuterRef(#self_pk_column),
},
..SelectQuery::new(child_schema)
};
WhereExpr::Exists(::std::boxed::Box::new(inner))
}
#[doc = #not_exists_doc]
pub fn #not_exists_ident() -> #root::core::WhereExpr {
use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
let child_schema =
<#child as #root::core::Model>::SCHEMA;
let inner = SelectQuery {
where_clause: WhereExpr::ExprCompare {
lhs: Expr::Column(#child_fk_column),
op: Op::Eq,
rhs: Expr::OuterRef(#self_pk_column),
},
..SelectQuery::new(child_schema)
};
WhereExpr::NotExists(::std::boxed::Box::new(inner))
}
#[doc = #count_doc]
pub async fn #count_ident(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
i64,
#root::sql::ExecError,
> {
use #root::sql::CounterPool as _;
#root::query::QuerySet::<#child>::new()
.filter(#child_fk_column, self.__rustango_pk_value())
.count(pool)
.await
}
}
});
quote! {
impl #struct_name {
#( #methods )*
}
}
}
fn through_accessor_tokens(
struct_name: &syn::Ident,
through_relations: &[ThroughAttr],
) -> TokenStream2 {
let root = rustango_root();
if through_relations.is_empty() {
return TokenStream2::new();
}
let methods = through_relations.iter().map(|rel| {
let method_name = format!("{}_through", rel.name);
let count_name = format!("{}_through_count", rel.name);
let fetch_name = format!("{}_through_fetch", rel.name);
let first_name = format!("{}_through_first", rel.name);
let pluck_name = format!("{}_through_pluck", rel.name);
let method_ident = syn::Ident::new(&method_name, struct_name.span());
let count_ident = syn::Ident::new(&count_name, struct_name.span());
let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
let first_ident = syn::Ident::new(&first_name, struct_name.span());
let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
let far = &rel.far;
let intermediate = &rel.intermediate;
let far_fk_column = rel.far_fk_column.as_str();
let intermediate_fk_column = rel.intermediate_fk_column.as_str();
let intermediate_pk_column = rel.intermediate_pk_column.as_str();
let doc = format!(
"Eloquent `hasManyThrough` accessor — returns a \
`QuerySet<{far}>` whose rows reach this `{struct_name}` \
instance through the intermediate `{intermediate}` table. \
Generated SQL shape: \
`… WHERE {far_fk_column} IN (SELECT \
{intermediate_pk_column} FROM <{intermediate}> WHERE \
{intermediate_fk_column} = self.pk)`. Chainable like any \
other QuerySet — compose `.filter()` / `.order_by()` / \
`.limit()` etc. on top.",
);
let count_doc = format!(
"Eloquent `$model->{name}->count()` analog for the \
through-relation — returns the number of `{far}` rows \
reachable through `{intermediate}`. Equivalent to \
`self.{name}_through().count(pool)` but spelled \
as a bare instance method for parity with the \
`reverse_has` `<name>_count` shape.",
name = rel.name,
);
quote! {
#[doc = #doc]
pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
let intermediate_schema =
<#intermediate as #root::core::Model>::SCHEMA;
let sub = SelectQuery {
where_clause: WhereExpr::Predicate(Filter {
column: #intermediate_fk_column,
op: Op::Eq,
value: self.__rustango_pk_value(),
}),
projection: ::core::option::Option::Some(
::std::vec![#intermediate_pk_column],
),
..SelectQuery::new(intermediate_schema)
};
#root::query::QuerySet::<#far>::new().where_raw(
WhereExpr::InSubquery {
column: #far_fk_column,
negated: false,
subquery: ::std::boxed::Box::new(sub),
},
)
}
#[doc = #count_doc]
pub async fn #count_ident(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
i64,
#root::sql::ExecError,
> {
use #root::sql::CounterPool as _;
self.#method_ident().count(pool).await
}
pub async fn #fetch_ident(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<#far>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
self.#method_ident().fetch(pool).await
}
pub async fn #first_ident(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<#far>,
#root::sql::ExecError,
> {
self.#method_ident().first(pool).await
}
pub async fn #pluck_ident<U>(
&self,
col: &'static str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<U>,
#root::sql::ExecError,
>
where
U: #root::sql::MaybePgScalar
+ #root::sql::MaybeMyScalar
+ #root::sql::MaybeSqliteScalar
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
self.#method_ident()
.values_list_flat(col)
.fetch::<U>(pool)
.await
}
}
});
quote! {
impl #struct_name {
#( #methods )*
}
}
}
struct ColumnEntry {
ident: syn::Ident,
value_ty: Type,
name: String,
column: String,
field_type_tokens: TokenStream2,
}
struct CollectedFields {
field_schemas: Vec<TokenStream2>,
from_row_inits: Vec<TokenStream2>,
from_aliased_row_inits: Vec<TokenStream2>,
insert_columns: Vec<TokenStream2>,
insert_values: Vec<TokenStream2>,
insert_pushes: Vec<TokenStream2>,
returning_cols: Vec<TokenStream2>,
auto_assigns: Vec<TokenStream2>,
auto_field_idents: Vec<(syn::Ident, String)>,
generated_field_idents: Vec<(syn::Ident, String)>,
first_auto_value_ty: Option<Type>,
bulk_pushes_no_auto: Vec<TokenStream2>,
bulk_pushes_all: Vec<TokenStream2>,
bulk_columns_no_auto: Vec<TokenStream2>,
bulk_columns_all: Vec<TokenStream2>,
bulk_auto_uniformity: Vec<TokenStream2>,
first_auto_ident: Option<syn::Ident>,
has_auto: bool,
pk_is_auto: bool,
update_assignments: Vec<TokenStream2>,
upsert_update_columns: Vec<TokenStream2>,
primary_key: Option<(syn::Ident, String)>,
column_entries: Vec<ColumnEntry>,
field_names: Vec<String>,
fk_relations: Vec<FkRelation>,
soft_delete_column: Option<String>,
soft_delete_field_ident: Option<syn::Ident>,
}
#[derive(Clone)]
struct FkRelation {
parent_type: Type,
fk_column: String,
pk_kind: DetectedKind,
nullable: bool,
related_name: Option<String>,
}
fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
let root = rustango_root();
let cap = named.named.len();
let mut out = CollectedFields {
field_schemas: Vec::with_capacity(cap),
from_row_inits: Vec::with_capacity(cap),
from_aliased_row_inits: Vec::with_capacity(cap),
insert_columns: Vec::with_capacity(cap),
insert_values: Vec::with_capacity(cap),
insert_pushes: Vec::with_capacity(cap),
returning_cols: Vec::new(),
auto_assigns: Vec::new(),
auto_field_idents: Vec::new(),
generated_field_idents: Vec::new(),
first_auto_value_ty: None,
bulk_pushes_no_auto: Vec::with_capacity(cap),
bulk_pushes_all: Vec::with_capacity(cap),
bulk_columns_no_auto: Vec::with_capacity(cap),
bulk_columns_all: Vec::with_capacity(cap),
bulk_auto_uniformity: Vec::new(),
first_auto_ident: None,
has_auto: false,
pk_is_auto: false,
update_assignments: Vec::with_capacity(cap),
upsert_update_columns: Vec::with_capacity(cap),
primary_key: None,
column_entries: Vec::with_capacity(cap),
field_names: Vec::with_capacity(cap),
fk_relations: Vec::new(),
soft_delete_column: None,
soft_delete_field_ident: None,
};
for field in &named.named {
let info = process_field(field, table)?;
out.field_names.push(info.ident.to_string());
out.field_schemas.push(info.schema);
out.from_row_inits.push(info.from_row_init);
out.from_aliased_row_inits.push(info.from_aliased_row_init);
if let Some(parent_ty) = info.fk_inner.clone() {
out.fk_relations.push(FkRelation {
parent_type: parent_ty,
fk_column: info.column.clone(),
pk_kind: info.fk_pk_kind,
nullable: info.nullable,
related_name: info.related_name.clone(),
});
}
if info.soft_delete {
if out.soft_delete_column.is_some() {
return Err(syn::Error::new_spanned(
field,
"only one field may be marked `#[rustango(soft_delete)]`",
));
}
out.soft_delete_column = Some(info.column.clone());
out.soft_delete_field_ident = Some(info.ident.clone());
}
let column = info.column.as_str();
let ident = info.ident;
if info.generated_as.is_some() {
out.column_entries.push(ColumnEntry {
ident: ident.clone(),
value_ty: info.value_ty.clone(),
name: ident.to_string(),
column: info.column.clone(),
field_type_tokens: info.field_type_tokens,
});
out.returning_cols.push(quote!(#column));
out.generated_field_idents
.push((ident.clone(), info.column.clone()));
continue;
}
out.insert_columns.push(quote!(#column));
out.insert_values.push(quote! {
::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#ident)
)
});
if info.auto {
out.has_auto = true;
if out.first_auto_ident.is_none() {
out.first_auto_ident = Some(ident.clone());
out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
}
if !info.default_uuid_v7 {
out.returning_cols.push(quote!(#column));
out.auto_field_idents
.push((ident.clone(), info.column.clone()));
out.auto_assigns.push(quote! {
self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
});
}
if info.default_uuid_v7 {
out.insert_pushes.push(quote! {
if matches!(&self.#ident, #root::sql::Auto::Unset) {
self.#ident = #root::sql::Auto::Set(
#root::__uuid::Uuid::now_v7(),
);
}
if let #root::sql::Auto::Set(_v) = &self.#ident {
_columns.push(#column);
_values.push(::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(_v)
));
}
});
} else {
out.insert_pushes.push(quote! {
if let #root::sql::Auto::Set(_v) = &self.#ident {
_columns.push(#column);
_values.push(::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(_v)
));
}
});
}
out.bulk_columns_all.push(quote!(#column));
out.bulk_pushes_all.push(quote! {
_row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&_row.#ident)
));
});
let ident_clone = ident.clone();
out.bulk_auto_uniformity.push(quote! {
for _r in rows.iter().skip(1) {
if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
return ::core::result::Result::Err(
#root::sql::ExecError::Sql(
#root::sql::SqlError::BulkAutoMixed
)
);
}
}
});
} else {
out.insert_pushes.push(quote! {
_columns.push(#column);
_values.push(::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#ident)
));
});
out.bulk_columns_no_auto.push(quote!(#column));
out.bulk_columns_all.push(quote!(#column));
let push_expr = quote! {
_row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&_row.#ident)
));
};
out.bulk_pushes_no_auto.push(push_expr.clone());
out.bulk_pushes_all.push(push_expr);
}
if info.primary_key {
if out.primary_key.is_some() {
return Err(syn::Error::new_spanned(
field,
"only one field may be marked `#[rustango(primary_key)]`",
));
}
out.primary_key = Some((ident.clone(), info.column.clone()));
if info.auto {
out.pk_is_auto = true;
}
} else if info.auto_now_add {
} else if info.auto_now {
out.update_assignments.push(quote! {
#root::core::Assignment {
column: #column,
value: ::core::convert::Into::<#root::core::Expr>::into(
::core::convert::Into::<#root::core::SqlValue>::into(
#root::__chrono::Utc::now()
)
),
}
});
out.upsert_update_columns.push(quote!(#column));
} else {
out.update_assignments.push(quote! {
#root::core::Assignment {
column: #column,
value: ::core::convert::Into::<#root::core::Expr>::into(
::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#ident)
)
),
}
});
out.upsert_update_columns.push(quote!(#column));
}
out.column_entries.push(ColumnEntry {
ident: ident.clone(),
value_ty: info.value_ty.clone(),
name: ident.to_string(),
column: info.column.clone(),
field_type_tokens: info.field_type_tokens,
});
}
Ok(out)
}
fn model_impl_tokens(
struct_name: &syn::Ident,
model_name: &str,
table: &str,
display: Option<&str>,
app_label: Option<&str>,
admin: Option<&AdminAttrs>,
default_order: &[(String, bool, proc_macro2::Span)],
field_schemas: &[TokenStream2],
soft_delete_column: Option<&str>,
permissions: bool,
audit_track: Option<&[String]>,
m2m_relations: &[M2MAttr],
indexes: &[IndexAttr],
checks: &[CheckAttr],
excludes: &[ExcludeAttr],
composite_fks: &[CompositeFkAttr],
generic_fks: &[GenericFkAttr],
scope: Option<&str>,
is_view: bool,
verbose_name: Option<&str>,
verbose_name_plural: Option<&str>,
managed: bool,
base_manager_name: Option<&str>,
order_with_respect_to: Option<&str>,
proxy: bool,
required_db_features: &[String],
required_db_vendor: Option<&str>,
default_related_name: Option<&str>,
db_table_comment: Option<&str>,
get_latest_by: Option<(&str, bool)>,
extra_permissions: &[(String, String)],
default_permissions: &[String],
global_scopes: &[GlobalScopeAttr],
reverse_has_relations: &[ReverseHasAttr],
generic_has_relations: &[GenericHasAttr],
) -> TokenStream2 {
let root = rustango_root();
let display_tokens = if let Some(name) = display {
quote!(::core::option::Option::Some(#name))
} else {
quote!(::core::option::Option::None)
};
let app_label_tokens = if let Some(name) = app_label {
quote!(::core::option::Option::Some(#name))
} else {
quote!(::core::option::Option::None)
};
let soft_delete_tokens = if let Some(col) = soft_delete_column {
quote!(::core::option::Option::Some(#col))
} else {
quote!(::core::option::Option::None)
};
let audit_track_tokens = match audit_track {
None => quote!(::core::option::Option::None),
Some(names) => {
let lits = names.iter().map(|n| n.as_str());
quote!(::core::option::Option::Some(&[ #(#lits),* ]))
}
};
let admin_tokens = admin_config_tokens(admin);
let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
Some("registry") => quote!(#root::core::ModelScope::Registry),
_ => quote!(#root::core::ModelScope::Tenant),
};
let verbose_name_tokens = optional_str(verbose_name);
let verbose_name_plural_tokens = optional_str(verbose_name_plural);
let base_manager_name_tokens = optional_str(base_manager_name);
let order_with_respect_to_tokens = optional_str(order_with_respect_to);
let required_db_features_lits: Vec<&str> =
required_db_features.iter().map(String::as_str).collect();
let required_db_vendor_tokens = optional_str(required_db_vendor);
let default_related_name_tokens = optional_str(default_related_name);
let db_table_comment_tokens = optional_str(db_table_comment);
let get_latest_by_tokens = match get_latest_by {
Some((col, desc)) => {
quote!(::core::option::Option::Some((#col, #desc)))
}
None => quote!(::core::option::Option::None),
};
let extra_permission_tokens: Vec<_> = extra_permissions
.iter()
.map(|(c, l)| quote!((#c, #l)))
.collect();
let default_permission_tokens: Vec<_> = default_permissions
.iter()
.map(|action| quote!(#action))
.collect();
let indexes_tokens = indexes.iter().map(|idx| {
let derived_name = idx.name.clone().unwrap_or_else(|| {
let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
if n.len() > 63 {
n.truncate(63);
}
n
});
let name = derived_name.as_str();
let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
let unique = idx.unique;
let method_variant = match idx.method.as_str() {
"gin" => quote!(#root::core::IndexMethod::Gin),
"gist" => quote!(#root::core::IndexMethod::Gist),
"brin" => quote!(#root::core::IndexMethod::Brin),
"spgist" => quote!(#root::core::IndexMethod::SpGist),
"hash" => quote!(#root::core::IndexMethod::Hash),
"bloom" => quote!(#root::core::IndexMethod::Bloom),
_ => quote!(#root::core::IndexMethod::BTree),
};
let where_clause = match &idx.where_clause {
Some(s) => quote!(::core::option::Option::Some(#s)),
None => quote!(::core::option::Option::None),
};
let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
quote! {
#root::core::IndexSchema {
name: #name,
columns: &[ #(#cols),* ],
unique: #unique,
method: #method_variant,
where_clause: #where_clause,
include: &[ #(#include_lits),* ],
}
}
});
let checks_tokens = checks.iter().map(|c| {
let name = c.name.as_str();
let expr = c.expr.as_str();
quote! {
#root::core::CheckConstraint {
name: #name,
expr: #expr,
}
}
});
let excludes_tokens = excludes.iter().map(|e| {
let name = e.name.as_str();
let using = e.using.as_str();
let element_tokens = e.elements.iter().map(|(col, op)| {
let col_s = col.as_str();
let op_s = op.as_str();
quote!((#col_s, #op_s))
});
let where_tokens = match e.where_clause.as_deref() {
Some(w) => quote!(::core::option::Option::Some(#w)),
None => quote!(::core::option::Option::None),
};
quote! {
#root::core::ExclusionConstraint {
name: #name,
using: #using,
elements: &[ #(#element_tokens),* ],
where_clause: #where_tokens,
}
}
});
let composite_fk_tokens = composite_fks.iter().map(|rel| {
let name = rel.name.as_str();
let to = rel.to.as_str();
let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
quote! {
#root::core::CompositeFkRelation {
name: #name,
to: #to,
from: &[ #(#from_cols),* ],
on: &[ #(#on_cols),* ],
}
}
});
let generic_fk_tokens = generic_fks.iter().map(|rel| {
let name = rel.name.as_str();
let ct_col = rel.ct_column.as_str();
let pk_col = rel.pk_column.as_str();
quote! {
#root::core::GenericRelation {
name: #name,
ct_column: #ct_col,
pk_column: #pk_col,
}
}
});
let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
let col_lit = col.as_str();
quote! { (#col_lit, #desc) }
});
let global_scope_tokens = global_scopes.iter().map(|s| {
let name = s.name.as_str();
let apply = &s.apply;
quote! {
#root::core::GlobalScope {
name: #name,
apply: #apply,
}
}
});
let m2m_tokens = m2m_relations.iter().map(|rel| {
let name = rel.name.as_str();
let to = rel.to.as_str();
let through = rel.through.as_str();
let src = rel.src.as_str();
let dst = rel.dst.as_str();
let auto_create = rel.auto_create;
quote! {
#root::core::M2MRelation {
name: #name,
to: #to,
through: #through,
src_col: #src,
dst_col: #dst,
auto_create: #auto_create,
}
}
});
let reverse_relations_override = if reverse_has_relations.is_empty() {
quote!()
} else {
let entries = reverse_has_relations.iter().map(|rel| {
let name = rel.name.as_str();
let child = &rel.child;
let child_fk_column = rel.child_fk_column.as_str();
let self_pk_column = rel.self_pk_column.as_str();
quote! {
#root::core::ReverseRelation {
name: #name,
child_schema: <#child as #root::core::Model>::SCHEMA,
child_fk_column: #child_fk_column,
self_pk_column: #self_pk_column,
}
}
});
quote! {
fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
RELS
}
}
};
let generic_reverse_relations_override = if generic_has_relations.is_empty() {
quote!()
} else {
let entries = generic_has_relations.iter().map(|rel| {
let name = rel.name.as_str();
let child = &rel.child;
let ct_column = rel.ct_column.as_str();
let pk_column = rel.pk_column.as_str();
let self_pk_column = rel.self_pk_column.as_str();
quote! {
#root::core::GenericReverseRelation {
name: #name,
child_schema: <#child as #root::core::Model>::SCHEMA,
ct_column: #ct_column,
pk_column: #pk_column,
self_pk_column: #self_pk_column,
}
}
});
quote! {
fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
RELS
}
}
};
quote! {
impl #root::core::Model for #struct_name {
const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
name: #model_name,
table: #table,
fields: &[ #(#field_schemas),* ],
display: #display_tokens,
app_label: #app_label_tokens,
admin: #admin_tokens,
soft_delete_column: #soft_delete_tokens,
permissions: #permissions,
audit_track: #audit_track_tokens,
m2m: &[ #(#m2m_tokens),* ],
indexes: &[ #(#indexes_tokens),* ],
check_constraints: &[ #(#checks_tokens),* ],
exclusion_constraints: &[ #(#excludes_tokens),* ],
composite_relations: &[ #(#composite_fk_tokens),* ],
generic_relations: &[ #(#generic_fk_tokens),* ],
scope: #scope_tokens,
default_order: &[ #(#default_order_tokens),* ],
is_view: #is_view,
verbose_name: #verbose_name_tokens,
verbose_name_plural: #verbose_name_plural_tokens,
managed: #managed,
base_manager_name: #base_manager_name_tokens,
order_with_respect_to: #order_with_respect_to_tokens,
proxy: #proxy,
required_db_features: &[ #(#required_db_features_lits),* ],
required_db_vendor: #required_db_vendor_tokens,
default_related_name: #default_related_name_tokens,
db_table_comment: #db_table_comment_tokens,
get_latest_by: #get_latest_by_tokens,
extra_permissions: &[ #(#extra_permission_tokens),* ],
default_permissions: &[ #(#default_permission_tokens),* ],
global_scopes: &[ #(#global_scope_tokens),* ],
};
#reverse_relations_override
#generic_reverse_relations_override
}
}
}
fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
let root = rustango_root();
let Some(admin) = admin else {
return quote!(::core::option::Option::None);
};
let list_display = admin
.list_display
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let list_display_lits = list_display.iter().map(|s| s.as_str());
let search_fields = admin
.search_fields
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let search_fields_lits = search_fields.iter().map(|s| s.as_str());
let readonly_fields = admin
.readonly_fields
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
let list_filter = admin
.list_filter
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let list_filter_lits = list_filter.iter().map(|s| s.as_str());
let actions = admin
.actions
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let actions_lits = actions.iter().map(|s| s.as_str());
let fieldsets = admin
.fieldsets
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
let title = title.as_str();
let field_lits = fields.iter().map(|s| s.as_str());
quote!(#root::core::Fieldset {
title: #title,
fields: &[ #( #field_lits ),* ],
})
});
let list_display_links = admin
.list_display_links
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
let actions_on_top = admin.actions_on_top.unwrap_or(true);
let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
let prepopulated = admin
.prepopulated_fields
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
let target = target.as_str();
let source_lits = sources.iter().map(|s| s.as_str());
quote!(#root::core::PrepopulatedField {
target: #target,
sources: &[ #( #source_lits ),* ],
})
});
let raw_id_fields = admin
.raw_id_fields
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
let autocomplete_fields = admin
.autocomplete_fields
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
let list_select_related_tokens = match admin.list_select_related.as_deref() {
None | Some("all") => quote!(#root::core::ListSelectRelated::All),
Some("none") => quote!(#root::core::ListSelectRelated::None),
Some(raw) => {
let names: Vec<&str> = raw
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
}
};
let formfield_pairs: Vec<(&str, &str)> = admin
.formfield_overrides
.as_ref()
.map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
.unwrap_or_default();
let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
let field = *field;
let widget = *widget;
quote!((#field, #widget))
});
let list_per_page = admin.list_per_page.unwrap_or(0);
let ordering_pairs = admin
.ordering
.as_ref()
.map(|(v, _)| v.as_slice())
.unwrap_or(&[]);
let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
let name = name.as_str();
let desc = *desc;
quote!((#name, #desc))
});
quote! {
::core::option::Option::Some(&#root::core::AdminConfig {
list_display: &[ #( #list_display_lits ),* ],
search_fields: &[ #( #search_fields_lits ),* ],
list_per_page: #list_per_page,
ordering: &[ #( #ordering_tokens ),* ],
readonly_fields: &[ #( #readonly_fields_lits ),* ],
list_filter: &[ #( #list_filter_lits ),* ],
actions: &[ #( #actions_lits ),* ],
fieldsets: &[ #( #fieldset_tokens ),* ],
list_display_links: &[ #( #list_display_links_lits ),* ],
search_help_text: #search_help_text,
actions_on_top: #actions_on_top,
actions_on_bottom: #actions_on_bottom,
date_hierarchy: #date_hierarchy,
prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
list_select_related: #list_select_related_tokens,
formfield_overrides: &[ #( #formfield_tokens ),* ],
})
}
}
fn inherent_impl_tokens(
struct_name: &syn::Ident,
fields: &CollectedFields,
primary_key: Option<&(syn::Ident, String)>,
column_consts: &TokenStream2,
audited_fields: Option<&[&ColumnEntry]>,
indexes: &[IndexAttr],
manager_fns: &[syn::Ident],
) -> TokenStream2 {
let root = rustango_root();
let executor_passes_to_data_write = if audited_fields.is_some() {
quote!(&mut *_executor)
} else {
quote!(_executor)
};
let executor_param = if audited_fields.is_some() {
quote!(_executor: &mut #root::sql::sqlx::PgConnection)
} else {
quote!(_executor: _E)
};
let executor_generics = if audited_fields.is_some() {
quote!()
} else {
quote!(<'_c, _E>)
};
let executor_where = if audited_fields.is_some() {
quote!()
} else {
quote! {
where
_E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
}
};
let pool_to_save_on = if audited_fields.is_some() {
quote! {
let mut _conn = pool.acquire().await?;
self.save_on(&mut *_conn).await
}
} else {
quote!(self.save_on(pool).await)
};
let pool_to_insert_on = if audited_fields.is_some() {
quote! {
let mut _conn = pool.acquire().await?;
self.insert_on(&mut *_conn).await
}
} else {
quote!(self.insert_on(pool).await)
};
let pool_to_delete_on = if audited_fields.is_some() {
quote! {
let mut _conn = pool.acquire().await?;
self.delete_on(&mut *_conn).await
}
} else {
quote!(self.delete_on(pool).await)
};
let pool_to_bulk_insert_on = if audited_fields.is_some() {
quote! {
let mut _conn = pool.acquire().await?;
Self::bulk_insert_on(rows, &mut *_conn).await
}
} else {
quote!(Self::bulk_insert_on(rows, pool).await)
};
let pool_to_upsert_on = if audited_fields.is_some() {
quote! {
let mut _conn = pool.acquire().await?;
self.upsert_on(&mut *_conn).await
}
} else {
quote!(self.upsert_on(pool).await)
};
let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
quote!()
} else if audited_fields.is_some() && fields.has_auto {
quote!()
} else if fields.has_auto {
let pushes = &fields.insert_pushes;
let returning_cols = &fields.returning_cols;
if fields.returning_cols.is_empty() {
quote! {
pub async fn insert_pool(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
let mut _columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::new();
let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes )*
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
values: _values,
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::None,
};
#root::sql::insert_pool(pool, &_query).await
}
pub async fn insert_or_ignore(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<bool, #root::sql::ExecError> {
let mut _columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::new();
let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes )*
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
values: _values,
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::Some(
#root::core::ConflictClause::DoNothing,
),
};
let dialect = pool.dialect();
let stmt = dialect.compile_insert(&_query)?;
let rows = #root::sql::raw_execute_pool(
pool, &stmt.sql, stmt.params,
).await?;
::core::result::Result::Ok(rows > 0)
}
}
} else {
quote! {
pub async fn insert_pool(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
let mut _columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::new();
let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes )*
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
values: _values,
returning: ::std::vec![ #( #returning_cols ),* ],
on_conflict: ::core::option::Option::None,
};
let _result = #root::sql::insert_returning_pool(
pool, &_query,
).await?;
#root::sql::apply_auto_pk(_result, self)
}
pub async fn insert_or_ignore(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<bool, #root::sql::ExecError> {
let mut _columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::new();
let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes )*
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
values: _values,
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::Some(
#root::core::ConflictClause::DoNothing,
),
};
let dialect = pool.dialect();
let stmt = dialect.compile_insert(&_query)?;
let rows = #root::sql::raw_execute_pool(
pool, &stmt.sql, stmt.params,
).await?;
::core::result::Result::Ok(rows > 0)
}
}
}
} else {
let insert_columns = &fields.insert_columns;
let insert_values = &fields.insert_values;
quote! {
pub async fn insert_pool(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: ::std::vec![ #( #insert_columns ),* ],
values: ::std::vec![ #( #insert_values ),* ],
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::None,
};
#root::sql::insert_pool(pool, &_query).await
}
pub async fn insert_or_ignore(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<bool, #root::sql::ExecError> {
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: ::std::vec![ #( #insert_columns ),* ],
values: ::std::vec![ #( #insert_values ),* ],
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::Some(
#root::core::ConflictClause::DoNothing,
),
};
let dialect = pool.dialect();
let stmt = dialect.compile_insert(&_query)?;
let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
::core::result::Result::Ok(rows > 0)
}
}
};
let audit_pair_tokens: Vec<TokenStream2> = audited_fields
.map(|tracked| {
tracked
.iter()
.map(|c| {
let column_lit = c.column.as_str();
let ident = &c.ident;
quote! {
(
#column_lit,
#root::__serde_json::to_value(&self.#ident)
.unwrap_or(#root::__serde_json::Value::Null),
)
}
})
.collect()
})
.unwrap_or_default();
let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
if fields.pk_is_auto {
quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
} else {
quote!(::std::format!("{}", &self.#pk_ident))
}
} else {
quote!(::std::string::String::new())
};
let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
if audited_fields.is_some() {
let pairs = audit_pair_tokens.iter();
let pk_str = audit_pk_to_string.clone();
quote! {
let _audit_entry = #root::audit::PendingEntry {
entity_table: <Self as #root::core::Model>::SCHEMA.table,
entity_pk: #pk_str,
operation: #op_path,
source: #root::audit::current_source(),
changes: #root::audit::snapshot_changes(&[
#( #pairs ),*
]),
};
#root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
}
} else {
quote!()
}
};
let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
let pk_column_lit = pk_col.as_str();
let assignments = &fields.update_assignments;
if audited_fields.is_some() {
if fields.pk_is_auto {
quote!()
} else {
let pairs = audit_pair_tokens.iter();
let pairs2 = audit_pair_tokens.iter();
let pk_str = audit_pk_to_string.clone();
let pk_str2 = audit_pk_to_string.clone();
quote! {
pub async fn save_pool(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![ #( #assignments ),* ],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _audit_entry = #root::audit::PendingEntry {
entity_table: <Self as #root::core::Model>::SCHEMA.table,
entity_pk: #pk_str,
operation: #root::audit::AuditOp::Update,
source: #root::audit::current_source(),
changes: #root::audit::snapshot_changes(&[
#( #pairs ),*
]),
};
let _affected = #root::audit::save_one_with_audit(
pool, &_query, &_audit_entry,
).await?;
::core::result::Result::Ok(_affected)
}
pub async fn save_partial(
&mut self,
fields: &[&str],
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
if fields.is_empty() {
#root::__tracing::warn!(
target: "rustango::save_partial",
model = <Self as #root::core::Model>::SCHEMA.name,
"save_partial called with empty field list — no-op"
);
return ::core::result::Result::Ok(0);
}
let _schema = <Self as #root::core::Model>::SCHEMA;
let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
::std::collections::HashSet::with_capacity(fields.len());
for f in fields {
match _schema.field(f) {
::core::option::Option::Some(fs) => {
_wanted_cols.insert(fs.column);
}
::core::option::Option::None => {
return ::core::result::Result::Err(
#root::sql::ExecError::Query(
#root::core::QueryError::UnknownField {
model: _schema.name,
field: (*f).to_owned(),
}
)
);
}
}
}
let _full: ::std::vec::Vec<#root::core::Assignment> =
::std::vec![ #( #assignments ),* ];
let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
.into_iter()
.filter(|a| _wanted_cols.contains(a.column))
.collect();
if _filtered.is_empty() {
#root::__tracing::warn!(
target: "rustango::save_partial",
model = _schema.name,
"save_partial: every named field maps to a non-assignable column — no-op"
);
return ::core::result::Result::Ok(0);
}
let _query = #root::core::UpdateQuery {
model: _schema,
set: _filtered,
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
::std::vec![ #( #pairs2 ),* ];
let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
_all_pairs
.into_iter()
.filter(|(col, _)| _wanted_cols.contains(col))
.collect();
let _audit_entry = #root::audit::PendingEntry {
entity_table: _schema.table,
entity_pk: #pk_str2,
operation: #root::audit::AuditOp::Update,
source: #root::audit::current_source(),
changes: #root::audit::snapshot_changes(&_narrowed),
};
let _affected = #root::audit::save_one_with_audit(
pool, &_query, &_audit_entry,
).await?;
::core::result::Result::Ok(_affected)
}
pub async fn save_partial_typed<
L: #root::core::TypedFieldList<Self>,
>(
&mut self,
fields: L,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _names = fields.rust_field_names();
let _refs: ::std::vec::Vec<&str> =
_names.iter().copied().collect();
self.save_partial(&_refs, pool).await
}
}
}
} else {
let dispatch_unset = if fields.pk_is_auto {
quote! {
if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
return self.insert_pool(pool).await.map(|()| 1u64);
}
}
} else {
quote!()
};
quote! {
pub async fn save_pool(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
#dispatch_unset
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![ #( #assignments ),* ],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _affected = #root::sql::update_pool(pool, &_query).await?;
::core::result::Result::Ok(_affected)
}
pub async fn save_partial(
&mut self,
fields: &[&str],
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
if fields.is_empty() {
#root::__tracing::warn!(
target: "rustango::save_partial",
model = <Self as #root::core::Model>::SCHEMA.name,
"save_partial called with empty field list — no-op"
);
return ::core::result::Result::Ok(0);
}
let _schema = <Self as #root::core::Model>::SCHEMA;
let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
::std::collections::HashSet::with_capacity(fields.len());
for f in fields {
match _schema.field(f) {
::core::option::Option::Some(fs) => {
_wanted_cols.insert(fs.column);
}
::core::option::Option::None => {
return ::core::result::Result::Err(
#root::sql::ExecError::Query(
#root::core::QueryError::UnknownField {
model: _schema.name,
field: (*f).to_owned(),
}
)
);
}
}
}
let _full: ::std::vec::Vec<#root::core::Assignment> =
::std::vec![ #( #assignments ),* ];
let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
.into_iter()
.filter(|a| _wanted_cols.contains(a.column))
.collect();
if _filtered.is_empty() {
#root::__tracing::warn!(
target: "rustango::save_partial",
model = _schema.name,
"save_partial: every named field maps to a non-assignable column — no-op"
);
return ::core::result::Result::Ok(0);
}
let _query = #root::core::UpdateQuery {
model: _schema,
set: _filtered,
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _affected = #root::sql::update_pool(pool, &_query).await?;
::core::result::Result::Ok(_affected)
}
pub async fn save_partial_typed<
L: #root::core::TypedFieldList<Self>,
>(
&mut self,
fields: L,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _names = fields.rust_field_names();
let _refs: ::std::vec::Vec<&str> =
_names.iter().copied().collect();
self.save_partial(&_refs, pool).await
}
}
}
} else {
quote!()
};
let pool_insert_method = if audited_fields.is_some() {
if let Some(_) = primary_key {
let pushes = if fields.has_auto {
fields.insert_pushes.clone()
} else {
fields
.insert_columns
.iter()
.zip(&fields.insert_values)
.map(|(col, val)| {
quote! {
_columns.push(#col);
_values.push(#val);
}
})
.collect()
};
let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
fields.returning_cols.clone()
} else {
primary_key
.map(|(_, col)| {
let lit = col.as_str();
vec![quote!(#lit)]
})
.unwrap_or_default()
};
let pairs = audit_pair_tokens.iter();
let pk_str = audit_pk_to_string.clone();
quote! {
pub async fn insert_pool(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
let mut _columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::new();
let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes )*
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
values: _values,
returning: ::std::vec![ #( #returning_cols ),* ],
on_conflict: ::core::option::Option::None,
};
let _audit_entry = #root::audit::PendingEntry {
entity_table: <Self as #root::core::Model>::SCHEMA.table,
entity_pk: #pk_str,
operation: #root::audit::AuditOp::Create,
source: #root::audit::current_source(),
changes: #root::audit::snapshot_changes(&[
#( #pairs ),*
]),
};
let _result = #root::audit::insert_one_with_audit(
pool, &_query, &_audit_entry,
).await?;
#root::sql::apply_auto_pk(_result, self)
}
}
} else {
quote!()
}
} else {
pool_insert_method
};
let pool_save_method = if let Some(tracked) = audited_fields {
if let Some((pk_ident, pk_col)) = primary_key {
let pk_column_lit = pk_col.as_str();
let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
let pk_str = audit_pk_to_string.clone();
let mk_before_pairs =
|getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
tracked
.iter()
.map(|c| {
let column_lit = c.column.as_str();
let value_ty = &c.value_ty;
quote! {
(
#column_lit,
match #getter::<#value_ty>(
_audit_before_row, #column_lit,
) {
::core::result::Result::Ok(v) => {
#root::__serde_json::to_value(&v)
.unwrap_or(#root::__serde_json::Value::Null)
}
::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
},
)
}
})
.collect()
};
let before_pairs_pg: Vec<proc_macro2::TokenStream> =
mk_before_pairs(quote!(#root::sql::try_get_returning));
let before_pairs_my: Vec<proc_macro2::TokenStream> =
mk_before_pairs(quote!(#root::sql::try_get_returning_my));
let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
let pg_select_cols: String = tracked
.iter()
.map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
.collect::<Vec<_>>()
.join(", ");
let my_select_cols: String = tracked
.iter()
.map(|c| format!("`{}`", c.column.replace('`', "``")))
.collect::<Vec<_>>()
.join(", ");
let sqlite_select_cols: String = pg_select_cols.clone();
let pk_value_for_bind = if fields.pk_is_auto {
quote!(self.#pk_ident.get().copied().unwrap_or_default())
} else {
quote!(::core::clone::Clone::clone(&self.#pk_ident))
};
let assignments = &fields.update_assignments;
let unset_dispatch = if fields.has_auto {
quote! {
if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
return self.insert_pool(pool).await.map(|()| 1u64);
}
}
} else {
quote!()
};
quote! {
pub async fn save_pool(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
#unset_dispatch
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![ #( #assignments ),* ],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
::std::vec![ #( #after_pairs_pg ),* ];
#root::audit::save_one_with_diff(
pool,
&_query,
#pk_column_lit,
::core::convert::Into::<#root::core::SqlValue>::into(
#pk_value_for_bind,
),
<Self as #root::core::Model>::SCHEMA.table,
#pk_str,
_after_pairs,
#pg_select_cols,
#my_select_cols,
#sqlite_select_cols,
|_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
|_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
|_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
).await
}
}
} else {
quote!()
}
} else {
pool_save_method
};
let pool_delete_method = {
let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
if let Some(pk_ident) = pk_ident_for_pool {
if audited_fields.is_some() {
let pairs = audit_pair_tokens.iter();
let pk_str = audit_pk_to_string.clone();
quote! {
pub async fn delete_pool(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _query = #root::core::DeleteQuery {
model: <Self as #root::core::Model>::SCHEMA,
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _audit_entry = #root::audit::PendingEntry {
entity_table: <Self as #root::core::Model>::SCHEMA.table,
entity_pk: #pk_str,
operation: #root::audit::AuditOp::Delete,
source: #root::audit::current_source(),
changes: #root::audit::snapshot_changes(&[
#( #pairs ),*
]),
};
#root::audit::delete_one_with_audit(
pool, &_query, &_audit_entry,
).await
}
}
} else {
quote! {
pub async fn delete_pool(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _query = #root::core::DeleteQuery {
model: <Self as #root::core::Model>::SCHEMA,
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
#root::sql::delete_pool(pool, &_query).await
}
}
}
} else {
quote!()
}
};
let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
let other_field_clones: Vec<TokenStream2> = fields
.column_entries
.iter()
.filter(|c| &c.ident != pk_ident)
.map(|c| {
let ident = &c.ident;
quote! {
#ident: ::core::clone::Clone::clone(&self.#ident)
}
})
.collect();
let pk_clone_token = if fields.pk_is_auto {
quote! { #pk_ident: #root::sql::Auto::Unset }
} else {
quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
};
let replicate_doc = if fields.pk_is_auto {
quote! {
}
} else {
quote! {
}
};
let column_names: ::std::collections::HashSet<String> = fields
.column_entries
.iter()
.map(|c| c.ident.to_string())
.collect();
let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
if column_names.contains(name) {
quote! {}
} else {
tokens
}
};
let count_method = emit_if_no_field_collision(
"count",
quote! {
pub async fn count(
pool: &#root::sql::Pool,
) -> ::core::result::Result<i64, #root::sql::ExecError> {
use #root::sql::CounterPool as _;
#root::query::QuerySet::<Self>::default()
.count(pool)
.await
}
},
);
let value_method = emit_if_no_field_collision(
"value",
quote! {
pub async fn value<U>(
col: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<U>,
#root::sql::ExecError,
>
where
U: #root::sql::MaybePgScalar
+ #root::sql::MaybeMyScalar
+ #root::sql::MaybeSqliteScalar
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
let _col_static: &'static str = Self::__resolve_col(col)?;
#root::query::QuerySet::<Self>::default()
.values_list_flat(_col_static)
.first::<U>(pool)
.await
}
},
);
let sum_method = emit_if_no_field_collision(
"sum",
quote! {
pub async fn sum<U>(
col: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<U>,
#root::sql::ExecError,
>
where
(::core::option::Option<U>,): #root::sql::MaybePgFromRow
+ #root::sql::MaybeMyFromRow
+ #root::sql::MaybeSqliteFromRow
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
Self::__aggregate_one_pool::<U>(
col,
|c| #root::core::AggregateExpr::Sum(c),
pool,
)
.await
}
},
);
let avg_method = emit_if_no_field_collision(
"avg",
quote! {
pub async fn avg<U>(
col: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<U>,
#root::sql::ExecError,
>
where
(::core::option::Option<U>,): #root::sql::MaybePgFromRow
+ #root::sql::MaybeMyFromRow
+ #root::sql::MaybeSqliteFromRow
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
Self::__aggregate_one_pool::<U>(
col,
|c| #root::core::AggregateExpr::Avg(c),
pool,
)
.await
}
},
);
let min_method = emit_if_no_field_collision(
"min",
quote! {
pub async fn min<U>(
col: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<U>,
#root::sql::ExecError,
>
where
(::core::option::Option<U>,): #root::sql::MaybePgFromRow
+ #root::sql::MaybeMyFromRow
+ #root::sql::MaybeSqliteFromRow
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
Self::__aggregate_one_pool::<U>(
col,
|c| #root::core::AggregateExpr::Min(c),
pool,
)
.await
}
},
);
let max_method = emit_if_no_field_collision(
"max",
quote! {
pub async fn max<U>(
col: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<U>,
#root::sql::ExecError,
>
where
(::core::option::Option<U>,): #root::sql::MaybePgFromRow
+ #root::sql::MaybeMyFromRow
+ #root::sql::MaybeSqliteFromRow
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
Self::__aggregate_one_pool::<U>(
col,
|c| #root::core::AggregateExpr::Max(c),
pool,
)
.await
}
},
);
let first_method = emit_if_no_field_collision(
"first",
quote! {
pub async fn first(
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<Self>,
#root::sql::ExecError,
> {
#root::query::QuerySet::<Self>::default()
.first(pool)
.await
}
},
);
let last_method = emit_if_no_field_collision(
"last",
quote! {
pub async fn last(
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<Self>,
#root::sql::ExecError,
> {
#root::query::QuerySet::<Self>::default()
.last(pool)
.await
}
},
);
quote! {
pub async fn refresh_from_db(
&mut self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
use #root::sql::FetcherPool as _;
let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
::core::clone::Clone::clone(&self.#pk_ident),
);
let mut _rows: ::std::vec::Vec<Self> =
#root::query::QuerySet::<Self>::default()
.filter(::core::stringify!(#pk_ident), _pk_val)
.limit(1)
.fetch(pool)
.await?;
match _rows.into_iter().next() {
::core::option::Option::Some(_fresh) => {
*self = _fresh;
::core::result::Result::Ok(())
}
::core::option::Option::None => ::core::result::Result::Err(
#root::sql::ExecError::Driver(
#root::sql::sqlx::Error::RowNotFound,
),
),
}
}
pub async fn increment(
&self,
col: &str,
by: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
Self::__increment_one(self, col, by, pool).await
}
pub async fn decrement(
&self,
col: &str,
by: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
Self::__increment_one(self, col, -by, pool).await
}
pub async fn increment_each(
col: &str,
by: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
Self::__increment_all(col, by, pool).await
}
pub async fn decrement_each(
col: &str,
by: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
Self::__increment_all(col, -by, pool).await
}
#[doc(hidden)]
pub async fn __increment_one(
this: &Self,
col: &str,
by: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
::core::clone::Clone::clone(&this.#pk_ident),
);
#root::sql::model_shortcuts::increment_one_pool::<Self>(
::core::stringify!(#pk_ident),
_pk_val,
col,
by,
pool,
)
.await
}
#[doc(hidden)]
pub async fn __increment_all(
col: &str,
by: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
#root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
}
#[doc(hidden)]
pub fn __resolve_col(
col: &str,
) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
#root::sql::model_shortcuts::resolve_col::<Self>(col)
}
#[doc(hidden)]
#[must_use]
pub fn __add_signed_expr(
col_static: &'static str,
signed_by: i64,
) -> #root::core::Expr {
#root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
}
pub async fn fresh(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
::core::clone::Clone::clone(&self.#pk_ident),
);
let _rows: ::std::vec::Vec<Self> =
#root::query::QuerySet::<Self>::default()
.filter(::core::stringify!(#pk_ident), _pk_val)
.limit(1)
.fetch(pool)
.await?;
::core::result::Result::Ok(_rows.into_iter().next())
}
#replicate_doc
#[must_use]
pub fn replicate(&self) -> Self {
Self {
#pk_clone_token,
#( #other_field_clones, )*
}
}
#first_method
#last_method
pub async fn first_or_fail(
pool: &#root::sql::Pool,
) -> ::core::result::Result<Self, #root::sql::ExecError> {
match #root::query::QuerySet::<Self>::default().first(pool).await? {
::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
::core::option::Option::None => ::core::result::Result::Err(
#root::sql::ExecError::Driver(
#root::sql::sqlx::Error::RowNotFound,
),
),
}
}
pub async fn pluck<U>(
col: &'static str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
where
U: #root::sql::MaybePgScalar
+ #root::sql::MaybeMyScalar
+ #root::sql::MaybeSqliteScalar
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
#root::query::QuerySet::<Self>::default()
.values_list_flat(col)
.fetch::<U>(pool)
.await
}
pub async fn chunk<F, Fut>(
n: i64,
pool: &#root::sql::Pool,
mut cb: F,
) -> ::core::result::Result<(), #root::sql::ExecError>
where
F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
Fut: ::core::future::Future<
Output = ::core::result::Result<(), #root::sql::ExecError>,
>,
{
use #root::sql::FetcherPool as _;
let pk_col = match Self::primary_key_column() {
::core::option::Option::Some(c) => c,
::core::option::Option::None => {
return ::core::result::Result::Ok(());
}
};
let mut offset: i64 = 0;
loop {
let rows: ::std::vec::Vec<Self> =
#root::query::QuerySet::<Self>::default()
.order_by(&[(pk_col, false)])
.limit(n)
.offset(offset)
.fetch(pool)
.await?;
if rows.is_empty() {
return ::core::result::Result::Ok(());
}
let len = rows.len() as i64;
cb(rows).await?;
if len < n {
return ::core::result::Result::Ok(());
}
offset += n;
}
}
pub async fn chunk_by_id<F, Fut>(
n: i64,
pool: &#root::sql::Pool,
mut cb: F,
) -> ::core::result::Result<(), #root::sql::ExecError>
where
F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
Fut: ::core::future::Future<
Output = ::core::result::Result<(), #root::sql::ExecError>,
>,
{
use #root::sql::FetcherPool as _;
let pk_col = match Self::primary_key_column() {
::core::option::Option::Some(c) => c,
::core::option::Option::None => {
return ::core::result::Result::Ok(());
}
};
let mut last_seen: i64 = i64::MIN;
loop {
let key = ::std::format!("{}__gt", pk_col);
let rows: ::std::vec::Vec<Self> =
#root::query::QuerySet::<Self>::default()
.filter(key.as_str(), last_seen)
.order_by(&[(pk_col, false)])
.limit(n)
.fetch(pool)
.await?;
if rows.is_empty() {
return ::core::result::Result::Ok(());
}
let len = rows.len() as i64;
let max_pk = match rows
.last()
.map(|r| r.__rustango_pk_value())
{
::core::option::Option::Some(
#root::core::SqlValue::I64(v),
) => v,
::core::option::Option::Some(
#root::core::SqlValue::I32(v),
) => i64::from(v),
_ => return ::core::result::Result::Ok(()),
};
cb(rows).await?;
if len < n {
return ::core::result::Result::Ok(());
}
last_seen = max_pk;
}
}
pub async fn each<F, Fut>(
batch: i64,
pool: &#root::sql::Pool,
mut cb: F,
) -> ::core::result::Result<(), #root::sql::ExecError>
where
F: ::core::ops::FnMut(Self) -> Fut,
Fut: ::core::future::Future<
Output = ::core::result::Result<(), #root::sql::ExecError>,
>,
{
use #root::sql::FetcherPool as _;
let pk_col = match Self::primary_key_column() {
::core::option::Option::Some(c) => c,
::core::option::Option::None => {
return ::core::result::Result::Ok(());
}
};
let mut last_seen: i64 = i64::MIN;
loop {
let key = ::std::format!("{}__gt", pk_col);
let rows: ::std::vec::Vec<Self> =
#root::query::QuerySet::<Self>::default()
.filter(key.as_str(), last_seen)
.order_by(&[(pk_col, false)])
.limit(batch)
.fetch(pool)
.await?;
if rows.is_empty() {
return ::core::result::Result::Ok(());
}
let len = rows.len() as i64;
let max_pk = match rows
.last()
.map(|r| r.__rustango_pk_value())
{
::core::option::Option::Some(
#root::core::SqlValue::I64(v),
) => v,
::core::option::Option::Some(
#root::core::SqlValue::I32(v),
) => i64::from(v),
_ => return ::core::result::Result::Ok(()),
};
for row in rows {
cb(row).await?;
}
if len < batch {
return ::core::result::Result::Ok(());
}
last_seen = max_pk;
}
}
pub async fn truncate(
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _table = <Self as #root::core::Model>::SCHEMA.table;
let _dialect = pool.dialect();
let _quoted = _dialect.quote_ident(_table);
let _sql = if _dialect.name() == "postgres" {
::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
} else {
::std::format!("DELETE FROM {}", _quoted)
};
#root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
}
pub async fn destroy<V>(
pks: impl ::core::iter::IntoIterator<Item = V>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError>
where
V: ::core::convert::Into<#root::core::SqlValue>,
{
let _values: ::std::vec::Vec<#root::core::SqlValue> =
pks.into_iter().map(::core::convert::Into::into).collect();
if _values.is_empty() {
return ::core::result::Result::Ok(0);
}
let _query = #root::core::DeleteQuery {
model: <Self as #root::core::Model>::SCHEMA,
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: <Self as #root::core::Model>::SCHEMA
.primary_key()
.ok_or_else(|| {
#root::sql::ExecError::Sql(
#root::sql::SqlError::MissingPrimaryKey,
)
})?
.column,
op: #root::core::Op::In,
value: #root::core::SqlValue::List(_values),
},
),
};
#root::sql::delete_pool(pool, &_query).await
}
pub async fn where_(
col: &str,
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.filter(col, val)
.fetch(pool)
.await
}
pub async fn where_in<V>(
col: &str,
vals: impl ::core::iter::IntoIterator<Item = V>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
>
where
V: ::core::convert::Into<#root::core::SqlValue>,
{
use #root::sql::FetcherPool as _;
let _values: ::std::vec::Vec<#root::core::SqlValue> =
vals.into_iter().map(::core::convert::Into::into).collect();
if _values.is_empty() {
return ::core::result::Result::Ok(::std::vec::Vec::new());
}
let _key = ::std::format!("{}__in", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::List(_values))
.fetch(pool)
.await
}
pub async fn where_not_in<V>(
col: &str,
vals: impl ::core::iter::IntoIterator<Item = V>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
>
where
V: ::core::convert::Into<#root::core::SqlValue>,
{
use #root::sql::FetcherPool as _;
let _values: ::std::vec::Vec<#root::core::SqlValue> =
vals.into_iter().map(::core::convert::Into::into).collect();
if _values.is_empty() {
return #root::query::QuerySet::<Self>::default()
.fetch(pool)
.await;
}
let _key = ::std::format!("{}__not_in", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::List(_values))
.fetch(pool)
.await
}
pub async fn where_null(
col: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__isnull", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::Bool(true))
.fetch(pool)
.await
}
pub async fn where_not_null(
col: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__isnull", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::Bool(false))
.fetch(pool)
.await
}
pub async fn random_n(
n: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.order_random()
.limit(n)
.fetch(pool)
.await
}
pub async fn random(
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<Self>,
#root::sql::ExecError,
> {
::core::result::Result::Ok(
Self::random_n(1, pool).await?.into_iter().next(),
)
}
pub async fn oldest(
field: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.order_by(&[(field, false)])
.fetch(pool)
.await
}
pub async fn newest(
field: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.order_by(&[(field, true)])
.fetch(pool)
.await
}
pub async fn where_year(
col: &str,
year: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__year", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::I64(year))
.fetch(pool)
.await
}
pub async fn where_month(
col: &str,
month: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__month", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::I64(month))
.fetch(pool)
.await
}
pub async fn where_day(
col: &str,
day: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__day", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::I64(day))
.fetch(pool)
.await
}
pub async fn where_hour(
col: &str,
hour: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__hour", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::I64(hour))
.fetch(pool)
.await
}
pub async fn where_minute(
col: &str,
minute: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__minute", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::I64(minute))
.fetch(pool)
.await
}
pub async fn where_like(
col: &str,
pattern: impl ::core::convert::Into<::std::string::String>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__like", col);
#root::query::QuerySet::<Self>::default()
.filter(
&_key,
#root::core::SqlValue::String(pattern.into()),
)
.fetch(pool)
.await
}
pub async fn where_ilike(
col: &str,
pattern: impl ::core::convert::Into<::std::string::String>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__ilike", col);
#root::query::QuerySet::<Self>::default()
.filter(
&_key,
#root::core::SqlValue::String(pattern.into()),
)
.fetch(pool)
.await
}
pub async fn where_starts_with(
col: &str,
prefix: impl ::core::convert::Into<::std::string::String>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__startswith", col);
#root::query::QuerySet::<Self>::default()
.filter(
&_key,
#root::core::SqlValue::String(prefix.into()),
)
.fetch(pool)
.await
}
pub async fn where_ends_with(
col: &str,
suffix: impl ::core::convert::Into<::std::string::String>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__endswith", col);
#root::query::QuerySet::<Self>::default()
.filter(
&_key,
#root::core::SqlValue::String(suffix.into()),
)
.fetch(pool)
.await
}
pub async fn where_contains(
col: &str,
substr: impl ::core::convert::Into<::std::string::String>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__contains", col);
#root::query::QuerySet::<Self>::default()
.filter(
&_key,
#root::core::SqlValue::String(substr.into()),
)
.fetch(pool)
.await
}
pub async fn where_gt(
col: &str,
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__gt", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, val)
.fetch(pool)
.await
}
pub async fn where_gte(
col: &str,
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__gte", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, val)
.fetch(pool)
.await
}
pub async fn where_lt(
col: &str,
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__lt", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, val)
.fetch(pool)
.await
}
pub async fn where_lte(
col: &str,
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__lte", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, val)
.fetch(pool)
.await
}
pub async fn where_ne(
col: &str,
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__ne", col);
#root::query::QuerySet::<Self>::default()
.filter(&_key, val)
.fetch(pool)
.await
}
pub async fn where_any(
cols: &[&str],
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
Self::__where_multi(cols, val, false, pool).await
}
pub async fn where_all(
cols: &[&str],
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
Self::__where_multi(cols, val, true, pool).await
}
#[doc(hidden)]
pub async fn __where_multi(
cols: &[&str],
val: impl ::core::convert::Into<#root::core::SqlValue>,
all: bool,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
#root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
.await
}
pub async fn take(
n: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.limit(n)
.fetch(pool)
.await
}
pub async fn for_page(
page: i64,
per_page: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
#root::query::QuerySet::<Self>::default()
.limit(per_page)
.offset(_offset)
.fetch(pool)
.await
}
pub async fn paginate(
page: i64,
per_page: i64,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
(::std::vec::Vec<Self>, i64),
#root::sql::ExecError,
> {
let total = {
use #root::sql::CounterPool as _;
#root::query::QuerySet::<Self>::default()
.count(pool)
.await?
};
let rows = Self::for_page(page, per_page, pool).await?;
::core::result::Result::Ok((rows, total))
}
pub async fn update_where(
where_col: &str,
where_val: impl ::core::convert::Into<#root::core::SqlValue>,
set_col: &str,
set_val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
use #root::sql::UpdaterPool as _;
#root::query::QuerySet::<Self>::default()
.filter(where_col, where_val)
.update()
.set(set_col, set_val)
.execute_pool(pool)
.await
}
pub async fn delete_where(
where_col: &str,
where_val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _query = #root::core::DeleteQuery {
model: <Self as #root::core::Model>::SCHEMA,
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: <Self as #root::core::Model>::SCHEMA
.field(where_col)
.ok_or_else(|| {
#root::sql::ExecError::Query(
#root::core::QueryError::UnknownField {
model: <Self as #root::core::Model>::SCHEMA.name,
field: ::std::string::ToString::to_string(where_col),
},
)
})?
.column,
op: #root::core::Op::Eq,
value: ::core::convert::Into::into(where_val),
},
),
};
#root::sql::delete_pool(pool, &_query).await
}
pub async fn update_all(
set_col: &str,
set_val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
use #root::sql::UpdaterPool as _;
#root::query::QuerySet::<Self>::default()
.update()
.set(set_col, set_val)
.execute_pool(pool)
.await
}
pub async fn where_not_like(
col: &str,
pattern: impl ::core::convert::Into<::std::string::String>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__not_like", col);
#root::query::QuerySet::<Self>::default()
.filter(
&_key,
#root::core::SqlValue::String(pattern.into()),
)
.fetch(pool)
.await
}
pub async fn where_not_ilike(
col: &str,
pattern: impl ::core::convert::Into<::std::string::String>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__not_ilike", col);
#root::query::QuerySet::<Self>::default()
.filter(
&_key,
#root::core::SqlValue::String(pattern.into()),
)
.fetch(pool)
.await
}
pub async fn where_not_between(
col: &str,
lo: impl ::core::convert::Into<#root::core::SqlValue>,
hi: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__not_between", col);
let _vals = #root::core::SqlValue::List(::std::vec![
::core::convert::Into::into(lo),
::core::convert::Into::into(hi),
]);
#root::query::QuerySet::<Self>::default()
.filter(&_key, _vals)
.fetch(pool)
.await
}
#[must_use]
pub fn table_name() -> &'static str {
<Self as #root::core::Model>::SCHEMA.table
}
#[must_use]
pub fn primary_key_column() -> ::core::option::Option<&'static str> {
<Self as #root::core::Model>::SCHEMA
.primary_key()
.map(|f| f.column)
}
pub async fn where_between(
col: &str,
lo: impl ::core::convert::Into<#root::core::SqlValue>,
hi: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
let _key = ::std::format!("{}__between", col);
let _vals = #root::core::SqlValue::List(::std::vec![
::core::convert::Into::into(lo),
::core::convert::Into::into(hi),
]);
#root::query::QuerySet::<Self>::default()
.filter(&_key, _vals)
.fetch(pool)
.await
}
pub async fn first_where(
col: &str,
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<Self>,
#root::sql::ExecError,
> {
#root::query::QuerySet::<Self>::default()
.filter(col, val)
.first(pool)
.await
}
#value_method
pub async fn latest(
field: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<Self>,
#root::sql::ExecError,
> {
#root::query::QuerySet::<Self>::default()
.latest(field, pool)
.await
}
pub async fn earliest(
field: &str,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<Self>,
#root::sql::ExecError,
> {
#root::query::QuerySet::<Self>::default()
.earliest(field, pool)
.await
}
#count_method
pub async fn exists(
pool: &#root::sql::Pool,
) -> ::core::result::Result<bool, #root::sql::ExecError> {
use #root::sql::ExistsPool as _;
#root::query::QuerySet::<Self>::default()
.exists(pool)
.await
}
pub async fn doesnt_exist(
pool: &#root::sql::Pool,
) -> ::core::result::Result<bool, #root::sql::ExecError> {
Self::exists(pool).await.map(|e| !e)
}
pub async fn contains_pk(
pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
pool: &#root::sql::Pool,
) -> ::core::result::Result<bool, #root::sql::ExecError> {
use #root::sql::ExistsPool as _;
#root::query::QuerySet::<Self>::default()
.contains_pk(pool, pk)
.await
}
#sum_method
#avg_method
#min_method
#max_method
#[doc(hidden)]
pub async fn __aggregate_one_pool<U>(
col: &str,
build: fn(&'static str) -> #root::core::AggregateExpr,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::core::option::Option<U>,
#root::sql::ExecError,
>
where
(::core::option::Option<U>,): #root::sql::MaybePgFromRow
+ #root::sql::MaybeMyFromRow
+ #root::sql::MaybeSqliteFromRow
+ ::core::marker::Send
+ ::core::marker::Unpin,
{
#root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
.await
}
pub async fn all(
pool: &#root::sql::Pool,
) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
{
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.fetch(pool)
.await
}
pub async fn find_many<V>(
pks: impl ::core::iter::IntoIterator<Item = V>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
>
where
V: ::core::convert::Into<#root::core::SqlValue>,
{
use #root::sql::FetcherPool as _;
let _values: ::std::vec::Vec<#root::core::SqlValue> =
pks.into_iter().map(::core::convert::Into::into).collect();
if _values.is_empty() {
return ::core::result::Result::Ok(::std::vec::Vec::new());
}
let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
#root::query::QuerySet::<Self>::default()
.filter(&_key, #root::core::SqlValue::List(_values))
.fetch(pool)
.await
}
pub async fn find(
pk: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
{
use #root::sql::FetcherPool as _;
let _pk_val: #root::core::SqlValue = pk.into();
let mut _rows: ::std::vec::Vec<Self> =
#root::query::QuerySet::<Self>::default()
.filter(::core::stringify!(#pk_ident), _pk_val)
.limit(1)
.fetch(pool)
.await?;
::core::result::Result::Ok(_rows.into_iter().next())
}
pub async fn find_or_fail(
pk: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<Self, #root::sql::ExecError> {
match Self::find(pk, pool).await? {
::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
::core::option::Option::None => ::core::result::Result::Err(
#root::sql::ExecError::Driver(
#root::sql::sqlx::Error::RowNotFound,
),
),
}
}
pub async fn find_many_or_fail<V>(
pks: impl ::core::iter::IntoIterator<Item = V>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
>
where
V: ::core::convert::Into<#root::core::SqlValue>,
{
let _values: ::std::vec::Vec<#root::core::SqlValue> =
pks.into_iter().map(::core::convert::Into::into).collect();
if _values.is_empty() {
return ::core::result::Result::Ok(::std::vec::Vec::new());
}
let mut _seen: ::std::collections::HashSet<
::std::string::String,
> = ::std::collections::HashSet::new();
for v in &_values {
_seen.insert(v.to_display_string());
}
let _expected = _seen.len();
let _rows = Self::find_many(_values, pool).await?;
if _rows.len() < _expected {
return ::core::result::Result::Err(
#root::sql::ExecError::Driver(
#root::sql::sqlx::Error::RowNotFound,
),
);
}
::core::result::Result::Ok(_rows)
}
pub async fn find_or<F>(
pk: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
fallback: F,
) -> ::core::result::Result<Self, #root::sql::ExecError>
where
F: ::core::ops::FnOnce() -> Self,
{
::core::result::Result::Ok(
Self::find(pk, pool).await?.unwrap_or_else(fallback),
)
}
pub async fn find_or_new<F>(
pk: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
fallback: F,
) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
where
F: ::core::ops::FnOnce() -> Self,
{
match Self::find(pk, pool).await? {
::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
::core::option::Option::None => {
::core::result::Result::Ok((fallback(), false))
}
}
}
pub async fn find_or_insert<F>(
pk: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
fallback: F,
) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
where
F: ::core::ops::FnOnce() -> Self,
{
if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
return ::core::result::Result::Ok((_row, true));
}
let mut _new = fallback();
_new.save_pool(pool).await?;
::core::result::Result::Ok((_new, false))
}
pub async fn first_or<F>(
pool: &#root::sql::Pool,
fallback: F,
) -> ::core::result::Result<Self, #root::sql::ExecError>
where
F: ::core::ops::FnOnce() -> Self,
{
::core::result::Result::Ok(
#root::query::QuerySet::<Self>::default()
.first(pool)
.await?
.unwrap_or_else(fallback),
)
}
pub async fn sole(
col: &str,
val: impl ::core::convert::Into<#root::core::SqlValue>,
pool: &#root::sql::Pool,
) -> ::core::result::Result<Self, #root::sql::ExecError> {
let mut _rows = Self::where_(col, val, pool).await?;
match _rows.len() {
0 => ::core::result::Result::Err(
#root::sql::ExecError::Driver(
#root::sql::sqlx::Error::RowNotFound,
),
),
1 => ::core::result::Result::Ok(_rows.remove(0)),
n => ::core::result::Result::Err(
#root::sql::ExecError::MultipleRowsReturned {
op: "sole",
table: <Self as #root::core::Model>::SCHEMA.name,
count: n,
},
),
}
}
}
} else {
quote!()
};
let tx_insert_method = if fields.has_auto {
let pushes = &fields.insert_pushes;
let returning_cols = &fields.returning_cols;
quote! {
pub async fn insert_tx(
&mut self,
tx: &mut #root::sql::PoolTx<'_>,
) -> ::core::result::Result<(), #root::sql::ExecError> {
let mut _columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::new();
let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes )*
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
values: _values,
returning: ::std::vec![ #( #returning_cols ),* ],
on_conflict: ::core::option::Option::None,
};
let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
#root::sql::apply_auto_pk(_result, self)
}
}
} else {
let insert_columns = &fields.insert_columns;
let insert_values = &fields.insert_values;
quote! {
pub async fn insert_tx(
&self,
tx: &mut #root::sql::PoolTx<'_>,
) -> ::core::result::Result<(), #root::sql::ExecError> {
let _query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: ::std::vec![ #( #insert_columns ),* ],
values: ::std::vec![ #( #insert_values ),* ],
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::None,
};
#root::sql::insert_tx(tx, &_query).await
}
}
};
let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
let pk_column_lit = pk_col.as_str();
let assignments = &fields.update_assignments;
let dispatch_unset = if fields.pk_is_auto {
quote! {
if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
return self.insert_tx(tx).await.map(|()| 1u64);
}
}
} else {
quote!()
};
quote! {
pub async fn save_tx(
&mut self,
tx: &mut #root::sql::PoolTx<'_>,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
#dispatch_unset
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![ #( #assignments ),* ],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _affected = #root::sql::update_tx(tx, &_query).await?;
::core::result::Result::Ok(_affected)
}
}
} else {
quote!()
};
let tx_delete_method = {
let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
if let Some(pk_ident) = pk_ident_for_tx {
quote! {
pub async fn delete_tx(
&self,
tx: &mut #root::sql::PoolTx<'_>,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _query = #root::core::DeleteQuery {
model: <Self as #root::core::Model>::SCHEMA,
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
#root::sql::delete_tx(tx, &_query).await
}
}
} else {
quote!()
}
};
let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
audited_fields
{
if tracked.is_empty() {
(quote!(), quote!())
} else {
let select_cols: String = tracked
.iter()
.map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
.collect::<Vec<_>>()
.join(", ");
let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
let select_cols_lit = select_cols;
let pk_column_lit_for_select = pk_column_for_select;
let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
if fields.pk_is_auto {
quote!(self.#pk_ident.get().copied().unwrap_or_default())
} else {
quote!(::core::clone::Clone::clone(&self.#pk_ident))
}
} else {
quote!(0_i64)
};
let before_pairs = tracked.iter().map(|c| {
let column_lit = c.column.as_str();
let value_ty = &c.value_ty;
quote! {
(
#column_lit,
match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
&_audit_before_row, #column_lit,
) {
::core::result::Result::Ok(v) => {
#root::__serde_json::to_value(&v)
.unwrap_or(#root::__serde_json::Value::Null)
}
::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
},
)
}
});
let after_pairs = tracked.iter().map(|c| {
let column_lit = c.column.as_str();
let ident = &c.ident;
quote! {
(
#column_lit,
#root::__serde_json::to_value(&self.#ident)
.unwrap_or(#root::__serde_json::Value::Null),
)
}
});
let pk_str = audit_pk_to_string.clone();
let pre = quote! {
let _audit_select_sql = ::std::format!(
r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
#select_cols_lit,
<Self as #root::core::Model>::SCHEMA.table,
#pk_column_lit_for_select,
);
let _audit_before_pairs:
::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
match #root::sql::sqlx::query(&_audit_select_sql)
.bind(#pk_value_for_bind)
.fetch_optional(&mut *_executor)
.await
{
::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
}
_ => ::core::option::Option::None,
};
};
let post = quote! {
if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
let _audit_after:
::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
::std::vec![ #( #after_pairs ),* ];
let _audit_entry = #root::audit::PendingEntry {
entity_table: <Self as #root::core::Model>::SCHEMA.table,
entity_pk: #pk_str,
operation: #root::audit::AuditOp::Update,
source: #root::audit::current_source(),
changes: #root::audit::diff_changes(
&_audit_before,
&_audit_after,
),
};
#root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
}
};
(pre, post)
}
} else {
(quote!(), quote!())
};
let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
let row_pk_str = if let Some((pk_ident, _)) = primary_key {
if fields.pk_is_auto {
quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
} else {
quote!(::std::format!("{}", &_row.#pk_ident))
}
} else {
quote!(::std::string::String::new())
};
let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
let column_lit = c.column.as_str();
let ident = &c.ident;
quote! {
(
#column_lit,
#root::__serde_json::to_value(&_row.#ident)
.unwrap_or(#root::__serde_json::Value::Null),
)
}
});
quote! {
let _audit_source = #root::audit::current_source();
let mut _audit_entries:
::std::vec::Vec<#root::audit::PendingEntry> =
::std::vec::Vec::with_capacity(rows.len());
for _row in rows.iter() {
_audit_entries.push(#root::audit::PendingEntry {
entity_table: <Self as #root::core::Model>::SCHEMA.table,
entity_pk: #row_pk_str,
operation: #root::audit::AuditOp::Create,
source: _audit_source.clone(),
changes: #root::audit::snapshot_changes(&[
#( #row_pairs ),*
]),
});
}
#root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
}
} else {
quote!()
};
let save_method = if fields.pk_is_auto {
let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
let pk_column_lit = pk_column.as_str();
let assignments = &fields.update_assignments;
let upsert_cols = &fields.upsert_update_columns;
let upsert_pushes = &fields.insert_pushes;
let upsert_returning = &fields.returning_cols;
let upsert_auto_assigns = &fields.auto_assigns;
let upsert_target_columns: Vec<String> = indexes
.iter()
.find(|i| i.unique && !i.columns.is_empty())
.map(|i| i.columns.clone())
.unwrap_or_else(|| vec![pk_column.clone()]);
let upsert_target_lits = upsert_target_columns
.iter()
.map(String::as_str)
.collect::<Vec<_>>();
let conflict_clause = if fields.upsert_update_columns.is_empty() {
quote!(#root::core::ConflictClause::DoNothing)
} else {
quote!(#root::core::ConflictClause::DoUpdate {
target: ::std::vec![ #( #upsert_target_lits ),* ],
update_columns: ::std::vec![ #( #upsert_cols ),* ],
})
};
Some(quote! {
#[cfg(feature = "postgres")]
pub async fn save(
&mut self,
pool: &#root::sql::sqlx::PgPool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
#pool_to_save_on
}
#[cfg(feature = "postgres")]
pub async fn save_on #executor_generics (
&mut self,
#executor_param,
) -> ::core::result::Result<u64, #root::sql::ExecError>
#executor_where
{
if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
}
#audit_update_pre
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![ #( #assignments ),* ],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _affected = #root::sql::__macro_internals::update_on(
#executor_passes_to_data_write,
&_query,
).await?;
#audit_update_post
::core::result::Result::Ok(_affected)
}
#[cfg(feature = "postgres")]
pub async fn save_on_with #executor_generics (
&mut self,
#executor_param,
source: #root::audit::AuditSource,
) -> ::core::result::Result<u64, #root::sql::ExecError>
#executor_where
{
#root::audit::with_source(source, self.save_on(_executor)).await
}
#[cfg(feature = "postgres")]
pub async fn upsert(
&mut self,
pool: &#root::sql::sqlx::PgPool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
#pool_to_upsert_on
}
#[cfg(feature = "postgres")]
pub async fn upsert_on #executor_generics (
&mut self,
#executor_param,
) -> ::core::result::Result<(), #root::sql::ExecError>
#executor_where
{
let mut _columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::new();
let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #upsert_pushes )*
let query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
values: _values,
returning: ::std::vec![ #( #upsert_returning ),* ],
on_conflict: ::core::option::Option::Some(#conflict_clause),
};
let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
#executor_passes_to_data_write,
&query,
).await?;
let _returning_row = &_returning_row_v;
#( #upsert_auto_assigns )*
::core::result::Result::Ok(())
}
})
} else {
None
};
let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
let pk_column_lit = pk_column.as_str();
let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
let col_lit = col;
let sd_field_ident = fields
.soft_delete_field_ident
.clone()
.expect("soft_delete_column without ident");
quote! {
pub async fn soft_delete_on #executor_generics (
&self,
#executor_param,
) -> ::core::result::Result<u64, #root::sql::ExecError>
#executor_where
{
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![
#root::core::Assignment {
column: #col_lit,
value: ::core::convert::Into::<#root::core::Expr>::into(
::core::convert::Into::<#root::core::SqlValue>::into(
#root::__chrono::Utc::now()
)
),
},
],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _affected = #root::sql::__macro_internals::update_on(
#executor_passes_to_data_write,
&_query,
).await?;
#audit_softdelete_emit
::core::result::Result::Ok(_affected)
}
pub async fn restore_on #executor_generics (
&self,
#executor_param,
) -> ::core::result::Result<u64, #root::sql::ExecError>
#executor_where
{
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![
#root::core::Assignment {
column: #col_lit,
value: ::core::convert::Into::<#root::core::Expr>::into(
#root::core::SqlValue::Null
),
},
],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _affected = #root::sql::__macro_internals::update_on(
#executor_passes_to_data_write,
&_query,
).await?;
#audit_restore_emit
::core::result::Result::Ok(_affected)
}
pub async fn soft_delete(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![
#root::core::Assignment {
column: #col_lit,
value: ::core::convert::Into::<#root::core::Expr>::into(
::core::convert::Into::<#root::core::SqlValue>::into(
#root::__chrono::Utc::now()
)
),
},
],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
#root::sql::update_pool(pool, &_query).await
}
pub async fn restore(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
let _query = #root::core::UpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
set: ::std::vec![
#root::core::Assignment {
column: #col_lit,
value: ::core::convert::Into::<#root::core::Expr>::into(
#root::core::SqlValue::Null
),
},
],
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
#root::sql::update_pool(pool, &_query).await
}
pub async fn force_delete(
&self,
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
Self::delete_pool(self, pool).await
}
pub fn trashed(&self) -> bool {
::core::option::Option::is_some(&self.#sd_field_ident)
}
pub async fn active(
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.active()
.fetch(pool)
.await
}
pub async fn only_trashed(
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.only_trashed()
.fetch(pool)
.await
}
pub async fn with_trashed(
pool: &#root::sql::Pool,
) -> ::core::result::Result<
::std::vec::Vec<Self>,
#root::sql::ExecError,
> {
use #root::sql::FetcherPool as _;
#root::query::QuerySet::<Self>::default()
.with_trashed()
.fetch(pool)
.await
}
}
} else {
quote!()
};
quote! {
#[cfg(feature = "postgres")]
pub async fn delete(
&self,
pool: &#root::sql::sqlx::PgPool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
#pool_to_delete_on
}
#[cfg(feature = "postgres")]
pub async fn delete_on #executor_generics (
&self,
#executor_param,
) -> ::core::result::Result<u64, #root::sql::ExecError>
#executor_where
{
let query = #root::core::DeleteQuery {
model: <Self as #root::core::Model>::SCHEMA,
where_clause: #root::core::WhereExpr::Predicate(
#root::core::Filter {
column: #pk_column_lit,
op: #root::core::Op::Eq,
value: ::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
),
}
),
};
let _affected = #root::sql::__macro_internals::delete_on(
#executor_passes_to_data_write,
&query,
).await?;
#audit_delete_emit
::core::result::Result::Ok(_affected)
}
#[cfg(feature = "postgres")]
pub async fn delete_on_with #executor_generics (
&self,
#executor_param,
source: #root::audit::AuditSource,
) -> ::core::result::Result<u64, #root::sql::ExecError>
#executor_where
{
#root::audit::with_source(source, self.delete_on(_executor)).await
}
#pool_delete_method
#pool_insert_method
#pool_save_method
#refresh_replicate_methods
#tx_delete_method
#tx_insert_method
#tx_save_method
#soft_delete_methods
pub fn is(&self, other: &Self) -> bool {
self.#pk_ident == other.#pk_ident
}
pub fn is_not(&self, other: &Self) -> bool {
self.#pk_ident != other.#pk_ident
}
#[must_use]
pub fn get_key(&self) -> #root::core::SqlValue {
::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
}
}
});
let insert_method = if fields.has_auto {
let pushes = &fields.insert_pushes;
let returning_cols = &fields.returning_cols;
let auto_assigns = &fields.auto_assigns;
quote! {
#[cfg(feature = "postgres")]
pub async fn insert(
&mut self,
pool: &#root::sql::sqlx::PgPool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
#pool_to_insert_on
}
#[cfg(feature = "postgres")]
pub async fn insert_on #executor_generics (
&mut self,
#executor_param,
) -> ::core::result::Result<(), #root::sql::ExecError>
#executor_where
{
let mut _columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::new();
let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes )*
let query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
values: _values,
returning: ::std::vec![ #( #returning_cols ),* ],
on_conflict: ::core::option::Option::None,
};
let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
#executor_passes_to_data_write,
&query,
).await?;
let _returning_row = &_returning_row_v;
#( #auto_assigns )*
#audit_insert_emit
::core::result::Result::Ok(())
}
#[cfg(feature = "postgres")]
pub async fn insert_on_with #executor_generics (
&mut self,
#executor_param,
source: #root::audit::AuditSource,
) -> ::core::result::Result<(), #root::sql::ExecError>
#executor_where
{
#root::audit::with_source(source, self.insert_on(_executor)).await
}
}
} else {
let insert_columns = &fields.insert_columns;
let insert_values = &fields.insert_values;
quote! {
#[cfg(feature = "postgres")]
pub async fn insert(
&self,
pool: &#root::sql::sqlx::PgPool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
self.insert_on(pool).await
}
#[cfg(feature = "postgres")]
pub async fn insert_on<'_c, _E>(
&self,
_executor: _E,
) -> ::core::result::Result<(), #root::sql::ExecError>
where
_E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
{
let query = #root::core::InsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: ::std::vec![ #( #insert_columns ),* ],
values: ::std::vec![ #( #insert_values ),* ],
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::None,
};
#root::sql::__macro_internals::insert_on(_executor, &query).await
}
}
};
let bulk_insert_method = if fields.has_auto {
let cols_no_auto = &fields.bulk_columns_no_auto;
let cols_all = &fields.bulk_columns_all;
let pushes_no_auto = &fields.bulk_pushes_no_auto;
let pushes_all = &fields.bulk_pushes_all;
let returning_cols = &fields.returning_cols;
let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
let uniformity = &fields.bulk_auto_uniformity;
let first_auto_ident = fields
.first_auto_ident
.as_ref()
.expect("has_auto implies first_auto_ident is Some");
quote! {
#[cfg(feature = "postgres")]
pub async fn bulk_insert(
rows: &mut [Self],
pool: &#root::sql::sqlx::PgPool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
#pool_to_bulk_insert_on
}
#[cfg(feature = "postgres")]
pub async fn bulk_insert_on #executor_generics (
rows: &mut [Self],
#executor_param,
) -> ::core::result::Result<(), #root::sql::ExecError>
#executor_where
{
if rows.is_empty() {
return ::core::result::Result::Ok(());
}
let _first_unset = matches!(
rows[0].#first_auto_ident,
#root::sql::Auto::Unset
);
#( #uniformity )*
let mut _all_rows: ::std::vec::Vec<
::std::vec::Vec<#root::core::SqlValue>,
> = ::std::vec::Vec::with_capacity(rows.len());
let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
for _row in rows.iter() {
let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes_no_auto )*
_all_rows.push(_row_vals);
}
::std::vec![ #( #cols_no_auto ),* ]
} else {
for _row in rows.iter() {
let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes_all )*
_all_rows.push(_row_vals);
}
::std::vec![ #( #cols_all ),* ]
};
let _query = #root::core::BulkInsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: _columns,
rows: _all_rows,
returning: ::std::vec![ #( #returning_cols ),* ],
on_conflict: ::core::option::Option::None,
};
let _returned = #root::sql::__macro_internals::bulk_insert_on(
#executor_passes_to_data_write,
&_query,
).await?;
if _returned.len() != rows.len() {
return ::core::result::Result::Err(
#root::sql::ExecError::Sql(
#root::sql::SqlError::BulkInsertReturningMismatch {
expected: rows.len(),
actual: _returned.len(),
}
)
);
}
for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
#auto_assigns_for_row
}
#audit_bulk_insert_emit
::core::result::Result::Ok(())
}
}
} else {
let cols_all = &fields.bulk_columns_all;
let pushes_all = &fields.bulk_pushes_all;
quote! {
#[cfg(feature = "postgres")]
pub async fn bulk_insert(
rows: &[Self],
pool: &#root::sql::sqlx::PgPool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
Self::bulk_insert_on(rows, pool).await
}
#[cfg(feature = "postgres")]
pub async fn bulk_insert_on<'_c, _E>(
rows: &[Self],
_executor: _E,
) -> ::core::result::Result<(), #root::sql::ExecError>
where
_E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
{
if rows.is_empty() {
return ::core::result::Result::Ok(());
}
let mut _all_rows: ::std::vec::Vec<
::std::vec::Vec<#root::core::SqlValue>,
> = ::std::vec::Vec::with_capacity(rows.len());
for _row in rows.iter() {
let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #pushes_all )*
_all_rows.push(_row_vals);
}
let _query = #root::core::BulkInsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: ::std::vec![ #( #cols_all ),* ],
rows: _all_rows,
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::None,
};
let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
::core::result::Result::Ok(())
}
}
};
let bulk_upsert_pool_method = {
let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
(
fields.bulk_columns_no_auto.clone(),
fields.bulk_pushes_no_auto.clone(),
)
} else {
(
fields.bulk_columns_all.clone(),
fields.bulk_pushes_all.clone(),
)
};
quote! {
pub async fn bulk_upsert_pool(
rows: &[Self],
target: &[&'static str],
update_cols: &[&'static str],
pool: &#root::sql::Pool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
if rows.is_empty() {
return ::core::result::Result::Ok(());
}
let mut _all_rows: ::std::vec::Vec<
::std::vec::Vec<#root::core::SqlValue>,
> = ::std::vec::Vec::with_capacity(rows.len());
for _row in rows.iter() {
let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #upsert_pushes )*
_all_rows.push(_row_vals);
}
let _query = #root::core::BulkInsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: ::std::vec![ #( #upsert_cols ),* ],
rows: _all_rows,
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::Some(
#root::core::ConflictClause::DoUpdate {
target: target.to_vec(),
update_columns: update_cols.to_vec(),
}
),
};
#root::sql::bulk_insert_pool(pool, &_query).await
}
pub async fn bulk_insert_or_ignore_pool(
rows: &[Self],
pool: &#root::sql::Pool,
) -> ::core::result::Result<(), #root::sql::ExecError> {
if rows.is_empty() {
return ::core::result::Result::Ok(());
}
let mut _all_rows: ::std::vec::Vec<
::std::vec::Vec<#root::core::SqlValue>,
> = ::std::vec::Vec::with_capacity(rows.len());
for _row in rows.iter() {
let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::new();
#( #upsert_pushes )*
_all_rows.push(_row_vals);
}
let _query = #root::core::BulkInsertQuery {
model: <Self as #root::core::Model>::SCHEMA,
columns: ::std::vec![ #( #upsert_cols ),* ],
rows: _all_rows,
returning: ::std::vec::Vec::new(),
on_conflict: ::core::option::Option::Some(
#root::core::ConflictClause::DoNothing
),
};
#root::sql::bulk_insert_pool(pool, &_query).await
}
}
};
let bulk_update_method = match &fields.primary_key {
None => quote! {},
Some((pk_ident, pk_col)) => {
let mut col_arms: Vec<TokenStream2> = Vec::new();
let mut val_arms: Vec<TokenStream2> = Vec::new();
for entry in &fields.column_entries {
if &entry.column == pk_col {
continue;
}
let col = &entry.column;
let ident = &entry.ident;
col_arms.push(quote! { #col => #col, });
val_arms.push(quote! {
#col => _row_vals.push(
::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&_o.#ident)
)
),
});
}
quote! {
pub async fn bulk_update(
objs: &[Self],
fields: &[&str],
pool: &#root::sql::Pool,
) -> ::core::result::Result<u64, #root::sql::ExecError> {
if objs.is_empty() || fields.is_empty() {
return ::core::result::Result::Ok(0);
}
let _model_name = <Self as #root::core::Model>::SCHEMA.name;
let mut _update_columns: ::std::vec::Vec<&'static str> =
::std::vec::Vec::with_capacity(fields.len());
for &_f in fields {
let _col: &'static str = match _f {
#pk_col => {
return ::core::result::Result::Err(
::core::convert::Into::into(
#root::core::QueryError::BulkUpdatePrimaryKey {
model: _model_name,
field: ::std::string::ToString::to_string(_f),
}
)
);
}
#( #col_arms )*
_ => {
return ::core::result::Result::Err(
::core::convert::Into::into(
#root::core::QueryError::UnknownField {
model: _model_name,
field: ::std::string::ToString::to_string(_f),
}
)
);
}
};
_update_columns.push(_col);
}
let mut _rows: ::std::vec::Vec<
::std::vec::Vec<#root::core::SqlValue>,
> = ::std::vec::Vec::with_capacity(objs.len());
for _o in objs.iter() {
let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
::std::vec::Vec::with_capacity(fields.len() + 1);
_row_vals.push(
::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&_o.#pk_ident)
)
);
for &_f in fields {
match _f {
#( #val_arms )*
_ => {}
}
}
_rows.push(_row_vals);
}
let _query = #root::core::BulkUpdateQuery {
model: <Self as #root::core::Model>::SCHEMA,
update_columns: _update_columns,
rows: _rows,
};
#root::sql::bulk_update_pool(pool, &_query).await
}
}
}
};
let pk_value_helper = primary_key.map(|(pk_ident, _)| {
quote! {
#[doc(hidden)]
pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
)
}
}
});
let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
quote! {
impl #root::sql::HasPkValue for #struct_name {
fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
::core::convert::Into::<#root::core::SqlValue>::into(
::core::clone::Clone::clone(&self.#pk_ident)
)
}
}
}
});
let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
let assign_auto_pk_pool_impl = {
let auto_assigns = &fields.auto_assigns;
let auto_assigns_sqlite: Vec<TokenStream2> = fields
.auto_field_idents
.iter()
.map(|(ident, column)| {
quote! {
self.#ident = #root::sql::try_get_returning_sqlite(
_returning_row, #column
)?;
}
})
.collect();
let generated_assigns: Vec<TokenStream2> = fields
.generated_field_idents
.iter()
.map(|(ident, column)| {
quote! {
self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
}
})
.collect();
let generated_assigns_sqlite: Vec<TokenStream2> = fields
.generated_field_idents
.iter()
.map(|(ident, column)| {
quote! {
self.#ident = #root::sql::try_get_returning_sqlite(
_returning_row, #column
)?;
}
})
.collect();
let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
let value_ty = fields
.first_auto_value_ty
.as_ref()
.expect("first_auto_value_ty set whenever first_auto_ident is");
quote! {
let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
::rustango_from_mysql_auto_id(_id)?;
self.#first = #root::sql::Auto::Set(_converted);
::core::result::Result::Ok(())
}
} else {
quote! {
let _ = _id;
::core::result::Result::Ok(())
}
};
quote! {
impl #root::sql::AssignAutoPkPool for #struct_name {
fn __rustango_assign_from_pg_row(
&mut self,
_returning_row: &#root::sql::PgReturningRow,
) -> ::core::result::Result<(), #root::sql::ExecError> {
#( #auto_assigns )*
#( #generated_assigns )*
::core::result::Result::Ok(())
}
fn __rustango_assign_from_mysql_id(
&mut self,
_id: i64,
) -> ::core::result::Result<(), #root::sql::ExecError> {
#mysql_body
}
fn __rustango_assign_from_sqlite_row(
&mut self,
_returning_row: &#root::sql::SqliteReturningRow,
) -> ::core::result::Result<(), #root::sql::ExecError> {
#( #auto_assigns_sqlite )*
#( #generated_assigns_sqlite )*
::core::result::Result::Ok(())
}
}
}
};
let from_aliased_row_inits = &fields.from_aliased_row_inits;
let aliased_row_helper = quote! {
#[doc(hidden)]
#[cfg(feature = "postgres")]
pub fn __rustango_from_aliased_row(
row: &#root::sql::sqlx::postgres::PgRow,
prefix: &str,
) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
::core::result::Result::Ok(Self {
#( #from_aliased_row_inits ),*
})
}
};
let aliased_row_helper_my = quote! {
#root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
#( #from_aliased_row_inits ),*
});
};
let aliased_row_helper_sqlite = quote! {
#root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
#( #from_aliased_row_inits ),*
});
};
let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
let load_related_impl_sqlite =
load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
let extra_manager_fns: Vec<TokenStream2> = manager_fns
.iter()
.map(|fn_ident| {
let model_name_str = struct_name.to_string();
let fn_name_str = fn_ident.to_string();
let doc = format!(
"Custom-named QuerySet accessor for [`{model_name_str}`]. \
Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
equivalent to `Self::objects()`. Chains with any \
`impl ... for QuerySet<{model_name_str}> {{ ... }}` \
extension methods."
);
quote! {
#[doc = #doc]
#[must_use]
pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
#root::query::QuerySet::new()
}
}
})
.collect();
quote! {
impl #struct_name {
#[must_use]
pub fn objects() -> #root::query::QuerySet<#struct_name> {
#root::query::QuerySet::new()
}
#[must_use]
pub fn query() -> #root::query::QuerySet<#struct_name> {
#root::query::QuerySet::new()
}
#( #extra_manager_fns )*
#insert_method
#bulk_insert_method
#bulk_upsert_pool_method
#bulk_update_method
#save_method
#pk_methods
#pk_value_helper
#aliased_row_helper
#column_consts
}
#aliased_row_helper_my
#aliased_row_helper_sqlite
#load_related_impl
#load_related_impl_my
#load_related_impl_sqlite
#has_pk_value_impl
#fk_pk_access_impl
#assign_auto_pk_pool_impl
}
}
fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
let root = rustango_root();
let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
let col_lit = column.as_str();
quote! {
_row_mut.#ident = #root::sql::sqlx::Row::try_get(
_returning_row,
#col_lit,
)?;
}
});
quote! { #( #lines )* }
}
fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
let lines = entries.iter().map(|e| {
let ident = &e.ident;
let col_ty = column_type_ident(ident);
quote! {
#[allow(non_upper_case_globals)]
pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
}
});
quote! { #(#lines)* }
}
fn column_module_tokens(
module_ident: &syn::Ident,
struct_name: &syn::Ident,
entries: &[ColumnEntry],
) -> TokenStream2 {
let root = rustango_root();
let items = entries.iter().map(|e| {
let col_ty = column_type_ident(&e.ident);
let value_ty = &e.value_ty;
let name = &e.name;
let column = &e.column;
let field_type_tokens = &e.field_type_tokens;
quote! {
#[derive(::core::clone::Clone, ::core::marker::Copy)]
pub struct #col_ty;
impl #root::core::Column for #col_ty {
type Model = super::#struct_name;
type Value = #value_ty;
const NAME: &'static str = #name;
const COLUMN: &'static str = #column;
const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
}
}
});
quote! {
#[doc(hidden)]
#[allow(non_camel_case_types, non_snake_case)]
pub mod #module_ident {
#[allow(unused_imports)]
use super::*;
#(#items)*
}
}
}
fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
}
fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
syn::Ident::new(
&format!("__rustango_cols_{struct_name}"),
struct_name.span(),
)
}
fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
let root = rustango_root();
quote! {
#[cfg(feature = "postgres")]
impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
for #struct_name
{
fn from_row(
row: &'r #root::sql::sqlx::postgres::PgRow,
) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
::core::result::Result::Ok(Self {
#( #from_row_inits ),*
})
}
}
#root::__impl_my_from_row!(#struct_name, |row| {
#( #from_row_inits ),*
});
#root::__impl_sqlite_from_row!(#struct_name, |row| {
#( #from_row_inits ),*
});
}
}
struct ContainerAttrs {
table: Option<String>,
display: Option<(String, proc_macro2::Span)>,
app: Option<String>,
admin: Option<AdminAttrs>,
audit: Option<AuditAttrs>,
permissions: bool,
m2m: Vec<M2MAttr>,
generic_m2m: Vec<GenericM2MAttr>,
indexes: Vec<IndexAttr>,
checks: Vec<CheckAttr>,
excludes: Vec<ExcludeAttr>,
composite_fks: Vec<CompositeFkAttr>,
generic_fks: Vec<GenericFkAttr>,
scope: Option<String>,
manager_ext: Option<syn::Ident>,
manager_fns: Vec<syn::Ident>,
default_order: Vec<(String, bool, proc_macro2::Span)>,
is_view: bool,
managed: bool,
base_manager_name: Option<String>,
order_with_respect_to: Option<String>,
proxy: bool,
required_db_features: Vec<String>,
required_db_vendor: Option<String>,
default_related_name: Option<String>,
db_table_comment: Option<String>,
get_latest_by: Option<(String, bool)>,
extra_permissions: Vec<(String, String)>,
default_permissions: Vec<String>,
verbose_name: Option<String>,
verbose_name_plural: Option<String>,
global_scopes: Vec<GlobalScopeAttr>,
through_relations: Vec<ThroughAttr>,
reverse_has_relations: Vec<ReverseHasAttr>,
generic_has_relations: Vec<GenericHasAttr>,
}
struct GlobalScopeAttr {
name: String,
apply: syn::Path,
}
struct ThroughAttr {
name: String,
far: syn::Ident,
far_fk_column: String,
intermediate: syn::Ident,
intermediate_fk_column: String,
intermediate_pk_column: String,
}
struct ReverseHasAttr {
name: String,
child: syn::Ident,
child_fk_column: String,
self_pk_column: String,
}
struct GenericHasAttr {
name: String,
child: syn::Ident,
ct_column: String,
pk_column: String,
self_pk_column: String,
}
struct IndexAttr {
name: Option<String>,
columns: Vec<String>,
unique: bool,
method: String,
where_clause: Option<String>,
include: Vec<String>,
}
struct CheckAttr {
name: String,
expr: String,
}
struct ExcludeAttr {
name: String,
using: String,
elements: Vec<(String, String)>,
where_clause: Option<String>,
}
struct CompositeFkAttr {
name: String,
to: String,
from: Vec<String>,
on: Vec<String>,
}
struct GenericFkAttr {
name: String,
ct_column: String,
pk_column: String,
}
struct M2MAttr {
name: String,
to: String,
through: String,
src: String,
dst: String,
auto_create: bool,
}
struct GenericM2MAttr {
name: String,
through: String,
pk_column: String,
ct_column: String,
related_column: String,
}
#[derive(Default)]
struct AuditAttrs {
track: Option<(Vec<String>, proc_macro2::Span)>,
}
#[derive(Default)]
struct AdminAttrs {
list_display: Option<(Vec<String>, proc_macro2::Span)>,
search_fields: Option<(Vec<String>, proc_macro2::Span)>,
list_per_page: Option<usize>,
ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
list_filter: Option<(Vec<String>, proc_macro2::Span)>,
actions: Option<(Vec<String>, proc_macro2::Span)>,
fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
search_help_text: Option<String>,
actions_on_top: Option<bool>,
actions_on_bottom: Option<bool>,
date_hierarchy: Option<String>,
prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
list_select_related: Option<String>,
formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
}
fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
let mut out = ContainerAttrs {
table: None,
display: None,
app: None,
admin: None,
audit: None,
permissions: true,
m2m: Vec::new(),
generic_m2m: Vec::new(),
indexes: Vec::new(),
checks: Vec::new(),
excludes: Vec::new(),
composite_fks: Vec::new(),
generic_fks: Vec::new(),
scope: None,
manager_ext: None,
manager_fns: Vec::new(),
default_order: Vec::new(),
is_view: false,
managed: true,
verbose_name: None,
verbose_name_plural: None,
base_manager_name: None,
order_with_respect_to: None,
proxy: false,
required_db_features: Vec::new(),
required_db_vendor: None,
default_related_name: None,
db_table_comment: None,
get_latest_by: None,
extra_permissions: Vec::new(),
default_permissions: Vec::new(),
global_scopes: Vec::new(),
through_relations: Vec::new(),
reverse_has_relations: Vec::new(),
generic_has_relations: Vec::new(),
};
for attr in &input.attrs {
if !attr.path().is_ident("rustango") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("table") {
let s: LitStr = meta.value()?.parse()?;
let name = s.value();
validate_table_name(&name, s.span())?;
out.table = Some(name);
return Ok(());
}
if meta.path.is_ident("display") {
let s: LitStr = meta.value()?.parse()?;
out.display = Some((s.value(), s.span()));
return Ok(());
}
if meta.path.is_ident("app") {
let s: LitStr = meta.value()?.parse()?;
out.app = Some(s.value());
return Ok(());
}
if meta.path.is_ident("scope") {
let s: LitStr = meta.value()?.parse()?;
let val = s.value();
if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
return Err(meta.error(format!(
"`scope` must be \"registry\" or \"tenant\", got {val:?}"
)));
}
out.scope = Some(val);
return Ok(());
}
if meta.path.is_ident("admin") {
let mut admin = AdminAttrs::default();
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("list_display") {
let s: LitStr = inner.value()?.parse()?;
admin.list_display =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("search_fields") {
let s: LitStr = inner.value()?.parse()?;
admin.search_fields =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("readonly_fields") {
let s: LitStr = inner.value()?.parse()?;
admin.readonly_fields =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("list_per_page") {
let lit: syn::LitInt = inner.value()?.parse()?;
admin.list_per_page = Some(lit.base10_parse::<usize>()?);
return Ok(());
}
if inner.path.is_ident("ordering") {
let s: LitStr = inner.value()?.parse()?;
admin.ordering = Some((
parse_ordering_list(&s.value()),
s.span(),
));
return Ok(());
}
if inner.path.is_ident("list_filter") {
let s: LitStr = inner.value()?.parse()?;
admin.list_filter =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("actions") {
let s: LitStr = inner.value()?.parse()?;
admin.actions =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("fieldsets") {
let s: LitStr = inner.value()?.parse()?;
admin.fieldsets =
Some((parse_fieldset_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("list_display_links") {
let s: LitStr = inner.value()?.parse()?;
admin.list_display_links =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("search_help_text") {
let s: LitStr = inner.value()?.parse()?;
admin.search_help_text = Some(s.value());
return Ok(());
}
if inner.path.is_ident("actions_on_top") {
let lit: syn::LitBool = inner.value()?.parse()?;
admin.actions_on_top = Some(lit.value);
return Ok(());
}
if inner.path.is_ident("actions_on_bottom") {
let lit: syn::LitBool = inner.value()?.parse()?;
admin.actions_on_bottom = Some(lit.value);
return Ok(());
}
if inner.path.is_ident("date_hierarchy") {
let s: LitStr = inner.value()?.parse()?;
admin.date_hierarchy = Some(s.value());
return Ok(());
}
if inner.path.is_ident("prepopulated_fields") {
let s: LitStr = inner.value()?.parse()?;
admin.prepopulated_fields =
Some((parse_prepopulated_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("raw_id_fields") {
let s: LitStr = inner.value()?.parse()?;
admin.raw_id_fields =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("autocomplete_fields") {
let s: LitStr = inner.value()?.parse()?;
admin.autocomplete_fields =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
if inner.path.is_ident("list_select_related") {
let s: LitStr = inner.value()?.parse()?;
admin.list_select_related = Some(s.value());
return Ok(());
}
if inner.path.is_ident("formfield_overrides") {
let s: LitStr = inner.value()?.parse()?;
admin.formfield_overrides =
Some((parse_formfield_overrides(&s.value()), s.span()));
return Ok(());
}
Err(inner.error(
"unknown admin attribute (supported: \
`list_display`, `list_display_links`, \
`search_fields`, `search_help_text`, \
`readonly_fields`, \
`list_filter`, `list_per_page`, `ordering`, `actions`, \
`actions_on_top`, `actions_on_bottom`, \
`date_hierarchy`, \
`prepopulated_fields`, \
`raw_id_fields`, \
`autocomplete_fields`, \
`list_select_related`, \
`formfield_overrides`, \
`fieldsets`)",
))
})?;
out.admin = Some(admin);
return Ok(());
}
if meta.path.is_ident("manager") {
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("ext") {
let s: LitStr = inner.value()?.parse()?;
let name = s.value();
if name.is_empty() {
return Err(inner.error("manager(ext = \"...\") cannot be empty"));
}
out.manager_ext =
Some(syn::Ident::new(&name, s.span()));
return Ok(());
}
Err(inner.error(
"unknown manager attribute (supported: `ext = \"TraitName\"`)",
))
})?;
return Ok(());
}
if meta.path.is_ident("manager_fn") {
let s: LitStr = meta.value()?.parse()?;
let name = s.value();
if name.is_empty() {
return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
}
if name == "objects" {
return Err(meta.error(
"`manager_fn = \"objects\"` collides with the default \
accessor — pick a different name",
));
}
let ident = syn::Ident::new(&name, s.span());
if out.manager_fns.iter().any(|prev| *prev == ident) {
return Err(meta.error(format!(
"duplicate `manager_fn = \"{name}\"`"
)));
}
out.manager_fns.push(ident);
return Ok(());
}
if meta.path.is_ident("default_order") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
let span = s.span();
let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
Vec::new();
for entry in raw.split(',') {
let trimmed = entry.trim();
if trimmed.is_empty() {
return Err(syn::Error::new(
span,
"`default_order = \"...\"` has an empty entry — \
check for a stray comma",
));
}
let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
(true, rest.trim().to_owned())
} else if let Some(rest) = trimmed.strip_prefix('+') {
(false, rest.trim().to_owned())
} else {
(false, trimmed.to_owned())
};
if name.is_empty() {
return Err(syn::Error::new(
span,
"`default_order` entry has no column name after the prefix",
));
}
if parsed.iter().any(|(n, _, _)| *n == name) {
return Err(syn::Error::new(
span,
format!("duplicate column `{name}` in `default_order`"),
));
}
parsed.push((name, desc, span));
}
if parsed.is_empty() {
return Err(syn::Error::new(
span,
"`default_order = \"...\"` cannot be empty",
));
}
out.default_order = parsed;
return Ok(());
}
if meta.path.is_ident("global_scope") {
let span = meta.path.span();
let mut scope_name: Option<String> = None;
let mut apply_path: Option<syn::Path> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
if raw.trim().is_empty() {
return Err(syn::Error::new(
s.span(),
"`global_scope(name = \"...\")` must not be empty",
));
}
scope_name = Some(raw);
return Ok(());
}
if inner.path.is_ident("apply") {
let p: syn::Path = inner.value()?.parse()?;
apply_path = Some(p);
return Ok(());
}
Err(inner.error(
"unknown `global_scope` attribute (supported: \
`name`, `apply`)",
))
})?;
let Some(name) = scope_name else {
return Err(syn::Error::new(
span,
"`global_scope` requires `name = \"...\"`",
));
};
let Some(apply) = apply_path else {
return Err(syn::Error::new(
span,
"`global_scope` requires `apply = fn_path`",
));
};
if out.global_scopes.iter().any(|s| s.name == name) {
return Err(syn::Error::new(
span,
format!(
"duplicate global scope name `{name}` — \
pick a unique identifier so \
`QuerySet::without_global_scope(\"{name}\")` \
is unambiguous"
),
));
}
out.global_scopes.push(GlobalScopeAttr { name, apply });
return Ok(());
}
if meta.path.is_ident("through") {
let span = meta.path.span();
let mut accessor_name: Option<String> = None;
let mut far_ident: Option<syn::Ident> = None;
let mut far_fk_column: Option<String> = None;
let mut intermediate_ident: Option<syn::Ident> = None;
let mut intermediate_fk_column: Option<String> = None;
let mut intermediate_pk_column: Option<String> = None;
fn parse_nonempty_string(
inner: &syn::meta::ParseNestedMeta<'_>,
field: &str,
) -> syn::Result<String> {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
format!("`through({field} = \"...\")` must not be empty"),
));
}
Ok(trimmed.to_owned())
}
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
accessor_name = Some(parse_nonempty_string(&inner, "name")?);
return Ok(());
}
if inner.path.is_ident("far") {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`through(far = \"...\")` must not be empty",
));
}
far_ident = Some(syn::Ident::new(trimmed, s.span()));
return Ok(());
}
if inner.path.is_ident("far_fk_column") {
far_fk_column =
Some(parse_nonempty_string(&inner, "far_fk_column")?);
return Ok(());
}
if inner.path.is_ident("intermediate") {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`through(intermediate = \"...\")` must not be empty",
));
}
intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
return Ok(());
}
if inner.path.is_ident("intermediate_fk_column") {
intermediate_fk_column =
Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
return Ok(());
}
if inner.path.is_ident("intermediate_pk_column") {
intermediate_pk_column =
Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
return Ok(());
}
Err(inner.error(
"unknown `through` attribute (supported: \
`name`, `far`, `far_fk_column`, \
`intermediate`, `intermediate_fk_column`, \
`intermediate_pk_column`)",
))
})?;
let Some(name) = accessor_name else {
return Err(syn::Error::new(
span,
"`through` requires `name = \"...\"`",
));
};
let Some(far) = far_ident else {
return Err(syn::Error::new(
span,
"`through` requires `far = \"FarModelType\"`",
));
};
let Some(far_fk_column) = far_fk_column else {
return Err(syn::Error::new(
span,
"`through` requires `far_fk_column = \"<column>\"`",
));
};
let Some(intermediate) = intermediate_ident else {
return Err(syn::Error::new(
span,
"`through` requires `intermediate = \"IntermediateModelType\"`",
));
};
let Some(intermediate_fk_column) = intermediate_fk_column else {
return Err(syn::Error::new(
span,
"`through` requires `intermediate_fk_column = \"<column>\"`",
));
};
let intermediate_pk_column =
intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
if out.through_relations.iter().any(|t| t.name == name) {
return Err(syn::Error::new(
span,
format!(
"duplicate `through(name = \"{name}\")` — \
pick a unique accessor name"
),
));
}
out.through_relations.push(ThroughAttr {
name,
far,
far_fk_column,
intermediate,
intermediate_fk_column,
intermediate_pk_column,
});
return Ok(());
}
if meta.path.is_ident("reverse_has") {
let span = meta.path.span();
let mut accessor_name: Option<String> = None;
let mut child_ident: Option<syn::Ident> = None;
let mut child_fk_column: Option<String> = None;
let mut self_pk_column: Option<String> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
if raw.trim().is_empty() {
return Err(syn::Error::new(
s.span(),
"`reverse_has(name = \"...\")` must not be empty",
));
}
accessor_name = Some(raw);
return Ok(());
}
if inner.path.is_ident("child") {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`reverse_has(child = \"...\")` must not be empty",
));
}
child_ident = Some(syn::Ident::new(trimmed, s.span()));
return Ok(());
}
if inner.path.is_ident("child_fk_column") {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`reverse_has(child_fk_column = \"...\")` must not be empty",
));
}
child_fk_column = Some(trimmed.to_owned());
return Ok(());
}
if inner.path.is_ident("self_pk_column") {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`reverse_has(self_pk_column = \"...\")` must not be empty",
));
}
self_pk_column = Some(trimmed.to_owned());
return Ok(());
}
Err(inner.error(
"unknown `reverse_has` attribute (supported: \
`name`, `child`, `child_fk_column`, \
`self_pk_column`)",
))
})?;
let Some(name) = accessor_name else {
return Err(syn::Error::new(
span,
"`reverse_has` requires `name = \"...\"`",
));
};
let Some(child) = child_ident else {
return Err(syn::Error::new(
span,
"`reverse_has` requires `child = \"ChildModelType\"`",
));
};
let Some(child_fk_column) = child_fk_column else {
return Err(syn::Error::new(
span,
"`reverse_has` requires `child_fk_column = \"<column>\"`",
));
};
let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
if out.reverse_has_relations.iter().any(|r| r.name == name) {
return Err(syn::Error::new(
span,
format!(
"duplicate `reverse_has(name = \"{name}\")` — \
pick a unique accessor name"
),
));
}
out.reverse_has_relations.push(ReverseHasAttr {
name,
child,
child_fk_column,
self_pk_column,
});
return Ok(());
}
if meta.path.is_ident("generic_has") {
let span = meta.path.span();
let mut accessor_name: Option<String> = None;
let mut child_ident: Option<syn::Ident> = None;
let mut ct_column: Option<String> = None;
let mut pk_column: Option<String> = None;
let mut self_pk_column: Option<String> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
let raw = s.value();
if raw.trim().is_empty() {
return Err(syn::Error::new(
s.span(),
"`generic_has(name = \"...\")` must not be empty",
));
}
accessor_name = Some(raw);
return Ok(());
}
if inner.path.is_ident("child") {
let s: LitStr = inner.value()?.parse()?;
let trimmed = s.value().trim().to_owned();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`generic_has(child = \"...\")` must not be empty",
));
}
child_ident = Some(syn::Ident::new(&trimmed, s.span()));
return Ok(());
}
if inner.path.is_ident("ct_column") {
let s: LitStr = inner.value()?.parse()?;
let trimmed = s.value().trim().to_owned();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`generic_has(ct_column = \"...\")` must not be empty",
));
}
ct_column = Some(trimmed);
return Ok(());
}
if inner.path.is_ident("pk_column") {
let s: LitStr = inner.value()?.parse()?;
let trimmed = s.value().trim().to_owned();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`generic_has(pk_column = \"...\")` must not be empty",
));
}
pk_column = Some(trimmed);
return Ok(());
}
if inner.path.is_ident("self_pk_column") {
let s: LitStr = inner.value()?.parse()?;
let trimmed = s.value().trim().to_owned();
if trimmed.is_empty() {
return Err(syn::Error::new(
s.span(),
"`generic_has(self_pk_column = \"...\")` must not be empty",
));
}
self_pk_column = Some(trimmed);
return Ok(());
}
Err(inner.error(
"unknown `generic_has` attribute (supported: \
`name`, `child`, `ct_column`, `pk_column`, \
`self_pk_column`)",
))
})?;
let Some(name) = accessor_name else {
return Err(syn::Error::new(
span,
"`generic_has` requires `name = \"...\"`",
));
};
let Some(child) = child_ident else {
return Err(syn::Error::new(
span,
"`generic_has` requires `child = \"ChildModelType\"`",
));
};
let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
if out.generic_has_relations.iter().any(|r| r.name == name) {
return Err(syn::Error::new(
span,
format!(
"duplicate `generic_has(name = \"{name}\")` — \
pick a unique accessor name"
),
));
}
out.generic_has_relations.push(GenericHasAttr {
name,
child,
ct_column,
pk_column,
self_pk_column,
});
return Ok(());
}
if meta.path.is_ident("audit") {
let mut audit = AuditAttrs::default();
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("track") {
let s: LitStr = inner.value()?.parse()?;
audit.track =
Some((split_field_list(&s.value()), s.span()));
return Ok(());
}
Err(inner.error(
"unknown audit attribute (supported: `track`)",
))
})?;
out.audit = Some(audit);
return Ok(());
}
if meta.path.is_ident("permissions") {
if let Ok(v) = meta.value() {
let lit: syn::LitBool = v.parse()?;
out.permissions = lit.value;
} else {
out.permissions = true;
}
return Ok(());
}
if meta.path.is_ident("view") {
if let Ok(v) = meta.value() {
let lit: syn::LitBool = v.parse()?;
out.is_view = lit.value;
} else {
out.is_view = true;
}
return Ok(());
}
if meta.path.is_ident("managed") {
let v = meta.value()?;
let lit: syn::LitBool = v.parse()?;
out.managed = lit.value;
return Ok(());
}
if meta.path.is_ident("verbose_name") {
let s: LitStr = meta.value()?.parse()?;
out.verbose_name = Some(s.value());
return Ok(());
}
if meta.path.is_ident("verbose_name_plural") {
let s: LitStr = meta.value()?.parse()?;
out.verbose_name_plural = Some(s.value());
return Ok(());
}
if meta.path.is_ident("db_table_comment") {
let s: LitStr = meta.value()?.parse()?;
out.db_table_comment = Some(s.value());
return Ok(());
}
if meta.path.is_ident("proxy") {
let value = if meta.input.peek(syn::Token![=]) {
meta.value()?.parse::<syn::LitBool>()?.value
} else {
true
};
out.proxy = value;
return Ok(());
}
if meta.path.is_ident("order_with_respect_to") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
if raw.is_empty() {
return Err(syn::Error::new(
s.span(),
"`order_with_respect_to` must be a non-empty FK field name",
));
}
let valid = raw
.chars()
.all(|c| c == '_' || c.is_ascii_alphanumeric())
&& !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
if !valid {
return Err(syn::Error::new(
s.span(),
format!(
"`order_with_respect_to` must be a valid Rust \
identifier (letters / digits / underscores, \
not starting with a digit); got `{raw}`"
),
));
}
out.order_with_respect_to = Some(raw);
return Ok(());
}
if meta.path.is_ident("required_db_features") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
let features: Vec<String> = raw
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
if features.is_empty() {
return Err(syn::Error::new(
s.span(),
"`required_db_features` must list at least one \
comma-separated capability token",
));
}
out.required_db_features = features;
return Ok(());
}
if meta.path.is_ident("required_db_vendor") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value().to_ascii_lowercase();
match raw.as_str() {
"postgresql" | "postgres" | "pg" => {
out.required_db_vendor = Some("postgres".to_owned());
}
"mysql" | "mariadb" => {
out.required_db_vendor = Some("mysql".to_owned());
}
"sqlite" | "sqlite3" => {
out.required_db_vendor = Some("sqlite".to_owned());
}
_ => {
return Err(syn::Error::new(
s.span(),
format!(
"unknown required_db_vendor `{raw}` — \
expected `postgres` (aliases: `postgresql`, `pg`), \
`mysql` (alias: `mariadb`), or `sqlite` \
(alias: `sqlite3`)"
),
));
}
}
return Ok(());
}
if meta.path.is_ident("base_manager_name") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
if raw.is_empty() {
return Err(syn::Error::new(
s.span(),
"`base_manager_name` must be a non-empty string",
));
}
let valid = raw
.chars()
.all(|c| c == '_' || c.is_ascii_alphanumeric())
&& !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
if !valid {
return Err(syn::Error::new(
s.span(),
format!(
"`base_manager_name` must be a valid Rust \
identifier (letters / digits / underscores, \
not starting with a digit); got `{raw}`"
),
));
}
out.base_manager_name = Some(raw);
return Ok(());
}
if meta.path.is_ident("default_related_name") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
if raw.is_empty() {
return Err(syn::Error::new(
s.span(),
"`default_related_name` must be a non-empty string",
));
}
let valid = raw
.chars()
.all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
&& !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
if !valid {
return Err(syn::Error::new(
s.span(),
format!(
"`default_related_name` must be snake_case ASCII \
(lowercase letters / digits / underscores, not \
starting with a digit); got `{raw}`"
),
));
}
out.default_related_name = Some(raw);
return Ok(());
}
if meta.path.is_ident("extra_permissions") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
let mut pairs = Vec::new();
for entry in raw.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let (codename, label) = match entry.split_once(':') {
Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
None => (entry.to_owned(), entry.to_owned()),
};
if codename.is_empty() {
return Err(meta.error(
"`extra_permissions` entries must be `codename:label` pairs",
));
}
pairs.push((codename, label));
}
if pairs.is_empty() {
return Err(meta
.error("`extra_permissions = \"…\"` must list at least one pair"));
}
out.extra_permissions = pairs;
return Ok(());
}
if meta.path.is_ident("default_permissions") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
let mut actions: Vec<String> = Vec::new();
for entry in raw.split(',') {
let action = entry.trim().to_ascii_lowercase();
if action.is_empty() {
continue;
}
match action.as_str() {
"add" | "change" | "delete" | "view" => {}
other => {
return Err(syn::Error::new(
s.span(),
format!(
"unknown default_permissions action `{other}` — \
expected one of `add`, `change`, `delete`, `view`"
),
));
}
}
if !actions.contains(&action) {
actions.push(action);
}
}
if actions.is_empty() {
return Err(syn::Error::new(
s.span(),
"`default_permissions = \"…\"` must list at least one action; \
use `permissions = false` on the container if you want NO \
permissions seeded for this model.",
));
}
out.default_permissions = actions;
return Ok(());
}
if meta.path.is_ident("get_latest_by") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(meta.error("`get_latest_by` must name a column"));
}
let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
(stripped.to_owned(), true)
} else if let Some(stripped) = trimmed.strip_prefix('+') {
(stripped.to_owned(), false)
} else {
(trimmed.to_owned(), false)
};
if col.is_empty() {
return Err(meta.error("`get_latest_by` must name a column"));
}
out.get_latest_by = Some((col, desc));
return Ok(());
}
if meta.path.is_ident("unique_together") {
let (columns, name) = parse_together_attr(&meta, "unique_together")?;
out.indexes.push(IndexAttr {
name,
columns,
unique: true,
method: "btree".to_owned(),
where_clause: None,
include: Vec::new(),
});
return Ok(());
}
if meta.path.is_ident("index_together") {
let (columns, name) = parse_together_attr(&meta, "index_together")?;
out.indexes.push(IndexAttr {
name,
columns,
unique: false,
method: "btree".to_owned(),
where_clause: None,
include: Vec::new(),
});
return Ok(());
}
if meta.path.is_ident("unique_when") {
let mut columns: Option<Vec<String>> = None;
let mut condition: Option<String> = None;
let mut name: Option<String> = None;
let mut include: Vec<String> = Vec::new();
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("columns") {
let s: LitStr = inner.value()?.parse()?;
columns = Some(split_field_list(&s.value()));
return Ok(());
}
if inner.path.is_ident("condition") {
let s: LitStr = inner.value()?.parse()?;
condition = Some(s.value());
return Ok(());
}
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
name = Some(s.value());
return Ok(());
}
if inner.path.is_ident("include") {
let s: LitStr = inner.value()?.parse()?;
include = split_field_list(&s.value());
return Ok(());
}
Err(inner.error(
"unknown unique_when attribute (supported: \
`columns = \"...\"`, `condition = \"...\"`, \
`name = \"...\"`, `include = \"...\"`)",
))
})?;
let columns = columns.ok_or_else(|| {
meta.error("`unique_when(...)` requires `columns = \"...\"`")
})?;
let condition = condition.ok_or_else(|| {
meta.error("`unique_when(...)` requires `condition = \"...\"`")
})?;
if columns.is_empty() {
return Err(meta.error("`unique_when(columns = \"\")` is empty"));
}
out.indexes.push(IndexAttr {
name,
columns,
unique: true,
method: "btree".to_owned(),
where_clause: Some(condition),
include,
});
return Ok(());
}
if meta.path.is_ident("index_when") {
let mut columns: Option<Vec<String>> = None;
let mut condition: Option<String> = None;
let mut name: Option<String> = None;
let mut method: String = "btree".to_owned();
let mut include: Vec<String> = Vec::new();
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("columns") {
let s: LitStr = inner.value()?.parse()?;
columns = Some(split_field_list(&s.value()));
return Ok(());
}
if inner.path.is_ident("condition") {
let s: LitStr = inner.value()?.parse()?;
condition = Some(s.value());
return Ok(());
}
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
name = Some(s.value());
return Ok(());
}
if inner.path.is_ident("method") {
let s: LitStr = inner.value()?.parse()?;
method = s.value();
return Ok(());
}
if inner.path.is_ident("include") {
let s: LitStr = inner.value()?.parse()?;
include = split_field_list(&s.value());
return Ok(());
}
Err(inner.error(
"unknown index_when attribute (supported: \
`columns = \"...\"`, `condition = \"...\"`, \
`name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
`include = \"...\"`)",
))
})?;
let columns = columns
.ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
let condition = condition.ok_or_else(|| {
meta.error("`index_when(...)` requires `condition = \"...\"`")
})?;
if columns.is_empty() {
return Err(meta.error("`index_when(columns = \"\")` is empty"));
}
out.indexes.push(IndexAttr {
name,
columns,
unique: false,
method,
where_clause: Some(condition),
include,
});
return Ok(());
}
if meta.path.is_ident("index") {
let cols_lit: LitStr;
let mut unique = false;
let mut name: Option<String> = None;
let mut method = "btree".to_owned();
if meta.input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
cols_lit = content.parse()?;
while content.peek(syn::Token![,]) {
content.parse::<syn::Token![,]>()?;
if content.is_empty() {
break;
}
let flag: syn::Ident = content.parse()?;
if flag == "unique" {
unique = true;
} else if flag == "name" {
content.parse::<syn::Token![=]>()?;
let s: LitStr = content.parse()?;
name = Some(s.value());
} else if flag == "method" {
content.parse::<syn::Token![=]>()?;
let s: LitStr = content.parse()?;
let v = s.value();
match v.as_str() {
"btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
| "bloom" => method = v,
other => {
return Err(syn::Error::new(
s.span(),
format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
));
}
}
} else {
return Err(syn::Error::new(
flag.span(),
"unknown index flag (supported: `unique`, `name`, `method`)",
));
}
}
} else {
cols_lit = meta.value()?.parse()?;
}
let columns = split_field_list(&cols_lit.value());
out.indexes.push(IndexAttr {
name,
columns,
unique,
method,
where_clause: None,
include: Vec::new(),
});
return Ok(());
}
if meta.path.is_ident("check") {
let mut name: Option<String> = None;
let mut expr: Option<String> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
name = Some(s.value());
return Ok(());
}
if inner.path.is_ident("expr") {
let s: LitStr = inner.value()?.parse()?;
expr = Some(s.value());
return Ok(());
}
Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
})?;
let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
out.checks.push(CheckAttr { name, expr });
return Ok(());
}
if meta.path.is_ident("exclude") {
let mut name: Option<String> = None;
let mut using: Option<String> = None;
let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
let mut where_clause: Option<String> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
name = Some(s.value());
return Ok(());
}
if inner.path.is_ident("using") {
let s: LitStr = inner.value()?.parse()?;
using = Some(s.value());
return Ok(());
}
if inner.path.is_ident("elements") {
let s: LitStr = inner.value()?.parse()?;
elements_raw = Some((s.value(), s.span()));
return Ok(());
}
if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
let s: LitStr = inner.value()?.parse()?;
where_clause = Some(s.value());
return Ok(());
}
Err(inner.error(
"unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
))
})?;
let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
let using = using.unwrap_or_else(|| "gist".to_owned());
let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
meta.error(
"exclude requires `elements = \"col WITH op, col WITH op\"`",
)
})?;
let mut elements: Vec<(String, String)> = Vec::new();
for pair in elements_str.split(',') {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
let mut split = pair.splitn(2, |c: char| c.is_whitespace());
let col = split.next().unwrap_or("").trim();
let rest = split.next().unwrap_or("").trim();
let rest_lc = rest.to_ascii_lowercase();
let op = rest_lc
.strip_prefix("with")
.map(|r| r.trim_start())
.filter(|r| !r.is_empty())
.map(|_| {
rest[4..].trim_start().to_owned()
});
let Some(op) = op else {
return Err(syn::Error::new(
elements_span,
format!(
"exclude elements: `{pair}` must be `<col> WITH <op>` \
(e.g. `room_id WITH =` or `during WITH &&`)"
),
));
};
if col.is_empty() || op.is_empty() {
return Err(syn::Error::new(
elements_span,
format!(
"exclude elements: `{pair}` must be `<col> WITH <op>` \
(both sides non-empty)"
),
));
}
elements.push((col.to_owned(), op));
}
if elements.is_empty() {
return Err(syn::Error::new(
elements_span,
"exclude requires at least one `col WITH op` element",
));
}
out.excludes.push(ExcludeAttr {
name,
using,
elements,
where_clause,
});
return Ok(());
}
if meta.path.is_ident("generic_fk") {
let mut gfk = GenericFkAttr {
name: String::new(),
ct_column: String::new(),
pk_column: String::new(),
};
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
gfk.name = s.value();
return Ok(());
}
if inner.path.is_ident("ct_column") {
let s: LitStr = inner.value()?.parse()?;
gfk.ct_column = s.value();
return Ok(());
}
if inner.path.is_ident("pk_column") {
let s: LitStr = inner.value()?.parse()?;
gfk.pk_column = s.value();
return Ok(());
}
Err(inner.error(
"unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
))
})?;
if gfk.name.is_empty() {
return Err(meta.error("generic_fk requires `name = \"...\"`"));
}
if gfk.ct_column.is_empty() {
return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
}
if gfk.pk_column.is_empty() {
return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
}
out.generic_fks.push(gfk);
return Ok(());
}
if meta.path.is_ident("fk_composite") {
let mut fk = CompositeFkAttr {
name: String::new(),
to: String::new(),
from: Vec::new(),
on: Vec::new(),
};
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
fk.name = s.value();
return Ok(());
}
if inner.path.is_ident("to") {
let s: LitStr = inner.value()?.parse()?;
fk.to = s.value();
return Ok(());
}
if inner.path.is_ident("on") || inner.path.is_ident("from") {
let value = inner.value()?;
let content;
syn::parenthesized!(content in value);
let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
content.parse_terminated(
|p| p.parse::<syn::LitStr>(),
syn::Token![,],
)?;
let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
if inner.path.is_ident("on") {
fk.on = cols;
} else {
fk.from = cols;
}
return Ok(());
}
Err(inner.error(
"unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
))
})?;
if fk.name.is_empty() {
return Err(meta.error("fk_composite requires `name = \"...\"`"));
}
if fk.to.is_empty() {
return Err(meta.error("fk_composite requires `to = \"...\"`"));
}
if fk.from.is_empty() || fk.on.is_empty() {
return Err(meta.error(
"fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
));
}
if fk.from.len() != fk.on.len() {
return Err(meta.error(format!(
"fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
fk.from.len(),
fk.on.len(),
)));
}
out.composite_fks.push(fk);
return Ok(());
}
if meta.path.is_ident("m2m") {
let mut m2m = M2MAttr {
name: String::new(),
to: String::new(),
through: String::new(),
src: String::new(),
dst: String::new(),
auto_create: true,
};
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
m2m.name = s.value();
return Ok(());
}
if inner.path.is_ident("to") {
let s: LitStr = inner.value()?.parse()?;
m2m.to = s.value();
return Ok(());
}
if inner.path.is_ident("through") {
let s: LitStr = inner.value()?.parse()?;
m2m.through = s.value();
return Ok(());
}
if inner.path.is_ident("src") {
let s: LitStr = inner.value()?.parse()?;
m2m.src = s.value();
return Ok(());
}
if inner.path.is_ident("dst") {
let s: LitStr = inner.value()?.parse()?;
m2m.dst = s.value();
return Ok(());
}
if inner.path.is_ident("auto_create") {
let lit: syn::LitBool = inner.value()?.parse()?;
m2m.auto_create = lit.value;
return Ok(());
}
Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
})?;
if m2m.name.is_empty() {
return Err(meta.error("m2m requires `name = \"...\"`"));
}
if m2m.to.is_empty() {
return Err(meta.error("m2m requires `to = \"...\"`"));
}
if m2m.through.is_empty() {
return Err(meta.error("m2m requires `through = \"...\"`"));
}
if m2m.src.is_empty() {
return Err(meta.error("m2m requires `src = \"...\"`"));
}
if m2m.dst.is_empty() {
return Err(meta.error("m2m requires `dst = \"...\"`"));
}
out.m2m.push(m2m);
return Ok(());
}
if meta.path.is_ident("generic_m2m") {
let mut gm = GenericM2MAttr {
name: String::new(),
through: String::new(),
pk_column: String::new(),
ct_column: String::new(),
related_column: String::new(),
};
meta.parse_nested_meta(|inner| {
let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
let s: LitStr = inner.value()?.parse()?;
Ok(s.value())
};
if inner.path.is_ident("name") {
gm.name = field(&inner)?;
return Ok(());
}
if inner.path.is_ident("through") {
gm.through = field(&inner)?;
return Ok(());
}
if inner.path.is_ident("pk_column") {
gm.pk_column = field(&inner)?;
return Ok(());
}
if inner.path.is_ident("ct_column") {
gm.ct_column = field(&inner)?;
return Ok(());
}
if inner.path.is_ident("related_column") {
gm.related_column = field(&inner)?;
return Ok(());
}
Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
})?;
for (val, label) in [
(&gm.name, "name"),
(&gm.through, "through"),
(&gm.pk_column, "pk_column"),
(&gm.ct_column, "ct_column"),
(&gm.related_column, "related_column"),
] {
if val.is_empty() {
return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
}
}
out.generic_m2m.push(gm);
return Ok(());
}
Err(meta.error("unknown rustango container attribute"))
})?;
}
Ok(out)
}
fn split_field_list(raw: &str) -> Vec<String> {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn parse_together_attr(
meta: &syn::meta::ParseNestedMeta<'_>,
attr: &str,
) -> syn::Result<(Vec<String>, Option<String>)> {
if meta.input.peek(syn::Token![=]) {
let cols_lit: LitStr = meta.value()?.parse()?;
let columns = split_field_list(&cols_lit.value());
check_together_columns(meta, attr, &columns)?;
return Ok((columns, None));
}
let mut columns: Option<Vec<String>> = None;
let mut name: Option<String> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("columns") {
let s: LitStr = inner.value()?.parse()?;
columns = Some(split_field_list(&s.value()));
return Ok(());
}
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
name = Some(s.value());
return Ok(());
}
Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
})?;
let columns = columns.ok_or_else(|| {
meta.error(format!(
"{attr}(...) requires a `columns = \"col1, col2\"` argument",
))
})?;
check_together_columns(meta, attr, &columns)?;
Ok((columns, name))
}
fn check_together_columns(
meta: &syn::meta::ParseNestedMeta<'_>,
attr: &str,
columns: &[String],
) -> syn::Result<()> {
if columns.len() < 2 {
let single = if attr == "unique_together" {
"#[rustango(unique)] on the field"
} else {
"#[rustango(index)] on the field"
};
return Err(meta.error(format!(
"{attr} expects two or more columns; for a single-column equivalent use {single}",
)));
}
Ok(())
}
fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
raw.split('|')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|section| {
let (title, rest) = match section.split_once(':') {
Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
_ => (String::new(), section),
};
let fields = split_field_list(rest);
(title, fields)
})
.collect()
}
fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.filter_map(|entry| {
let (target, sources_raw) = entry.split_once(':')?;
let target = target.trim().to_owned();
if target.is_empty() {
return None;
}
let sources: Vec<String> = sources_raw
.split('+')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect();
if sources.is_empty() {
return None;
}
Some((target, sources))
})
.collect()
}
fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.filter_map(|entry| {
let (field, widget) = entry.split_once(':')?;
let field = field.trim().to_owned();
let widget = widget.trim().to_owned();
if field.is_empty() || widget.is_empty() {
return None;
}
Some((field, widget))
})
.collect()
}
fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|spec| {
spec.strip_prefix('-')
.map_or((spec.to_owned(), false), |rest| {
(rest.trim().to_owned(), true)
})
})
.collect()
}
struct FieldAttrs {
column: Option<String>,
primary_key: bool,
fk: Option<String>,
o2o: Option<String>,
on: Option<String>,
on_delete: Option<String>,
related_name: Option<String>,
max_length: Option<u32>,
vector_dims: Option<u32>,
geometry_srid: Option<u32>,
min: Option<i64>,
max: Option<i64>,
default: Option<String>,
auto_uuid: bool,
default_uuid_v7: bool,
auto_now_add: bool,
auto_now: bool,
soft_delete: bool,
unique: bool,
index: bool,
index_unique: bool,
index_name: Option<String>,
index_method: String,
generated_as: Option<String>,
help_text: Option<String>,
choices: Option<Vec<(String, String)>>,
db_comment: Option<String>,
verbose_name: Option<String>,
editable: bool,
blank: bool,
case_insensitive: bool,
validators: Vec<String>,
}
fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
let mut out = FieldAttrs {
column: None,
primary_key: false,
fk: None,
o2o: None,
on: None,
on_delete: None,
related_name: None,
max_length: None,
vector_dims: None,
geometry_srid: None,
min: None,
max: None,
default: None,
auto_uuid: false,
default_uuid_v7: false,
auto_now_add: false,
auto_now: false,
soft_delete: false,
unique: false,
index: false,
index_unique: false,
index_name: None,
index_method: "btree".to_owned(),
generated_as: None,
help_text: None,
choices: None,
db_comment: None,
verbose_name: None,
editable: true,
blank: false,
case_insensitive: false,
validators: Vec::new(),
};
for attr in &field.attrs {
if !attr.path().is_ident("rustango") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("column") {
let s: LitStr = meta.value()?.parse()?;
let name = s.value();
validate_sql_identifier(&name, "column", s.span())?;
out.column = Some(name);
return Ok(());
}
if meta.path.is_ident("primary_key") {
out.primary_key = true;
return Ok(());
}
if meta.path.is_ident("fk") {
let s: LitStr = meta.value()?.parse()?;
out.fk = Some(s.value());
return Ok(());
}
if meta.path.is_ident("o2o") {
let s: LitStr = meta.value()?.parse()?;
out.o2o = Some(s.value());
return Ok(());
}
if meta.path.is_ident("on") {
let s: LitStr = meta.value()?.parse()?;
out.on = Some(s.value());
return Ok(());
}
if meta.path.is_ident("on_delete") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
let normalized = raw.trim().to_ascii_lowercase();
match normalized.as_str() {
"cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
_ => {
return Err(syn::Error::new(
s.span(),
format!(
"unknown on_delete action `{raw}`; expected one of \
`cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
),
));
}
}
out.on_delete = Some(normalized);
return Ok(());
}
if meta.path.is_ident("related_name") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
if raw.trim().is_empty() {
return Err(syn::Error::new(
s.span(),
"`related_name` must be a non-empty identifier",
));
}
if !raw
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
|| raw.starts_with(char::is_numeric)
{
return Err(syn::Error::new(
s.span(),
"`related_name` must be snake_case ASCII (lowercase letters, \
digits, underscores; no leading digit)",
));
}
out.related_name = Some(raw);
return Ok(());
}
if meta.path.is_ident("max_length") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.max_length = Some(lit.base10_parse::<u32>()?);
return Ok(());
}
if meta.path.is_ident("vector") {
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("dims") {
let lit: syn::LitInt = inner.value()?.parse()?;
out.vector_dims = Some(lit.base10_parse::<u32>()?);
return Ok(());
}
Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
})?;
return Ok(());
}
if meta.path.is_ident("geometry") {
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("srid") {
let lit: syn::LitInt = inner.value()?.parse()?;
out.geometry_srid = Some(lit.base10_parse::<u32>()?);
return Ok(());
}
Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
})?;
return Ok(());
}
if meta.path.is_ident("min") {
out.min = Some(parse_signed_i64(&meta)?);
return Ok(());
}
if meta.path.is_ident("max") {
out.max = Some(parse_signed_i64(&meta)?);
return Ok(());
}
if meta.path.is_ident("default") {
let s: LitStr = meta.value()?.parse()?;
out.default = Some(s.value());
return Ok(());
}
if meta.path.is_ident("generated_as") {
let s: LitStr = meta.value()?.parse()?;
out.generated_as = Some(s.value());
return Ok(());
}
if meta.path.is_ident("help_text") {
let s: LitStr = meta.value()?.parse()?;
out.help_text = Some(s.value());
return Ok(());
}
if meta.path.is_ident("choices") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
let mut pairs: Vec<(String, String)> = Vec::new();
for chunk in raw.split(',') {
let chunk = chunk.trim();
if chunk.is_empty() {
continue;
}
let (value, label) = match chunk.split_once(':') {
Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
None => (chunk.to_owned(), chunk.to_owned()),
};
if value.is_empty() {
return Err(syn::Error::new(
s.span(),
"`choices` entry has empty value before `:`",
));
}
pairs.push((value, label));
}
if pairs.is_empty() {
return Err(syn::Error::new(
s.span(),
"`choices = \"…\"` must contain at least one value",
));
}
out.choices = Some(pairs);
return Ok(());
}
if meta.path.is_ident("db_comment") {
let s: LitStr = meta.value()?.parse()?;
out.db_comment = Some(s.value());
return Ok(());
}
if meta.path.is_ident("verbose_name") {
let s: LitStr = meta.value()?.parse()?;
out.verbose_name = Some(s.value());
return Ok(());
}
if meta.path.is_ident("editable") {
if let Ok(v) = meta.value() {
let lit: syn::LitBool = v.parse()?;
out.editable = lit.value;
} else {
out.editable = true;
}
return Ok(());
}
if meta.path.is_ident("blank") {
if let Ok(v) = meta.value() {
let lit: syn::LitBool = v.parse()?;
out.blank = lit.value;
} else {
out.blank = true;
}
return Ok(());
}
if meta.path.is_ident("citext") {
if let Ok(v) = meta.value() {
let lit: syn::LitBool = v.parse()?;
out.case_insensitive = lit.value;
} else {
out.case_insensitive = true;
}
return Ok(());
}
if meta.path.is_ident("validators") {
let s: LitStr = meta.value()?.parse()?;
let raw = s.value();
out.validators = raw
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
if out.validators.is_empty() {
return Err(syn::Error::new(
s.span(),
"`validators = \"…\"` must list at least one name",
));
}
return Ok(());
}
if meta.path.is_ident("auto_uuid") {
out.auto_uuid = true;
out.primary_key = true;
if out.default.is_none() {
out.default = Some("gen_random_uuid()".into());
}
return Ok(());
}
if meta.path.is_ident("default_uuid_v7") {
out.default_uuid_v7 = true;
out.primary_key = true;
return Ok(());
}
if meta.path.is_ident("auto_now_add") {
out.auto_now_add = true;
if out.default.is_none() {
out.default = Some("now()".into());
}
return Ok(());
}
if meta.path.is_ident("auto_now") {
out.auto_now = true;
if out.default.is_none() {
out.default = Some("now()".into());
}
return Ok(());
}
if meta.path.is_ident("soft_delete") {
out.soft_delete = true;
return Ok(());
}
if meta.path.is_ident("unique") {
out.unique = true;
return Ok(());
}
if meta.path.is_ident("index") {
out.index = true;
if meta.input.peek(syn::token::Paren) {
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("unique") {
out.index_unique = true;
return Ok(());
}
if inner.path.is_ident("name") {
let s: LitStr = inner.value()?.parse()?;
out.index_name = Some(s.value());
return Ok(());
}
if inner.path.is_ident("method") {
let s: LitStr = inner.value()?.parse()?;
let v = s.value();
match v.as_str() {
"btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
out.index_method = v;
}
other => {
return Err(inner.error(format!(
"unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
)));
}
}
return Ok(());
}
Err(inner.error(
"unknown index sub-attribute (supported: `unique`, `name`, `method`)",
))
})?;
}
return Ok(());
}
Err(meta.error("unknown rustango field attribute"))
})?;
}
Ok(out)
}
fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
let expr: syn::Expr = meta.value()?.parse()?;
match expr {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) => lit.base10_parse::<i64>(),
syn::Expr::Unary(syn::ExprUnary {
op: syn::UnOp::Neg(_),
expr,
..
}) => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) = *expr
{
let v: i64 = lit.base10_parse()?;
Ok(-v)
} else {
Err(syn::Error::new_spanned(expr, "expected integer literal"))
}
}
other => Err(syn::Error::new_spanned(
other,
"expected integer literal (signed)",
)),
}
}
struct FieldInfo<'a> {
ident: &'a syn::Ident,
column: String,
primary_key: bool,
auto: bool,
value_ty: &'a Type,
field_type_tokens: TokenStream2,
schema: TokenStream2,
from_row_init: TokenStream2,
from_aliased_row_init: TokenStream2,
fk_inner: Option<Type>,
fk_pk_kind: DetectedKind,
nullable: bool,
auto_now: bool,
auto_now_add: bool,
soft_delete: bool,
generated_as: Option<String>,
default_uuid_v7: bool,
related_name: Option<String>,
}
fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
validate_sql_identifier(name, "table", span)
}
fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
if name.is_empty() {
return Err(syn::Error::new(
span,
format!("`{kind} = \"\"` is not a valid SQL identifier"),
));
}
let mut chars = name.chars();
let first = chars.next().unwrap();
if !(first.is_ascii_alphabetic() || first == '_') {
return Err(syn::Error::new(
span,
format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
));
}
for c in chars {
if !(c.is_ascii_alphanumeric() || c == '_') {
return Err(syn::Error::new(
span,
format!(
"{kind} name `{name}` contains invalid character {c:?} — \
SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
Hyphens in particular break FK / index name derivation \
downstream; use underscores instead (e.g. `{}`)",
name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
),
));
}
}
Ok(())
}
fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
let root = rustango_root();
let attrs = parse_field_attrs(field)?;
let ident = field
.ident
.as_ref()
.ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
let name = ident.to_string();
let column = attrs.column.clone().unwrap_or_else(|| name.clone());
let primary_key = attrs.primary_key;
let DetectedType {
kind,
nullable,
auto: detected_auto,
fk_inner,
} = detect_type(&field.ty)?;
check_bound_compatibility(field, &attrs, kind)?;
let auto = detected_auto;
if attrs.auto_uuid {
if kind != DetectedKind::Uuid {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(auto_uuid)]` requires the field type to be \
`Auto<uuid::Uuid>`",
));
}
if !detected_auto {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(auto_uuid)]` requires the field type to be \
wrapped in `Auto<...>` so the macro skips the column on \
INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
));
}
}
if attrs.default_uuid_v7 {
if kind != DetectedKind::Uuid {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(default_uuid_v7)]` requires the field type to be \
`Auto<uuid::Uuid>`",
));
}
if !detected_auto {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(default_uuid_v7)]` requires the field type to be \
wrapped in `Auto<...>` so the macro can detect the \
unset-vs-set state and fill a fresh UUIDv7 before INSERT",
));
}
if attrs.auto_uuid {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(default_uuid_v7)]` is mutually exclusive with \
`#[rustango(auto_uuid)]` — the former generates the UUID \
Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
Pick one.",
));
}
}
if attrs.auto_now_add || attrs.auto_now {
if kind != DetectedKind::DateTime {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
));
}
if !detected_auto {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
the field type to be wrapped in `Auto<...>` so the macro skips \
the column on INSERT and the DB DEFAULT (`now()`) fires",
));
}
}
if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(soft_delete)]` requires the field type to be \
`Option<chrono::DateTime<chrono::Utc>>`",
));
}
let is_mixin_auto =
attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
if detected_auto && !primary_key && !is_mixin_auto {
return Err(syn::Error::new_spanned(
field,
"`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
`auto_now`",
));
}
if detected_auto && attrs.default.is_some() && !is_mixin_auto {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
SERIAL / BIGSERIAL already supplies a default sequence.",
));
}
if fk_inner.is_some() && primary_key {
return Err(syn::Error::new_spanned(
field,
"`ForeignKey<T>` is not allowed on a primary-key field — \
a row's PK is its own identity, not a reference to a parent.",
));
}
if attrs.generated_as.is_some() {
if primary_key {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(generated_as = \"…\")]` is not allowed on a \
primary-key field — a PK must be writable so the row \
has an identity at INSERT time.",
));
}
if attrs.default.is_some() {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(generated_as = \"…\")]` cannot combine with \
`default = \"…\"` — Postgres rejects DEFAULT on \
generated columns. The expression IS the default.",
));
}
if detected_auto {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(generated_as = \"…\")]` is not allowed on \
an `Auto<T>` field — generated columns are computed \
by the DB, not server-assigned via a sequence. Use a \
plain Rust type (e.g. `f64`).",
));
}
if fk_inner.is_some() {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(generated_as = \"…\")]` is not allowed on a \
ForeignKey field.",
));
}
}
let relation = relation_tokens(field, &attrs, fk_inner, table)?;
let column_lit = column.as_str();
let field_type_tokens = if kind == DetectedKind::Vector {
let root = rustango_root();
let dims = attrs.vector_dims.unwrap_or(0);
quote!(#root::core::FieldType::Vector(#dims))
} else if kind == DetectedKind::Geometry {
let root = rustango_root();
let srid = attrs.geometry_srid.unwrap_or(0);
quote!(#root::core::FieldType::Geometry(#srid))
} else {
kind.variant_tokens()
};
let max_length = optional_u32(attrs.max_length);
let min = optional_i64(attrs.min);
let max = optional_i64(attrs.max);
let default = optional_str(attrs.default.as_deref());
let unique = attrs.unique;
let generated_as = optional_str(attrs.generated_as.as_deref());
let help_text = optional_str(attrs.help_text.as_deref());
let choices = optional_choices(attrs.choices.as_deref());
let db_comment = optional_str(attrs.db_comment.as_deref());
let verbose_name = optional_str(attrs.verbose_name.as_deref());
let editable = attrs.editable;
let blank = attrs.blank;
let case_insensitive = attrs.case_insensitive;
let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
return Err(syn::Error::new_spanned(
field,
"`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
or `o2o = \"<table>\"` on the same field — it has no meaning on a \
non-FK column.",
));
}
let fk_on_delete = match attrs.on_delete.as_deref() {
None => quote!(::core::option::Option::None),
Some(action) => {
let variant = match action {
"cascade" => quote!(Cascade),
"restrict" => quote!(Restrict),
"set_null" => quote!(SetNull),
"set_default" => quote!(SetDefault),
"no_action" => quote!(NoAction),
other => unreachable!("on_delete `{other}` should have been rejected at parse"),
};
quote!(::core::option::Option::Some(
#root::core::OnDeleteAction::#variant
))
}
};
let schema = quote! {
#root::core::FieldSchema {
name: #name,
column: #column_lit,
ty: #field_type_tokens,
nullable: #nullable,
primary_key: #primary_key,
relation: #relation,
max_length: #max_length,
min: #min,
max: #max,
default: #default,
auto: #auto,
unique: #unique,
generated_as: #generated_as,
help_text: #help_text,
choices: #choices,
db_comment: #db_comment,
verbose_name: #verbose_name,
editable: #editable,
blank: #blank,
case_insensitive: #case_insensitive,
fk_on_delete: #fk_on_delete,
validators: &[ #(#validators_lits),* ],
}
};
let from_row_init = quote! {
#ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
};
let from_aliased_row_init = quote! {
#ident: #root::sql::sqlx::Row::try_get(
row,
::std::format!("{}__{}", prefix, #column_lit).as_str(),
)?
};
Ok(FieldInfo {
ident,
column,
primary_key,
auto,
value_ty: &field.ty,
field_type_tokens,
schema,
from_row_init,
from_aliased_row_init,
fk_inner: fk_inner.cloned(),
fk_pk_kind: kind,
nullable,
auto_now: attrs.auto_now,
auto_now_add: attrs.auto_now_add,
soft_delete: attrs.soft_delete,
generated_as: attrs.generated_as.clone(),
default_uuid_v7: attrs.default_uuid_v7,
related_name: attrs.related_name.clone(),
})
}
fn check_bound_compatibility(
field: &syn::Field,
attrs: &FieldAttrs,
kind: DetectedKind,
) -> syn::Result<()> {
if attrs.max_length.is_some() && kind != DetectedKind::String {
return Err(syn::Error::new_spanned(
field,
"`max_length` is only valid on `String` fields (or `Option<String>`)",
));
}
if attrs.choices.is_some() && kind != DetectedKind::String {
return Err(syn::Error::new_spanned(
field,
"`choices` is only valid on `String` fields (or `Option<String>`) — \
integer-valued enumerations should be modeled with a Rust enum and \
custom (de)serializer for now",
));
}
if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
return Err(syn::Error::new_spanned(
field,
"`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
));
}
if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
if min > max {
return Err(syn::Error::new_spanned(
field,
format!("`min` ({min}) is greater than `max` ({max})"),
));
}
}
Ok(())
}
fn optional_u32(value: Option<u32>) -> TokenStream2 {
if let Some(v) = value {
quote!(::core::option::Option::Some(#v))
} else {
quote!(::core::option::Option::None)
}
}
fn optional_i64(value: Option<i64>) -> TokenStream2 {
if let Some(v) = value {
quote!(::core::option::Option::Some(#v))
} else {
quote!(::core::option::Option::None)
}
}
fn optional_str(value: Option<&str>) -> TokenStream2 {
if let Some(v) = value {
quote!(::core::option::Option::Some(#v))
} else {
quote!(::core::option::Option::None)
}
}
fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
let Some(pairs) = pairs else {
return quote!(::core::option::Option::None);
};
let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
quote!(::core::option::Option::Some(&[#(#entries),*]))
}
fn relation_tokens(
field: &syn::Field,
attrs: &FieldAttrs,
fk_inner: Option<&syn::Type>,
table: &str,
) -> syn::Result<TokenStream2> {
let root = rustango_root();
if let Some(inner) = fk_inner {
if attrs.fk.is_some() || attrs.o2o.is_some() {
return Err(syn::Error::new_spanned(
field,
"`ForeignKey<T>` already declares the FK target via the type parameter — \
remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
));
}
let on = attrs.on.as_deref().unwrap_or("id");
return Ok(quote! {
::core::option::Option::Some(#root::core::Relation::Fk {
to: <#inner as #root::core::Model>::SCHEMA.table,
on: #on,
})
});
}
match (&attrs.fk, &attrs.o2o) {
(Some(_), Some(_)) => Err(syn::Error::new_spanned(
field,
"`fk` and `o2o` are mutually exclusive",
)),
(Some(to), None) => {
let on = attrs.on.as_deref().unwrap_or("id");
let resolved = if to == "self" { table } else { to };
Ok(quote! {
::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
})
}
(None, Some(to)) => {
let on = attrs.on.as_deref().unwrap_or("id");
let resolved = if to == "self" { table } else { to };
Ok(quote! {
::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
})
}
(None, None) => {
if attrs.on.is_some() {
return Err(syn::Error::new_spanned(
field,
"`on` requires `fk` or `o2o`",
));
}
Ok(quote!(::core::option::Option::None))
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DetectedKind {
I16,
I32,
I64,
F32,
F64,
Bool,
String,
DateTime,
Date,
Time,
Uuid,
Json,
Decimal,
Binary,
ArrayText,
ArrayInt,
ArrayBigInt,
RangeInt,
RangeBigInt,
RangeNumeric,
RangeDate,
RangeDateTime,
HStore,
Vector,
Geometry,
}
impl DetectedKind {
fn variant_tokens(self) -> TokenStream2 {
let root = rustango_root();
match self {
Self::I16 => quote!(#root::core::FieldType::I16),
Self::I32 => quote!(#root::core::FieldType::I32),
Self::I64 => quote!(#root::core::FieldType::I64),
Self::F32 => quote!(#root::core::FieldType::F32),
Self::F64 => quote!(#root::core::FieldType::F64),
Self::Bool => quote!(#root::core::FieldType::Bool),
Self::String => quote!(#root::core::FieldType::String),
Self::DateTime => quote!(#root::core::FieldType::DateTime),
Self::Date => quote!(#root::core::FieldType::Date),
Self::Time => quote!(#root::core::FieldType::Time),
Self::Uuid => quote!(#root::core::FieldType::Uuid),
Self::Json => quote!(#root::core::FieldType::Json),
Self::Decimal => quote!(#root::core::FieldType::Decimal),
Self::Binary => quote!(#root::core::FieldType::Binary),
Self::ArrayText => {
quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
}
Self::ArrayInt => {
quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
}
Self::ArrayBigInt => {
quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
}
Self::RangeInt => {
quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
}
Self::RangeBigInt => {
quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
}
Self::RangeNumeric => {
quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
}
Self::RangeDate => {
quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
}
Self::RangeDateTime => {
quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
}
Self::HStore => quote!(#root::core::FieldType::HStore),
Self::Vector => quote!(#root::core::FieldType::Vector(0)),
Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
}
}
fn is_integer(self) -> bool {
matches!(self, Self::I16 | Self::I32 | Self::I64)
}
fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
let root = rustango_root();
match self {
Self::I16 => (quote!(I16), quote!(0i16)),
Self::I32 => (quote!(I32), quote!(0i32)),
Self::I64 => (quote!(I64), quote!(0i64)),
Self::F32 => (quote!(F32), quote!(0f32)),
Self::F64 => (quote!(F64), quote!(0f64)),
Self::Bool => (quote!(Bool), quote!(false)),
Self::String => (quote!(String), quote!(::std::string::String::new())),
Self::DateTime => (
quote!(DateTime),
quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
),
Self::Date => (
quote!(Date),
quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
),
Self::Time => (
quote!(Time),
quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
),
Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
Self::Decimal => (
quote!(Decimal),
quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
),
Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
(quote!(Array), quote!(::std::vec::Vec::new()))
}
Self::RangeInt
| Self::RangeBigInt
| Self::RangeNumeric
| Self::RangeDate
| Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
}
}
}
#[derive(Clone, Copy)]
struct DetectedType<'a> {
kind: DetectedKind,
nullable: bool,
auto: bool,
fk_inner: Option<&'a syn::Type>,
}
fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
let Type::Path(TypePath { path, qself: None }) = ty else {
return None;
};
let last = path.segments.last()?;
if last.ident != "Auto" {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
args.args.iter().find_map(|a| match a {
syn::GenericArgument::Type(t) => Some(t),
_ => None,
})
}
fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
let Type::Path(TypePath { path, qself: None }) = ty else {
return Err(syn::Error::new_spanned(ty, "unsupported field type"));
};
let last = path
.segments
.last()
.ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
if last.ident == "Option" {
let inner = generic_inner(ty, &last.arguments, "Option")?;
let inner_det = detect_type(inner)?;
if inner_det.nullable {
return Err(syn::Error::new_spanned(
ty,
"nested Option is not supported",
));
}
if inner_det.auto {
return Err(syn::Error::new_spanned(
ty,
"`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
));
}
return Ok(DetectedType {
nullable: true,
..inner_det
});
}
if last.ident == "Auto" {
let inner = generic_inner(ty, &last.arguments, "Auto")?;
let inner_det = detect_type(inner)?;
if inner_det.auto {
return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
}
if inner_det.nullable {
return Err(syn::Error::new_spanned(
ty,
"`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
));
}
if inner_det.fk_inner.is_some() {
return Err(syn::Error::new_spanned(
ty,
"`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
));
}
if !matches!(
inner_det.kind,
DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
) {
return Err(syn::Error::new_spanned(
ty,
"`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
`uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
(DEFAULT now())",
));
}
return Ok(DetectedType {
auto: true,
..inner_det
});
}
if last.ident == "ForeignKey" {
let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
let kind = match key_ty {
Some(k) => detect_type(k)?.kind,
None => DetectedKind::I64,
};
return Ok(DetectedType {
kind,
nullable: false,
auto: false,
fk_inner: Some(inner),
});
}
let kind = match last.ident.to_string().as_str() {
"i16" => DetectedKind::I16,
"i32" => DetectedKind::I32,
"i64" => DetectedKind::I64,
"f32" => DetectedKind::F32,
"f64" => DetectedKind::F64,
"bool" => DetectedKind::Bool,
"String" => DetectedKind::String,
"DateTime" => DetectedKind::DateTime,
"NaiveDate" => DetectedKind::Date,
"NaiveTime" => DetectedKind::Time,
"Uuid" => DetectedKind::Uuid,
"Value" => DetectedKind::Json,
"Decimal" => DetectedKind::Decimal,
"Vec" => {
let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
if let Type::Path(TypePath { path, qself: None }) = inner {
if let Some(seg) = path.segments.last() {
if seg.ident == "u8" && seg.arguments.is_empty() {
return Ok(DetectedType {
kind: DetectedKind::Binary,
nullable: false,
auto: false,
fk_inner: None,
});
}
}
}
return Err(syn::Error::new_spanned(
ty,
"unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
));
}
"Array" => {
let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
let elem = match inner {
Type::Path(TypePath { path, qself: None }) => {
path.segments.last().map(|s| s.ident.to_string())
}
_ => None,
};
let kind = match elem.as_deref() {
Some("String") => DetectedKind::ArrayText,
Some("i32") => DetectedKind::ArrayInt,
Some("i64") => DetectedKind::ArrayBigInt,
_ => {
return Err(syn::Error::new_spanned(
ty,
"unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
`Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
));
}
};
return Ok(DetectedType {
kind,
nullable: false,
auto: false,
fk_inner: None,
});
}
"Range" => {
let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
let elem = match inner {
Type::Path(TypePath { path, qself: None }) => {
path.segments.last().map(|s| s.ident.to_string())
}
_ => None,
};
let kind = match elem.as_deref() {
Some("i32") => DetectedKind::RangeInt,
Some("i64") => DetectedKind::RangeBigInt,
Some("Decimal") => DetectedKind::RangeNumeric,
Some("NaiveDate") => DetectedKind::RangeDate,
Some("DateTime") => DetectedKind::RangeDateTime,
_ => {
return Err(syn::Error::new_spanned(
ty,
"unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
`Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
`Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
(→ tstzrange) are supported (#343)",
));
}
};
return Ok(DetectedType {
kind,
nullable: false,
auto: false,
fk_inner: None,
});
}
"Cast" => {
return Ok(DetectedType {
kind: DetectedKind::String,
nullable: false,
auto: false,
fk_inner: None,
});
}
"HStore" => {
return Ok(DetectedType {
kind: DetectedKind::HStore,
nullable: false,
auto: false,
fk_inner: None,
});
}
"Vector" => {
return Ok(DetectedType {
kind: DetectedKind::Vector,
nullable: false,
auto: false,
fk_inner: None,
});
}
"Point" => {
return Ok(DetectedType {
kind: DetectedKind::Geometry,
nullable: false,
auto: false,
fk_inner: None,
});
}
other => {
return Err(syn::Error::new_spanned(
ty,
format!("unsupported field type `{other}`; supports i16/i32/i64/f32/f64/bool/String/DateTime/NaiveDate/NaiveTime/Uuid/serde_json::Value/Decimal/Vec<u8>, optionally wrapped in Option or Auto (Auto only on integers/Uuid/DateTime)"),
));
}
};
Ok(DetectedType {
kind,
nullable: false,
auto: false,
fk_inner: None,
})
}
fn generic_inner<'a>(
ty: &'a Type,
arguments: &'a PathArguments,
wrapper: &str,
) -> syn::Result<&'a Type> {
let PathArguments::AngleBracketed(args) = arguments else {
return Err(syn::Error::new_spanned(
ty,
format!("{wrapper} requires a generic argument"),
));
};
args.args
.iter()
.find_map(|a| match a {
GenericArgument::Type(t) => Some(t),
_ => None,
})
.ok_or_else(|| {
syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
})
}
fn generic_pair<'a>(
ty: &'a Type,
arguments: &'a PathArguments,
wrapper: &str,
) -> syn::Result<(&'a Type, Option<&'a Type>)> {
let PathArguments::AngleBracketed(args) = arguments else {
return Err(syn::Error::new_spanned(
ty,
format!("{wrapper} requires a generic argument"),
));
};
let mut types = args.args.iter().filter_map(|a| match a {
GenericArgument::Type(t) => Some(t),
_ => None,
});
let first = types.next().ok_or_else(|| {
syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
})?;
let second = types.next();
Ok((first, second))
}
fn to_snake_case(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
for (i, ch) in s.chars().enumerate() {
if ch.is_ascii_uppercase() {
if i > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
out
}
#[derive(Default)]
struct FormFieldAttrs {
min: Option<i64>,
max: Option<i64>,
min_length: Option<u32>,
max_length: Option<u32>,
clean: Option<syn::Ident>,
}
#[derive(Default)]
struct FormContainerAttrs {
validate: Option<syn::Ident>,
}
#[derive(Clone, Copy)]
enum FormFieldKind {
String,
I16,
I32,
I64,
F32,
F64,
Bool,
}
impl FormFieldKind {
fn parse_method(self) -> &'static str {
match self {
Self::I16 => "i16",
Self::I32 => "i32",
Self::I64 => "i64",
Self::F32 => "f32",
Self::F64 => "f64",
Self::String | Self::Bool => "",
}
}
}
fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
let root = rustango_root();
let struct_name = &input.ident;
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new_spanned(
struct_name,
"Form can only be derived on structs",
));
};
let Fields::Named(named) = &data.fields else {
return Err(syn::Error::new_spanned(
struct_name,
"Form requires a struct with named fields",
));
};
let container = parse_form_container_attrs(input)?;
let post_field_clean: Vec<TokenStream2> = Vec::new();
let _ = post_field_clean;
let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
for field in &named.named {
let ident = field
.ident
.as_ref()
.ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
let attrs = parse_form_field_attrs(field)?;
let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
let name_lit = ident.to_string();
let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
let clean_block = if let Some(clean_fn) = &attrs.clean {
quote! {
if __errors.fields().get(#name_lit).is_none() {
match Self::#clean_fn(&#ident) {
::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
::core::result::Result::Err(__msg) => {
__errors.add(#name_lit, __msg);
}
}
}
}
} else {
quote! {}
};
field_blocks.push(quote! {
#parse_block
#clean_block
});
field_idents.push(ident);
}
let cross_field_call = if let Some(validate_fn) = &container.validate {
quote! {
if __errors.is_empty() {
let __candidate = Self { #( #field_idents ),* };
if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
__errors.merge(__other);
}
if !__errors.is_empty() {
return ::core::result::Result::Err(__errors);
}
return ::core::result::Result::Ok(__candidate);
}
}
} else {
quote! {}
};
Ok(quote! {
impl #root::forms::Form for #struct_name {
fn parse(
data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
) -> ::core::result::Result<Self, #root::forms::FormErrors> {
let mut __errors = #root::forms::FormErrors::default();
#( #field_blocks )*
#cross_field_call
if !__errors.is_empty() {
return ::core::result::Result::Err(__errors);
}
::core::result::Result::Ok(Self {
#( #field_idents ),*
})
}
}
})
}
fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
let mut out = FormContainerAttrs::default();
for attr in &input.attrs {
if !attr.path().is_ident("form") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("validate") {
let s: LitStr = meta.value()?.parse()?;
out.validate = Some(syn::Ident::new(&s.value(), s.span()));
return Ok(());
}
Err(meta.error("unknown form container attribute (supported: `validate`)"))
})?;
}
Ok(out)
}
fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
let mut out = FormFieldAttrs::default();
for attr in &field.attrs {
if !attr.path().is_ident("form") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("min") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.min = Some(lit.base10_parse::<i64>()?);
return Ok(());
}
if meta.path.is_ident("max") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.max = Some(lit.base10_parse::<i64>()?);
return Ok(());
}
if meta.path.is_ident("min_length") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.min_length = Some(lit.base10_parse::<u32>()?);
return Ok(());
}
if meta.path.is_ident("max_length") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.max_length = Some(lit.base10_parse::<u32>()?);
return Ok(());
}
if meta.path.is_ident("clean") {
let s: LitStr = meta.value()?.parse()?;
out.clean = Some(syn::Ident::new(&s.value(), s.span()));
return Ok(());
}
Err(meta.error(
"unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
))
})?;
}
Ok(out)
}
fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
let Type::Path(TypePath { path, qself: None }) = ty else {
return Err(syn::Error::new(
span,
"Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
));
};
let last = path
.segments
.last()
.ok_or_else(|| syn::Error::new(span, "empty type path"))?;
if last.ident == "Option" {
let inner = generic_inner(ty, &last.arguments, "Option")?;
let (kind, nested) = detect_form_field(inner, span)?;
if nested {
return Err(syn::Error::new(
span,
"nested Option in Form fields is not supported",
));
}
return Ok((kind, true));
}
let kind = match last.ident.to_string().as_str() {
"String" => FormFieldKind::String,
"i16" => FormFieldKind::I16,
"i32" => FormFieldKind::I32,
"i64" => FormFieldKind::I64,
"f32" => FormFieldKind::F32,
"f64" => FormFieldKind::F64,
"bool" => FormFieldKind::Bool,
other => {
return Err(syn::Error::new(
span,
format!(
"Form field type `{other}` is not supported in v0.8 — use String / \
i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
),
));
}
};
Ok((kind, false))
}
#[allow(clippy::too_many_lines)]
fn render_form_field_parse(
ident: &syn::Ident,
name_lit: &str,
kind: FormFieldKind,
nullable: bool,
attrs: &FormFieldAttrs,
) -> TokenStream2 {
let lookup = quote! {
let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
};
let parsed_value = match kind {
FormFieldKind::Bool => quote! {
let __v: bool = match __raw {
::core::option::Option::None => false,
::core::option::Option::Some(__s) => !matches!(
__s.to_ascii_lowercase().as_str(),
"" | "false" | "0" | "off" | "no"
),
};
},
FormFieldKind::String => {
if nullable {
quote! {
let __v: ::core::option::Option<::std::string::String> = match __raw {
::core::option::Option::None => ::core::option::Option::None,
::core::option::Option::Some(__s) if __s.is_empty() => {
::core::option::Option::None
}
::core::option::Option::Some(__s) => {
::core::option::Option::Some(::core::clone::Clone::clone(__s))
}
};
}
} else {
quote! {
let __v: ::std::string::String = match __raw {
::core::option::Option::Some(__s) if !__s.is_empty() => {
::core::clone::Clone::clone(__s)
}
_ => {
__errors.add(#name_lit, "This field is required.");
::std::string::String::new()
}
};
}
}
}
FormFieldKind::I16
| FormFieldKind::I32
| FormFieldKind::I64
| FormFieldKind::F32
| FormFieldKind::F64 => {
let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
let ty_lit = kind.parse_method();
let default_val = match kind {
FormFieldKind::I16 => quote! { 0i16 },
FormFieldKind::I32 => quote! { 0i32 },
FormFieldKind::I64 => quote! { 0i64 },
FormFieldKind::F32 => quote! { 0f32 },
FormFieldKind::F64 => quote! { 0f64 },
_ => quote! { Default::default() },
};
if nullable {
quote! {
let __v: ::core::option::Option<#parse_ty> = match __raw {
::core::option::Option::None => ::core::option::Option::None,
::core::option::Option::Some(__s) if __s.is_empty() => {
::core::option::Option::None
}
::core::option::Option::Some(__s) => {
match __s.parse::<#parse_ty>() {
::core::result::Result::Ok(__n) => {
::core::option::Option::Some(__n)
}
::core::result::Result::Err(__e) => {
__errors.add(
#name_lit,
::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
);
::core::option::Option::None
}
}
}
};
}
} else {
quote! {
let __v: #parse_ty = match __raw {
::core::option::Option::Some(__s) if !__s.is_empty() => {
match __s.parse::<#parse_ty>() {
::core::result::Result::Ok(__n) => __n,
::core::result::Result::Err(__e) => {
__errors.add(
#name_lit,
::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
);
#default_val
}
}
}
_ => {
__errors.add(#name_lit, "This field is required.");
#default_val
}
};
}
}
}
};
let validators = render_form_validators(name_lit, kind, nullable, attrs);
quote! {
let mut #ident = {
#lookup
#parsed_value
#validators
__v
};
}
}
fn render_form_validators(
name_lit: &str,
kind: FormFieldKind,
nullable: bool,
attrs: &FormFieldAttrs,
) -> TokenStream2 {
let mut checks: Vec<TokenStream2> = Vec::new();
let val_ref = if nullable {
quote! { __v.as_ref() }
} else {
quote! { ::core::option::Option::Some(&__v) }
};
let is_string = matches!(kind, FormFieldKind::String);
let is_numeric = matches!(
kind,
FormFieldKind::I16
| FormFieldKind::I32
| FormFieldKind::I64
| FormFieldKind::F32
| FormFieldKind::F64
);
if is_string {
if let Some(min_len) = attrs.min_length {
let min_len_usize = min_len as usize;
checks.push(quote! {
if let ::core::option::Option::Some(__s) = #val_ref {
if __s.len() < #min_len_usize {
__errors.add(
#name_lit,
::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
);
}
}
});
}
if let Some(max_len) = attrs.max_length {
let max_len_usize = max_len as usize;
checks.push(quote! {
if let ::core::option::Option::Some(__s) = #val_ref {
if __s.len() > #max_len_usize {
__errors.add(
#name_lit,
::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
);
}
}
});
}
}
if is_numeric {
if let Some(min) = attrs.min {
checks.push(quote! {
if let ::core::option::Option::Some(__n) = #val_ref {
if (*__n as f64) < (#min as f64) {
__errors.add(
#name_lit,
::std::format!("Ensure this value is greater than or equal to {}.", #min),
);
}
}
});
}
if let Some(max) = attrs.max {
checks.push(quote! {
if let ::core::option::Option::Some(__n) = #val_ref {
if (*__n as f64) > (#max as f64) {
__errors.add(
#name_lit,
::std::format!("Ensure this value is less than or equal to {}.", #max),
);
}
}
});
}
}
quote! { #( #checks )* }
}
struct ViewSetAttrs {
model: syn::Path,
fields: Option<Vec<String>>,
filter_fields: Vec<String>,
search_fields: Vec<String>,
ordering: Vec<(String, bool)>,
page_size: Option<usize>,
read_only: bool,
perms: ViewSetPermsAttrs,
serializer: Option<syn::Path>,
}
#[derive(Default)]
struct ViewSetPermsAttrs {
list: Vec<String>,
retrieve: Vec<String>,
create: Vec<String>,
update: Vec<String>,
destroy: Vec<String>,
}
fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
let root = rustango_root();
let struct_name = &input.ident;
match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Unit | Fields::Named(_) => {}
Fields::Unnamed(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"ViewSet can only be derived on a unit struct or an empty named struct",
));
}
},
_ => {
return Err(syn::Error::new_spanned(
struct_name,
"ViewSet can only be derived on a struct",
));
}
}
let attrs = parse_viewset_attrs(input)?;
let model_path = &attrs.model;
let fields_call = if let Some(ref fields) = attrs.fields {
let lits = fields.iter().map(|f| f.as_str());
quote!(.fields(&[ #(#lits),* ]))
} else {
quote!()
};
let filter_fields_call = if attrs.filter_fields.is_empty() {
quote!()
} else {
let lits = attrs.filter_fields.iter().map(|f| f.as_str());
quote!(.filter_fields(&[ #(#lits),* ]))
};
let search_fields_call = if attrs.search_fields.is_empty() {
quote!()
} else {
let lits = attrs.search_fields.iter().map(|f| f.as_str());
quote!(.search_fields(&[ #(#lits),* ]))
};
let ordering_call = if attrs.ordering.is_empty() {
quote!()
} else {
let pairs = attrs.ordering.iter().map(|(f, desc)| {
let f = f.as_str();
quote!((#f, #desc))
});
quote!(.ordering(&[ #(#pairs),* ]))
};
let page_size_call = if let Some(n) = attrs.page_size {
quote!(.page_size(#n))
} else {
quote!()
};
let read_only_call = if attrs.read_only {
quote!(.read_only())
} else {
quote!()
};
let serializer_call = if let Some(ref ser) = attrs.serializer {
quote!(.serializer::<#ser>())
} else {
quote!()
};
let perms = &attrs.perms;
let perms_call = if perms.list.is_empty()
&& perms.retrieve.is_empty()
&& perms.create.is_empty()
&& perms.update.is_empty()
&& perms.destroy.is_empty()
{
quote!()
} else {
let list_lits = perms.list.iter().map(|s| s.as_str());
let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
let create_lits = perms.create.iter().map(|s| s.as_str());
let update_lits = perms.update.iter().map(|s| s.as_str());
let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
quote! {
.permissions(#root::viewset::ViewSetPerms {
list: ::std::vec![ #(#list_lits.to_owned()),* ],
retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
create: ::std::vec![ #(#create_lits.to_owned()),* ],
update: ::std::vec![ #(#update_lits.to_owned()),* ],
destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
})
}
};
Ok(quote! {
impl #struct_name {
pub fn router(prefix: &str, pool: #root::sql::sqlx::PgPool) -> #root::__axum::Router {
#root::viewset::ViewSet::for_model(
<#model_path as #root::core::Model>::SCHEMA
)
#fields_call
#filter_fields_call
#search_fields_call
#ordering_call
#page_size_call
#perms_call
#read_only_call
#serializer_call
.router(prefix, pool)
}
}
})
}
fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
let mut model: Option<syn::Path> = None;
let mut fields: Option<Vec<String>> = None;
let mut filter_fields: Vec<String> = Vec::new();
let mut search_fields: Vec<String> = Vec::new();
let mut ordering: Vec<(String, bool)> = Vec::new();
let mut page_size: Option<usize> = None;
let mut read_only = false;
let mut perms = ViewSetPermsAttrs::default();
let mut serializer: Option<syn::Path> = None;
for attr in &input.attrs {
if !attr.path().is_ident("viewset") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("model") {
let path: syn::Path = meta.value()?.parse()?;
model = Some(path);
return Ok(());
}
if meta.path.is_ident("serializer") {
let path: syn::Path = meta.value()?.parse()?;
serializer = Some(path);
return Ok(());
}
if meta.path.is_ident("fields") {
let s: LitStr = meta.value()?.parse()?;
fields = Some(split_field_list(&s.value()));
return Ok(());
}
if meta.path.is_ident("filter_fields") {
let s: LitStr = meta.value()?.parse()?;
filter_fields = split_field_list(&s.value());
return Ok(());
}
if meta.path.is_ident("search_fields") {
let s: LitStr = meta.value()?.parse()?;
search_fields = split_field_list(&s.value());
return Ok(());
}
if meta.path.is_ident("ordering") {
let s: LitStr = meta.value()?.parse()?;
ordering = parse_ordering_list(&s.value());
return Ok(());
}
if meta.path.is_ident("page_size") {
let lit: syn::LitInt = meta.value()?.parse()?;
page_size = Some(lit.base10_parse::<usize>()?);
return Ok(());
}
if meta.path.is_ident("read_only") {
read_only = true;
return Ok(());
}
if meta.path.is_ident("permissions") {
meta.parse_nested_meta(|inner| {
let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
let s: LitStr = inner.value()?.parse()?;
Ok(split_field_list(&s.value()))
};
if inner.path.is_ident("list") {
perms.list = parse_codenames(&inner)?;
} else if inner.path.is_ident("retrieve") {
perms.retrieve = parse_codenames(&inner)?;
} else if inner.path.is_ident("create") {
perms.create = parse_codenames(&inner)?;
} else if inner.path.is_ident("update") {
perms.update = parse_codenames(&inner)?;
} else if inner.path.is_ident("destroy") {
perms.destroy = parse_codenames(&inner)?;
} else {
return Err(inner.error(
"unknown permissions key (supported: list, retrieve, create, update, destroy)",
));
}
Ok(())
})?;
return Ok(());
}
Err(meta.error(
"unknown viewset attribute (supported: model, fields, filter_fields, \
search_fields, ordering, page_size, read_only, serializer, permissions(...))",
))
})?;
}
let model = model.ok_or_else(|| {
syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
})?;
Ok(ViewSetAttrs {
model,
fields,
filter_fields,
search_fields,
ordering,
page_size,
read_only,
perms,
serializer,
})
}
struct SerializerContainerAttrs {
model: syn::Path,
cross_validate: Option<syn::Ident>,
}
#[derive(Default)]
struct SerializerFieldAttrs {
read_only: bool,
write_only: bool,
source: Option<String>,
skip: bool,
method: Option<String>,
validate: Option<String>,
nested: bool,
nested_strict: bool,
many: Option<syn::Type>,
slug: Option<String>,
max_length: Option<u64>,
min_length: Option<u64>,
min: Option<i64>,
max: Option<i64>,
}
fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
let mut model: Option<syn::Path> = None;
let mut cross_validate: Option<syn::Ident> = None;
for attr in &input.attrs {
if !attr.path().is_ident("serializer") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("model") {
let _eq: syn::Token![=] = meta.input.parse()?;
model = Some(meta.input.parse()?);
return Ok(());
}
if meta.path.is_ident("validate") {
let s: LitStr = meta.value()?.parse()?;
cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
return Ok(());
}
Err(meta.error(
"unknown serializer container attribute \
(supported: `model`, `validate`)",
))
})?;
}
let model = model.ok_or_else(|| {
syn::Error::new_spanned(
&input.ident,
"`#[serializer(model = SomeModel)]` is required",
)
})?;
Ok(SerializerContainerAttrs {
model,
cross_validate,
})
}
fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
let mut out = SerializerFieldAttrs::default();
for attr in &field.attrs {
if !attr.path().is_ident("serializer") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("read_only") {
out.read_only = true;
return Ok(());
}
if meta.path.is_ident("write_only") {
out.write_only = true;
return Ok(());
}
if meta.path.is_ident("skip") {
out.skip = true;
return Ok(());
}
if meta.path.is_ident("source") {
let s: LitStr = meta.value()?.parse()?;
out.source = Some(s.value());
return Ok(());
}
if meta.path.is_ident("method") {
let s: LitStr = meta.value()?.parse()?;
out.method = Some(s.value());
return Ok(());
}
if meta.path.is_ident("validate") {
let s: LitStr = meta.value()?.parse()?;
out.validate = Some(s.value());
return Ok(());
}
if meta.path.is_ident("many") {
let _eq: syn::Token![=] = meta.input.parse()?;
out.many = Some(meta.input.parse()?);
return Ok(());
}
if meta.path.is_ident("nested") {
out.nested = true;
if meta.input.peek(syn::token::Paren) {
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("strict") {
out.nested_strict = true;
return Ok(());
}
Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
})?;
}
return Ok(());
}
if meta.path.is_ident("slug") {
let s: LitStr = meta.value()?.parse()?;
out.slug = Some(s.value());
return Ok(());
}
if meta.path.is_ident("max_length") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.max_length = Some(lit.base10_parse::<u64>()?);
return Ok(());
}
if meta.path.is_ident("min_length") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.min_length = Some(lit.base10_parse::<u64>()?);
return Ok(());
}
if meta.path.is_ident("min") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.min = Some(lit.base10_parse::<i64>()?);
return Ok(());
}
if meta.path.is_ident("max") {
let lit: syn::LitInt = meta.value()?.parse()?;
out.max = Some(lit.base10_parse::<i64>()?);
return Ok(());
}
Err(meta.error(
"unknown serializer field attribute (supported: \
`read_only`, `write_only`, `source`, `skip`, `method`, \
`validate`, `nested`, `many`, `slug`, `max_length`, \
`min_length`, `min`, `max`)",
))
})?;
}
if out.read_only && out.write_only {
return Err(syn::Error::new_spanned(
field,
"a field cannot be both `read_only` and `write_only`",
));
}
if out.method.is_some() && out.source.is_some() {
return Err(syn::Error::new_spanned(
field,
"`method` and `source` are mutually exclusive — `method` computes \
the value from a method, `source` reads it from a different model field",
));
}
if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
return Err(syn::Error::new_spanned(
field,
"`slug` is mutually exclusive with `method`, `nested`, and `many` \
— pick one strategy for populating the field",
));
}
Ok(out)
}
fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
let root = rustango_root();
let struct_name = &input.ident;
let struct_name_lit = struct_name.to_string();
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new_spanned(
struct_name,
"Serializer can only be derived on structs",
));
};
let Fields::Named(named) = &data.fields else {
return Err(syn::Error::new_spanned(
struct_name,
"Serializer requires a struct with named fields",
));
};
let container = parse_serializer_container_attrs(input)?;
let model_path = &container.model;
#[allow(dead_code)]
struct FieldInfo {
ident: syn::Ident,
ty: syn::Type,
attrs: SerializerFieldAttrs,
}
let mut fields_info: Vec<FieldInfo> = Vec::new();
for field in &named.named {
let ident = field.ident.clone().expect("named field has ident");
let attrs = parse_serializer_field_attrs(field)?;
fields_info.push(FieldInfo {
ident,
ty: field.ty.clone(),
attrs,
});
}
let from_model_fields = fields_info.iter().map(|fi| {
let ident = &fi.ident;
let ty = &fi.ty;
if let Some(_inner) = &fi.attrs.many {
quote! { #ident: ::std::vec::Vec::new() }
} else if let Some(method) = &fi.attrs.method {
let method_ident = syn::Ident::new(method, ident.span());
quote! { #ident: Self::#method_ident(model) }
} else if let Some(slug_field) = &fi.attrs.slug {
let src_name = fi
.attrs
.source
.as_deref()
.unwrap_or(&fi.ident.to_string())
.to_owned();
let src_ident = syn::Ident::new(&src_name, ident.span());
let slug_ident = syn::Ident::new(slug_field, ident.span());
quote! {
#ident: match model.#src_ident.value() {
::core::option::Option::Some(__loaded) =>
::core::clone::Clone::clone(&__loaded.#slug_ident),
::core::option::Option::None =>
::core::default::Default::default(),
}
}
} else if fi.attrs.nested {
let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
let src_ident = syn::Ident::new(&src_name, ident.span());
if fi.attrs.nested_strict {
let panic_msg = format!(
"nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
);
quote! {
#ident: <#ty as #root::serializer::ModelSerializer>::from_model(
model.#src_ident.value().expect(#panic_msg),
)
}
} else {
quote! {
#ident: match model.#src_ident.value() {
::core::option::Option::Some(__loaded) =>
<#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
::core::option::Option::None =>
::core::default::Default::default(),
}
}
}
} else if fi.attrs.write_only || fi.attrs.skip {
quote! { #ident: ::core::default::Default::default() }
} else if let Some(src) = &fi.attrs.source {
let src_ident = syn::Ident::new(src, ident.span());
quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
} else {
quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
}
});
let is_writable = |fi: &&FieldInfo| {
!fi.attrs.read_only
&& !fi.attrs.skip
&& fi.attrs.method.is_none()
&& !fi.attrs.nested
&& fi.attrs.many.is_none()
&& fi.attrs.slug.is_none()
};
let opt_usize = |v: Option<u64>| match v {
Some(n) => {
let n = n as usize;
quote!(::core::option::Option::Some(#n))
}
None => quote!(::core::option::Option::None),
};
let opt_i64 = |v: Option<i64>| match v {
Some(n) => quote!(::core::option::Option::Some(#n)),
None => quote!(::core::option::Option::None),
};
let constraint_blocks: Vec<_> = fields_info
.iter()
.filter(is_writable)
.map(|fi| {
let ident = &fi.ident;
let fname = ident.to_string();
let mname = fi
.attrs
.source
.clone()
.unwrap_or_else(|| fi.ident.to_string());
let attr_max_len = opt_usize(fi.attrs.max_length);
let attr_min_len = opt_usize(fi.attrs.min_length);
let attr_min = opt_i64(fi.attrs.min);
let attr_max = opt_i64(fi.attrs.max);
quote! {
{
let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
let __max_length: ::core::option::Option<usize> = #attr_max_len
.or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
let __min_length: ::core::option::Option<usize> = #attr_min_len;
let __min: ::core::option::Option<i64> =
#attr_min.or_else(|| __sf.and_then(|__f| __f.min));
let __max: ::core::option::Option<i64> =
#attr_max.or_else(|| __sf.and_then(|__f| __f.max));
let __choices = __sf.and_then(|__f| __f.choices);
let __v = #root::__serde_json::to_value(&self.#ident)
.unwrap_or(#root::__serde_json::Value::Null);
#root::forms::validators::check_value(
#fname, &__v, __max_length, __min_length, __min, __max, __choices,
&mut __errors,
);
}
}
})
.collect();
let has_constraints = !constraint_blocks.is_empty();
let validator_calls: Vec<_> = fields_info
.iter()
.filter_map(|fi| {
let ident = &fi.ident;
let name_lit = ident.to_string();
let method = fi.attrs.validate.as_ref()?;
let method_ident = syn::Ident::new(method, ident.span());
Some(quote! {
if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
__errors.add(#name_lit.to_owned(), __e);
}
})
})
.collect();
let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
quote! {
if let ::core::result::Result::Err(__cross) = self.#method_ident() {
__errors.merge(__cross);
}
}
});
let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
let has_run_validations = has_validators || has_constraints;
let validate_body = quote! {
let mut __errors = #root::forms::FormErrors::default();
#( #constraint_blocks )*
#( #validator_calls )*
#cross_validate_call
if __errors.is_empty() {
::core::result::Result::Ok(())
} else {
::core::result::Result::Err(__errors)
}
};
let validate_method = if has_validators {
quote! {
impl #struct_name {
pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
#validate_body
}
}
}
} else {
quote! {}
};
let trait_validate_override = if has_run_validations {
quote! {
fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
#validate_body
}
}
} else {
quote! {}
};
let many_setters: Vec<_> = fields_info
.iter()
.filter_map(|fi| {
let many_ty = fi.attrs.many.as_ref()?;
let ident = &fi.ident;
let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
Some(quote! {
pub fn #setter(
&mut self,
models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
) -> &mut Self {
self.#ident = models.iter()
.map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
.collect();
self
}
})
})
.collect();
let many_setters_impl = if many_setters.is_empty() {
quote! {}
} else {
quote! {
impl #struct_name {
#( #many_setters )*
}
}
};
let output_fields: Vec<_> = fields_info
.iter()
.filter(|fi| !fi.attrs.write_only)
.collect();
let output_field_count = output_fields.len();
let serialize_fields = output_fields.iter().map(|fi| {
let ident = &fi.ident;
let name_lit = ident.to_string();
quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
});
let writable_lits: Vec<_> = fields_info
.iter()
.filter(is_writable)
.map(|fi| fi.ident.to_string())
.collect();
let writable_source_lits: Vec<String> = fields_info
.iter()
.filter(is_writable)
.map(|fi| {
fi.attrs
.source
.clone()
.unwrap_or_else(|| fi.ident.to_string())
})
.collect();
let from_writable_json_inits: Vec<_> = fields_info
.iter()
.map(|fi| {
let ident = &fi.ident;
let fname = ident.to_string();
let ty = &fi.ty;
if is_writable(&fi) {
quote! {
#ident: match __obj.and_then(|__o| __o.get(#fname)) {
::core::option::Option::Some(__v) => {
match #root::__serde_json::from_value::<#ty>(
::core::clone::Clone::clone(__v),
) {
::core::result::Result::Ok(__x) => __x,
::core::result::Result::Err(__e) => {
__errors.add(#fname.to_owned(), __e.to_string());
::core::default::Default::default()
}
}
}
::core::option::Option::None => ::core::default::Default::default(),
}
}
} else {
quote! { #ident: ::core::default::Default::default() }
}
})
.collect();
let openapi_impl = {
#[cfg(feature = "openapi")]
{
let property_calls = output_fields.iter().map(|fi| {
let ident = &fi.ident;
let name_lit = ident.to_string();
let ty = &fi.ty;
let nullable_call = if is_option(ty) {
quote! { .nullable() }
} else {
quote! {}
};
quote! {
.property(
#name_lit,
<#ty as #root::openapi::OpenApiSchema>::openapi_schema()
#nullable_call,
)
}
});
let required_lits: Vec<_> = output_fields
.iter()
.filter(|fi| !is_option(&fi.ty))
.map(|fi| fi.ident.to_string())
.collect();
quote! {
impl #root::openapi::OpenApiSchema for #struct_name {
fn openapi_schema() -> #root::openapi::Schema {
#root::openapi::Schema::object()
#( #property_calls )*
.required([ #( #required_lits ),* ])
}
}
}
}
#[cfg(not(feature = "openapi"))]
{
quote! {}
}
};
Ok(quote! {
impl #root::serializer::ModelSerializer for #struct_name {
type Model = #model_path;
fn from_model(model: &Self::Model) -> Self {
Self {
#( #from_model_fields ),*
}
}
fn writable_fields() -> &'static [&'static str] {
&[ #( #writable_lits ),* ]
}
fn writable_source_fields() -> &'static [&'static str] {
&[ #( #writable_source_lits ),* ]
}
fn from_writable_json(
__body: &#root::__serde_json::Value,
) -> ::core::result::Result<Self, #root::forms::FormErrors> {
let mut __errors = #root::forms::FormErrors::default();
let __obj = __body.as_object();
let __out = Self {
#( #from_writable_json_inits ),*
};
if __errors.is_empty() {
::core::result::Result::Ok(__out)
} else {
::core::result::Result::Err(__errors)
}
}
#trait_validate_override
}
impl #root::__serde::Serialize for #struct_name {
fn serialize<S>(&self, serializer: S)
-> ::core::result::Result<S::Ok, S::Error>
where
S: #root::__serde::Serializer,
{
use #root::__serde::ser::SerializeStruct;
let mut __state = serializer.serialize_struct(
#struct_name_lit,
#output_field_count,
)?;
#( #serialize_fields )*
__state.end()
}
}
#openapi_impl
#validate_method
#many_setters_impl
})
}
#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
fn is_option(ty: &syn::Type) -> bool {
if let syn::Type::Path(p) = ty {
if let Some(last) = p.path.segments.last() {
return last.ident == "Option";
}
}
false
}