use crate::{Ident, Type};
use proc_macro2::TokenStream;
use structmeta::{Flag, StructMeta};
use syn::{Attribute, Field, LitStr, Path};
#[derive(Debug, Clone)]
pub enum Join {
ManyToOne {
column: String,
},
ManyToMany {},
OneToMany {},
}
#[derive(Clone, Debug)]
pub struct ColumnMeta {
pub name: String,
pub ty: Type,
pub marked_primary_key: bool,
pub has_database_default: bool,
pub ident: Ident,
pub many_to_one_column_name: Option<String>,
pub many_to_many_table: Option<String>,
pub one_to_many_foreign_key: Option<ForeignKey>,
pub skip: bool,
pub rust_default: Option<String>,
pub join: Option<Join>,
pub json: bool,
}
impl ColumnMeta {
pub fn is_default(&self) -> bool {
self.rust_default.is_some() || self.has_database_default
}
pub fn from_fields<'a>(fields: impl Iterator<Item = &'a Field>) -> Vec<Self> {
fields.map(|f| ColumnMeta::from_field(f)).collect()
}
pub fn from_syn(ident: &syn::Ident, ty: &syn::Type) -> Self {
let syn::Type::Path(ty) = &ty else {
panic!("No type on field {}", ident);
};
Self {
name: ident.to_string(),
ty: Type::from(&ty.path),
marked_primary_key: false,
has_database_default: false,
ident: Ident::from(ident),
many_to_one_column_name: None,
many_to_many_table: None,
one_to_many_foreign_key: None,
skip: false,
rust_default: None,
join: None,
json: false,
}
}
pub fn is_join(&self) -> bool {
matches!(self.ty, Type::Join(_))
}
pub fn is_join_many(&self) -> bool {
let Type::Join(join) = &self.ty else {
return false;
};
let Type::Inner(o) = join.as_ref() else {
return false;
};
o.ident == "Vec"
}
pub fn is_option(&self) -> bool {
matches!(self.ty, Type::Option(_))
}
pub fn is_json(&self) -> bool {
self.ty.is_json() || self.json
}
pub fn joined_struct_name(&self) -> Option<String> {
let Type::Join(join) = &self.ty else {
return None;
};
Some(join.inner_type_name())
}
pub fn joined_model(&self) -> TokenStream {
self.ty.qualified_inner_name()
}
pub fn from_field(f: &Field) -> Self {
let ident = f.ident.as_ref().expect("No ident on field");
let attrs = ColumnAttr::from_attrs(&f.attrs);
let mut column = ColumnMeta::from_syn(ident, &f.ty);
for attr in attrs {
if attr.primary_key.value() {
column.marked_primary_key = true;
column.has_database_default = true;
}
if let Some(c) = attr.column {
column.name = c.value();
}
if let Some(value) = attr.join_column {
let value = value.value();
column.many_to_one_column_name = Some(value.clone());
column.name = value.clone();
column.join = Some(Join::ManyToOne { column: value });
}
if let Some(path) = attr.many_to_many_table {
let value = path.to_string();
column.many_to_many_table = Some(value);
column.join = Some(Join::ManyToMany {});
}
if let Some(_path) = attr.one_to_many_foreign_key {
column.one_to_many_foreign_key = Some(ForeignKey {
model: "".to_string(),
column: "".to_string(),
});
column.join = Some(Join::OneToMany {});
panic!("Join support in ormlite is in alpha state, and one_to_many_foreign_key is unfortunately not implemented yet.");
}
if let Some(default_value) = attr.default_value {
column.rust_default = Some(default_value.value());
}
column.has_database_default |= attr.default.value();
column.marked_primary_key |= attr.insertable_primary_key.value();
column.skip |= attr.skip.value();
column.json |= attr.json.value();
}
if column.ty.is_join() ^ column.join.is_some() {
panic!("Column {ident} is a Join. You must specify one of these attributes: join_column (for many to one), many_to_many_table_name, or one_to_many_foreign_key");
}
column
}
#[doc(hidden)]
pub fn mock(name: &str, ty: &str) -> Self {
Self {
name: name.to_string(),
ty: Type::Inner(crate::InnerType::mock(ty)),
marked_primary_key: false,
has_database_default: false,
ident: Ident::from(name),
many_to_one_column_name: None,
many_to_many_table: None,
one_to_many_foreign_key: None,
skip: false,
rust_default: None,
join: None,
json: false,
}
}
#[doc(hidden)]
pub fn mock_join(name: &str, join_model: &str) -> Self {
Self {
name: name.to_string(),
ty: Type::Join(Box::new(Type::Inner(crate::InnerType::mock(join_model)))),
marked_primary_key: false,
has_database_default: false,
ident: Ident::from(name),
many_to_one_column_name: None,
many_to_many_table: None,
one_to_many_foreign_key: None,
skip: false,
rust_default: None,
join: None,
json: false,
}
}
}
#[derive(Clone, Debug)]
pub struct ForeignKey {
pub model: String,
pub column: String,
}
#[derive(StructMeta)]
pub struct ColumnAttr {
pub primary_key: Flag,
pub insertable_primary_key: Flag,
pub default: Flag,
pub default_value: Option<LitStr>,
pub join_column: Option<LitStr>,
pub many_to_many_table: Option<syn::Ident>,
pub one_to_many_foreign_key: Option<Path>,
pub column: Option<LitStr>,
pub skip: Flag,
pub json: Flag,
}
impl ColumnAttr {
pub fn from_attrs(ast: &[Attribute]) -> Vec<Self> {
ast.iter()
.filter(|a| a.path().is_ident("ormlite"))
.map(|a| a.parse_args().unwrap())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::{parse_quote, Attribute, Fields, ItemStruct};
#[test]
fn test_from_field() {
let item: ItemStruct = syn::parse_str(
r#"
struct Foo {
#[ormlite(default_value = "\"foo\".to_string()")]
pub name: String
}
"#,
)
.unwrap();
let Fields::Named(fields) = item.fields else {
panic!();
};
let field = fields.named.first().unwrap();
let column = ColumnMeta::from_field(field);
assert_eq!(column.name, "name");
assert_eq!(column.ty, "String");
assert_eq!(column.marked_primary_key, false);
assert_eq!(column.has_database_default, false);
assert_eq!(column.rust_default, Some("\"foo\".to_string()".to_string()));
assert_eq!(column.ident, "name");
}
#[test]
fn test_default() {
let attr: Attribute = parse_quote!(#[ormlite(default_value = "serde_json::Value::Null")]);
let args: ColumnAttr = attr.parse_args().unwrap();
assert!(args.default_value.is_some());
let attr: Attribute = parse_quote!(#[ormlite(default)]);
let args: ColumnAttr = attr.parse_args().unwrap();
assert!(args.default.value());
}
#[test]
fn test_join_column() {
let attr: Attribute = parse_quote!(#[ormlite(join_column = "org_id")]);
let args: ColumnAttr = attr.parse_args().unwrap();
assert!(args.join_column.is_some());
}
}