use proc_macro2::Span;
use quote::format_ident;
use syn::Result;
use crate::sql_ident::parse_sql_ident;
use super::graph_decl::UpdateGraphDeclarations;
use super::graph_parse::parse_update_graph_attr;
pub(super) struct StructAttrs {
pub(super) table: String,
pub(super) id_column: Option<String>,
pub(super) model: Option<syn::Path>,
pub(super) returning: Option<syn::Path>,
pub(super) graph: UpdateGraphDeclarations,
pub(super) input: Option<InputConfig>,
}
#[derive(Clone)]
pub(super) struct InputConfig {
pub(super) name: syn::Ident,
pub(super) vis: syn::Visibility,
}
pub(super) struct StructAttrList {
pub(super) table: Option<String>,
pub(super) id_column: Option<String>,
pub(super) model: Option<syn::Path>,
pub(super) returning: Option<syn::Path>,
pub(super) input: bool,
pub(super) input_name: Option<syn::Ident>,
pub(super) input_vis: Option<syn::Visibility>,
}
impl syn::parse::Parse for StructAttrList {
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
let mut table: Option<String> = None;
let mut id_column: Option<String> = None;
let mut model: Option<syn::Path> = None;
let mut returning: Option<syn::Path> = None;
let mut input_flag = false;
let mut input_name: Option<syn::Ident> = None;
let mut input_vis: Option<syn::Visibility> = None;
loop {
if input.is_empty() {
break;
}
let ident: syn::Ident = input.parse()?;
let key = ident.to_string();
if input.peek(syn::token::Paren) {
return Err(syn::Error::new(
Span::call_site(),
"unexpected function-style attribute",
));
}
if input.peek(syn::Token![=]) {
let _: syn::Token![=] = input.parse()?;
let value: syn::LitStr = input.parse()?;
match key.as_str() {
"table" => table = Some(value.value()),
"id_column" => id_column = Some(parse_sql_ident(&value, "id_column")?),
"model" => {
let ty: syn::Path = syn::parse_str(&value.value()).map_err(|e| {
syn::Error::new(Span::call_site(), format!("invalid model type: {e}"))
})?;
model = Some(ty);
}
"returning" => {
let ty: syn::Path = syn::parse_str(&value.value()).map_err(|e| {
syn::Error::new(
Span::call_site(),
format!("invalid returning type: {e}"),
)
})?;
returning = Some(ty);
}
"input" => {
input_flag = true;
let ty: syn::Ident = syn::parse_str(&value.value()).map_err(|e| {
syn::Error::new(Span::call_site(), format!("invalid input type: {e}"))
})?;
input_name = Some(ty);
}
"input_vis" => {
let vis: syn::Visibility = syn::parse_str(&value.value()).map_err(|e| {
syn::Error::new(Span::call_site(), format!("invalid input_vis: {e}"))
})?;
input_vis = Some(vis);
}
_ => {}
}
} else if key.as_str() == "input" {
input_flag = true;
}
if input.peek(syn::Token![,]) {
let _: syn::Token![,] = input.parse()?;
} else {
break;
}
}
Ok(Self {
table,
id_column,
model,
returning,
input: input_flag,
input_name,
input_vis,
})
}
}
pub(super) struct FieldAttrs {
pub(super) skip_update: bool,
pub(super) default: bool,
pub(super) auto_now: bool,
pub(super) version: bool,
pub(super) table: Option<String>,
pub(super) column: Option<String>,
pub(super) skip_input: bool,
pub(super) input_as: Option<syn::Type>,
pub(super) required: bool,
pub(super) len: Option<String>,
pub(super) range: Option<String>,
pub(super) email: bool,
pub(super) regex: Option<String>,
pub(super) url: bool,
pub(super) uuid: bool,
pub(super) ip: bool,
pub(super) one_of: Option<String>,
pub(super) custom: Option<syn::Path>,
}
impl syn::parse::Parse for FieldAttrs {
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
let mut attrs = FieldAttrs {
skip_update: false,
default: false,
auto_now: false,
version: false,
table: None,
column: None,
skip_input: false,
input_as: None,
required: false,
len: None,
range: None,
email: false,
regex: None,
url: false,
uuid: false,
ip: false,
one_of: None,
custom: None,
};
loop {
if input.is_empty() {
break;
}
let ident: syn::Ident = input.parse()?;
let key = ident.to_string();
match key.as_str() {
"skip_update" => attrs.skip_update = true,
"default" => attrs.default = true,
"auto_now" => attrs.auto_now = true,
"version" => attrs.version = true,
"skip_input" => attrs.skip_input = true,
"required" => attrs.required = true,
"email" => attrs.email = true,
"url" => attrs.url = true,
"uuid" => attrs.uuid = true,
"ip" => attrs.ip = true,
_ => {
let _: syn::Token![=] = input.parse()?;
let value: syn::LitStr = input.parse()?;
match key.as_str() {
"table" => attrs.table = Some(value.value()),
"column" => attrs.column = Some(value.value()),
"input_as" => {
let ty: syn::Type = syn::parse_str(&value.value()).map_err(|e| {
syn::Error::new(
Span::call_site(),
format!("invalid input_as type: {e}"),
)
})?;
attrs.input_as = Some(ty);
}
"len" => attrs.len = Some(value.value()),
"range" => attrs.range = Some(value.value()),
"regex" => attrs.regex = Some(value.value()),
"one_of" => attrs.one_of = Some(value.value()),
"custom" => {
let path: syn::Path = syn::parse_str(&value.value()).map_err(|e| {
syn::Error::new(
Span::call_site(),
format!("invalid custom path: {e}"),
)
})?;
attrs.custom = Some(path);
}
_ => {}
}
}
}
if input.peek(syn::Token![,]) {
let _: syn::Token![,] = input.parse()?;
} else {
break;
}
}
Ok(attrs)
}
}
pub(super) fn get_field_attrs(field: &syn::Field) -> Result<FieldAttrs> {
let mut merged = FieldAttrs {
skip_update: false,
default: false,
auto_now: false,
version: false,
table: None,
column: None,
skip_input: false,
input_as: None,
required: false,
len: None,
range: None,
email: false,
regex: None,
url: false,
uuid: false,
ip: false,
one_of: None,
custom: None,
};
for attr in &field.attrs {
if !attr.path().is_ident("orm") {
continue;
}
if let syn::Meta::List(meta_list) = &attr.meta {
let parsed = syn::parse2::<FieldAttrs>(meta_list.tokens.clone())?;
merged.skip_update |= parsed.skip_update;
merged.default |= parsed.default;
merged.auto_now |= parsed.auto_now;
merged.version |= parsed.version;
merged.skip_input |= parsed.skip_input;
merged.required |= parsed.required;
merged.email |= parsed.email;
merged.url |= parsed.url;
merged.uuid |= parsed.uuid;
merged.ip |= parsed.ip;
if parsed.table.is_some() {
merged.table = parsed.table;
}
if parsed.column.is_some() {
merged.column = parsed.column;
}
if let Some(ty) = parsed.input_as {
if merged.input_as.is_some() {
return Err(syn::Error::new_spanned(field, "duplicate input_as"));
}
merged.input_as = Some(ty);
}
if let Some(v) = parsed.len {
if merged.len.is_some() {
return Err(syn::Error::new_spanned(field, "duplicate len"));
}
merged.len = Some(v);
}
if let Some(v) = parsed.range {
if merged.range.is_some() {
return Err(syn::Error::new_spanned(field, "duplicate range"));
}
merged.range = Some(v);
}
if let Some(v) = parsed.regex {
if merged.regex.is_some() {
return Err(syn::Error::new_spanned(field, "duplicate regex"));
}
merged.regex = Some(v);
}
if let Some(v) = parsed.one_of {
if merged.one_of.is_some() {
return Err(syn::Error::new_spanned(field, "duplicate one_of"));
}
merged.one_of = Some(v);
}
if let Some(v) = parsed.custom {
if merged.custom.is_some() {
return Err(syn::Error::new_spanned(field, "duplicate custom"));
}
merged.custom = Some(v);
}
}
}
if merged.auto_now && merged.skip_update {
return Err(syn::Error::new_spanned(
field,
"auto_now and skip_update are mutually exclusive",
));
}
if merged.auto_now && merged.default {
return Err(syn::Error::new_spanned(
field,
"auto_now and default are mutually exclusive",
));
}
if merged.version && merged.skip_update {
return Err(syn::Error::new_spanned(
field,
"version and skip_update are mutually exclusive",
));
}
if merged.version && merged.default {
return Err(syn::Error::new_spanned(
field,
"version and default are mutually exclusive",
));
}
Ok(merged)
}
pub(super) fn get_struct_attrs(input: &syn::DeriveInput) -> Result<StructAttrs> {
let mut table: Option<String> = None;
let mut id_column: Option<String> = None;
let mut model: Option<syn::Path> = None;
let mut returning: Option<syn::Path> = None;
let mut graph = UpdateGraphDeclarations::default();
let mut input_enabled = false;
let mut input_name: Option<syn::Ident> = None;
let mut input_vis: Option<syn::Visibility> = None;
for attr in &input.attrs {
if !attr.path().is_ident("orm") {
continue;
}
if let syn::Meta::List(meta_list) = &attr.meta {
if let Ok(parsed) = syn::parse2::<StructAttrList>(meta_list.tokens.clone()) {
if parsed.table.is_some() {
table = parsed.table;
}
if parsed.id_column.is_some() {
id_column = parsed.id_column;
}
if parsed.model.is_some() {
model = parsed.model;
}
if parsed.returning.is_some() {
returning = parsed.returning;
}
if parsed.input {
input_enabled = true;
}
if parsed.input_name.is_some() {
input_name = parsed.input_name;
}
if parsed.input_vis.is_some() {
input_vis = parsed.input_vis;
}
continue;
}
parse_update_graph_attr(&meta_list.tokens, &mut graph)?;
}
}
let table = table.ok_or_else(|| {
syn::Error::new_spanned(
input,
"UpdateModel requires #[orm(table = \"table_name\")] attribute",
)
})?;
Ok(StructAttrs {
table,
id_column,
model,
returning,
graph,
input: if input_enabled {
let name = input_name.unwrap_or_else(|| format_ident!("{}Input", input.ident));
let vis = input_vis
.unwrap_or_else(|| syn::parse_str::<syn::Visibility>("pub").expect("valid vis"));
Some(InputConfig { name, vis })
} else {
None
},
})
}