use syn::{Attribute, Meta};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndexType {
#[default]
BTree,
Hash,
Gin,
Gist,
Brin
}
impl IndexType {
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"btree" | "b-tree" => Some(Self::BTree),
"hash" => Some(Self::Hash),
"gin" => Some(Self::Gin),
"gist" => Some(Self::Gist),
"brin" => Some(Self::Brin),
_ => None
}
}
#[must_use]
pub fn as_sql_using(&self) -> &'static str {
match self {
Self::BTree => "",
Self::Hash => " USING hash",
Self::Gin => " USING gin",
Self::Gist => " USING gist",
Self::Brin => " USING brin"
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReferentialAction {
Cascade,
SetNull,
SetDefault,
Restrict,
NoAction
}
impl ReferentialAction {
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().replace([' ', '_'], "").as_str() {
"cascade" => Some(Self::Cascade),
"setnull" => Some(Self::SetNull),
"setdefault" => Some(Self::SetDefault),
"restrict" => Some(Self::Restrict),
"noaction" => Some(Self::NoAction),
_ => None
}
}
#[must_use]
pub fn as_sql(&self) -> &'static str {
match self {
Self::Cascade => "CASCADE",
Self::SetNull => "SET NULL",
Self::SetDefault => "SET DEFAULT",
Self::Restrict => "RESTRICT",
Self::NoAction => "NO ACTION"
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ColumnConfig {
pub unique: bool,
pub index: Option<IndexType>,
pub default: Option<String>,
pub check: Option<String>,
pub varchar: Option<usize>,
pub sql_type: Option<String>,
pub nullable: bool,
pub name: Option<String>
}
impl ColumnConfig {
pub fn from_attr(attr: &Attribute) -> Self {
let mut config = Self::default();
if let Meta::List(meta_list) = &attr.meta {
let _ = meta_list.parse_nested_meta(|meta| {
if meta.path.is_ident("unique") {
config.unique = true;
} else if meta.path.is_ident("index") {
if meta.input.peek(syn::Token![=]) {
let _: syn::Token![=] = meta.input.parse()?;
let value: syn::LitStr = meta.input.parse()?;
config.index =
Some(IndexType::from_str(&value.value()).unwrap_or_default());
} else {
config.index = Some(IndexType::default());
}
} else if meta.path.is_ident("default") {
let _: syn::Token![=] = meta.input.parse()?;
let value: syn::LitStr = meta.input.parse()?;
config.default = Some(value.value());
} else if meta.path.is_ident("check") {
let _: syn::Token![=] = meta.input.parse()?;
let value: syn::LitStr = meta.input.parse()?;
config.check = Some(value.value());
} else if meta.path.is_ident("varchar") {
let _: syn::Token![=] = meta.input.parse()?;
let value: syn::LitInt = meta.input.parse()?;
config.varchar = value.base10_parse().ok();
} else if meta.path.is_ident("sql_type") {
let _: syn::Token![=] = meta.input.parse()?;
let value: syn::LitStr = meta.input.parse()?;
config.sql_type = Some(value.value());
} else if meta.path.is_ident("nullable") {
config.nullable = true;
} else if meta.path.is_ident("name") {
let _: syn::Token![=] = meta.input.parse()?;
let value: syn::LitStr = meta.input.parse()?;
config.name = Some(value.value());
}
Ok(())
});
}
config
}
#[must_use]
#[allow(dead_code)] pub fn has_constraints(&self) -> bool {
self.unique || self.check.is_some()
}
#[must_use]
pub fn has_index(&self) -> bool {
self.index.is_some()
}
#[must_use]
pub fn column_name<'a>(&'a self, field_name: &'a str) -> &'a str {
self.name.as_deref().unwrap_or(field_name)
}
}
#[cfg(test)]
mod tests {
use quote::quote;
use syn::parse_quote;
use super::*;
fn parse_column_attr(tokens: proc_macro2::TokenStream) -> ColumnConfig {
let attr: Attribute = parse_quote!(#[column(#tokens)]);
ColumnConfig::from_attr(&attr)
}
#[test]
fn default_is_empty() {
let config = ColumnConfig::default();
assert!(!config.unique);
assert!(config.index.is_none());
assert!(config.default.is_none());
assert!(config.check.is_none());
assert!(config.varchar.is_none());
assert!(config.sql_type.is_none());
assert!(!config.nullable);
assert!(config.name.is_none());
}
#[test]
fn parse_unique() {
let config = parse_column_attr(quote! { unique });
assert!(config.unique);
}
#[test]
fn parse_index_default() {
let config = parse_column_attr(quote! { index });
assert_eq!(config.index, Some(IndexType::BTree));
}
#[test]
fn parse_index_gin() {
let config = parse_column_attr(quote! { index = "gin" });
assert_eq!(config.index, Some(IndexType::Gin));
}
#[test]
fn parse_index_hash() {
let config = parse_column_attr(quote! { index = "hash" });
assert_eq!(config.index, Some(IndexType::Hash));
}
#[test]
fn parse_default_value() {
let config = parse_column_attr(quote! { default = "true" });
assert_eq!(config.default, Some("true".to_string()));
}
#[test]
fn parse_default_now() {
let config = parse_column_attr(quote! { default = "NOW()" });
assert_eq!(config.default, Some("NOW()".to_string()));
}
#[test]
fn parse_check_constraint() {
let config = parse_column_attr(quote! { check = "age >= 0" });
assert_eq!(config.check, Some("age >= 0".to_string()));
}
#[test]
fn parse_varchar() {
let config = parse_column_attr(quote! { varchar = 255 });
assert_eq!(config.varchar, Some(255));
}
#[test]
fn parse_sql_type() {
let config = parse_column_attr(quote! { sql_type = "JSONB" });
assert_eq!(config.sql_type, Some("JSONB".to_string()));
}
#[test]
fn parse_nullable() {
let config = parse_column_attr(quote! { nullable });
assert!(config.nullable);
}
#[test]
fn parse_custom_name() {
let config = parse_column_attr(quote! { name = "user_name" });
assert_eq!(config.name, Some("user_name".to_string()));
}
#[test]
fn parse_multiple_attrs() {
let config = parse_column_attr(quote! { unique, index = "btree", default = "true" });
assert!(config.unique);
assert_eq!(config.index, Some(IndexType::BTree));
assert_eq!(config.default, Some("true".to_string()));
}
#[test]
fn has_constraints_check() {
let config = parse_column_attr(quote! { unique });
assert!(config.has_constraints());
let config2 = parse_column_attr(quote! { check = "x > 0" });
assert!(config2.has_constraints());
let config3 = ColumnConfig::default();
assert!(!config3.has_constraints());
}
#[test]
fn has_index_check() {
let config = parse_column_attr(quote! { index });
assert!(config.has_index());
let config2 = ColumnConfig::default();
assert!(!config2.has_index());
}
#[test]
fn column_name_default() {
let config = ColumnConfig::default();
assert_eq!(config.column_name("email"), "email");
}
#[test]
fn column_name_custom() {
let config = parse_column_attr(quote! { name = "user_email" });
assert_eq!(config.column_name("email"), "user_email");
}
#[test]
fn index_type_as_sql() {
assert_eq!(IndexType::BTree.as_sql_using(), "");
assert_eq!(IndexType::Hash.as_sql_using(), " USING hash");
assert_eq!(IndexType::Gin.as_sql_using(), " USING gin");
assert_eq!(IndexType::Gist.as_sql_using(), " USING gist");
assert_eq!(IndexType::Brin.as_sql_using(), " USING brin");
}
#[test]
fn index_type_from_str_all() {
assert_eq!(IndexType::from_str("btree"), Some(IndexType::BTree));
assert_eq!(IndexType::from_str("b-tree"), Some(IndexType::BTree));
assert_eq!(IndexType::from_str("BTREE"), Some(IndexType::BTree));
assert_eq!(IndexType::from_str("hash"), Some(IndexType::Hash));
assert_eq!(IndexType::from_str("HASH"), Some(IndexType::Hash));
assert_eq!(IndexType::from_str("gin"), Some(IndexType::Gin));
assert_eq!(IndexType::from_str("GIN"), Some(IndexType::Gin));
assert_eq!(IndexType::from_str("gist"), Some(IndexType::Gist));
assert_eq!(IndexType::from_str("GIST"), Some(IndexType::Gist));
assert_eq!(IndexType::from_str("brin"), Some(IndexType::Brin));
assert_eq!(IndexType::from_str("BRIN"), Some(IndexType::Brin));
assert_eq!(IndexType::from_str("invalid"), None);
assert_eq!(IndexType::from_str("unknown"), None);
}
#[test]
fn parse_index_gist() {
let config = parse_column_attr(quote! { index = "gist" });
assert_eq!(config.index, Some(IndexType::Gist));
}
#[test]
fn parse_index_brin() {
let config = parse_column_attr(quote! { index = "brin" });
assert_eq!(config.index, Some(IndexType::Brin));
}
#[test]
fn parse_index_unknown_defaults_to_btree() {
let config = parse_column_attr(quote! { index = "unknown" });
assert_eq!(config.index, Some(IndexType::BTree));
}
#[test]
fn referential_action_from_str() {
assert_eq!(
ReferentialAction::from_str("cascade"),
Some(ReferentialAction::Cascade)
);
assert_eq!(
ReferentialAction::from_str("SET NULL"),
Some(ReferentialAction::SetNull)
);
assert_eq!(
ReferentialAction::from_str("set_default"),
Some(ReferentialAction::SetDefault)
);
assert_eq!(
ReferentialAction::from_str("RESTRICT"),
Some(ReferentialAction::Restrict)
);
assert_eq!(
ReferentialAction::from_str("no action"),
Some(ReferentialAction::NoAction)
);
assert_eq!(ReferentialAction::from_str("invalid"), None);
}
#[test]
fn referential_action_as_sql() {
assert_eq!(ReferentialAction::Cascade.as_sql(), "CASCADE");
assert_eq!(ReferentialAction::SetNull.as_sql(), "SET NULL");
assert_eq!(ReferentialAction::SetDefault.as_sql(), "SET DEFAULT");
assert_eq!(ReferentialAction::Restrict.as_sql(), "RESTRICT");
assert_eq!(ReferentialAction::NoAction.as_sql(), "NO ACTION");
}
}