Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"b4a57b9dba27bff0f09c1e9a36f370e4fae55f8fe814c86166c43b354c331f0a","Cargo.lock":"d525283bd7305897b31a5f360901f262fe2d3d86d082f3d20fef31aed2678551","Cargo.toml":"5af7159a6e6d560964e192c0d0c510f6b98c6148131dfa10667f64b432803fc3","Cargo.toml.orig":"5e43f5fb911218c5f422f5673ecc89929ca1bc25f312cf3078d5abeb7bebc300","src/derives/active_enum.rs":"201a3cd5417637e6a8041b60b810aad95ea30b55966161cd433db844416510e0","src/derives/active_enum_display.rs":"2539085686b54a712e050781ac6a57288b1cfade3c3798c487dc284bdbd38a69","src/derives/active_model.rs":"765a3a31591718eeabc8de1888167ce3e5c65a60dcf931971ecedba228ca7b0e","src/derives/active_model_behavior.rs":"7b9067cbe1012768e0c312fa07f8d4e2e510316f36fa334bc56ae83ee297f83e","src/derives/active_model_ex.rs":"803510e012e2e8e851f3c773303d2677ede4461720eeab92d680d16af68141a9","src/derives/arrow_schema.rs":"d17eb69e73cd201a681b5eebd215dd0891858f87b5d1cbe9e224c93fac7f3218","src/derives/attributes.rs":"d73ef733e5a7423bcc1ecaf6b08a7508ce06c040f86ae5ba4ba260ac1fe0aac3","src/derives/case_style.rs":"83e81d5962d1ced96a296531305225547c90fd95feed61492df3b59e85ba6b99","src/derives/column.rs":"e700a9d220bb45de0bc31bdaaa52dc82296f8c22dcf0204800167a6a2d856003","src/derives/derive_iden.rs":"1da3d08af473fe1c67d318b03a288ab807d716569d69d32a8ccc48e9da499a0e","src/derives/entity.rs":"1ed11b6cefb05fa9f2c2288246d73631893d8879f659bdf570b61983eb84fc59","src/derives/entity_loader.rs":"b91d4ba099fae3e93df4f54956ebf592f78da09540e0c609e7216f5494f8ef94","src/derives/entity_model.rs":"9fffc12bcb742082993b1a4eba9f6aa0d806eb3ed4e9f8cc2fb8cbdd55fc8390","src/derives/from_query_result.rs":"6b7f545eadc96bff646995ff604a631d605458eaa06badd3c4a37c3b51d7c14b","src/derives/into_active_model.rs":"5d4e5d5d1c28b4622150ba9f4da6ebc1092c9074f2f6e76cb16d54efeb7b1051","src/derives/migration.rs":"6916b642d1e4c8d5057262111a081b64158aa9fdade0a9d36a988d879e6014b2","src/derives/mod.rs":"b722a18c4e8086e1a17d624001c944b30edd1659a1cd51863a10b409d1111a8c","src/derives/model.rs":"6d3b015022c6ba85dc68e3896bdd13138aca82ece8c091d6e64dadfcb2dc83ee","src/derives/model_ex.rs":"966d06209e0fd6224cea03dcda55d33fbc4485229a74eaee78c326edb8d4f899","src/derives/partial_model.rs":"91d0b5a36fcc321f881e7fbf464802407c2b716917988ba35a38564a56e992a2","src/derives/primary_key.rs":"9295639b0d9659a1027ae82556c9ee482bbfae762ec25fc534db47f3117f0359","src/derives/related_entity.rs":"524a770e42a8146c8f312afa7c0ab622faa6b74038afdb720f96336ec76567aa","src/derives/relation.rs":"a3e1b725cafc56c1cc26fa597badb198d3fdeb31e0f21492e764359d42016b4c","src/derives/try_getable_from_json.rs":"f78d32334bf19b3cb0fa6d9454045492a7f56ca21f740f022434f405380092d8","src/derives/typed_column.rs":"fcf9cba9aa8342942789fb7d17359b735ab81f535f206e90d2c8286786041f85","src/derives/util.rs":"2390b451f23010e5379b733b9aa0df93c26fa2c4f83dcefff6426b76c3443f2d","src/derives/value_type.rs":"b6538a9822bb0969563cea4d0c42ef23d3f2efd6457d01ac82a9317b60f14cbc","src/derives/value_type_match.rs":"7a724647ce96576fe7b37ea34c6018d0ade950a8a86048f57aba38a522bee9de","src/lib.rs":"3d48f6065e4b1a8fa3e84074ef78e84e3cfe434ec8f5ee1889d3877715b31598","src/raw_sql.rs":"e278572ddf23b0640a4f581709d317c2b56f7b78e204c497a4bd4fd2e4de8ec6","src/strum/LICENSE":"ac298f80457f15e1249fcab63d7a6e7ee0b3a4d80b52e3c6a7a3c58ad3bdc263","src/strum/enum_iter.rs":"f25711064da76e545e9b89945d909ca81e24de131724396f1b27a2bdb38e362d","src/strum/helpers/case_style.rs":"8556a6b1d812769692dc7823f79be4bf84a0f963b5b7c48b55f55dd1aa7cb973","src/strum/helpers/metadata.rs":"03f1b6af3de0bcc6b1849a26bf5c9360678e285979a9017b991b38ef1aa06c6f","src/strum/helpers/mod.rs":"2eeedffb7a2d323d476cd8208a19b5ceec3905172bfa1f0f99bc572da5f894ac","src/strum/helpers/type_props.rs":"e304f29bf1eb1119a122fda8836c6c34f76e801bec6e100e1972210e9c1fb99f","src/strum/helpers/variant_props.rs":"96189e92cd9d283411ef88e2a41a87f669208a6f4a17983b6f72c964e91aa36f","src/strum/mod.rs":"3cefeed7b5bcd3aafdea1e73c674fadacf8339da799b1e956f80081ffed99c6f","tests/derive_active_enum_test.rs":"bae2dfef35ecd5b9e97c74f426bf4eb871d5325b00ffdeda1faa8ea6c8d94d6c","tests/derive_entity_model_auto_increment_test.rs":"d42af8a4a616ae59796818f062ffa7c6f362dd9162454a2681ca85b7a7add9ad","tests/derive_entity_model_column_name_test.rs":"35409c93b0f095e78a16e3135f21d96d8d9fea8166d2c95d4f2c8019d75e4dcd","tests/derive_value_type_test.rs":"afa3e0cb511f0fcff656ee590f3d7fff0cdc7c5b5dd03df2bb35a7728ea78ae9"},"package":"b4cbb7ae053b9b37c919ea944a3bc52f7830a90c223f8a765c908293bca8f22d"}
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "fd3d3de8a37df4ff790ff1661bb572b9ec1350d0"
},
"path_in_vcs": "sea-orm-macros"
}
+276
View File
@@ -0,0 +1,276 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pluralizer"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe"
dependencies = [
"once_cell",
"regex",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "sea-bae"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25"
dependencies = [
"heck 0.4.1",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sea-orm-macros"
version = "2.0.0"
dependencies = [
"heck 0.5.0",
"itertools",
"pluralizer",
"proc-macro-crate",
"proc-macro2",
"quote",
"sea-bae",
"serde",
"syn",
"unicode-ident",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "winnow"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
dependencies = [
"memchr",
]
+116
View File
@@ -0,0 +1,116 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
rust-version = "1.85.0"
name = "sea-orm-macros"
version = "2.0.0"
authors = ["Billy Chan <ccw.billy.123@gmail.com>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Derive macros for SeaORM"
homepage = "https://www.sea-ql.org/SeaORM"
documentation = "https://docs.rs/sea-orm"
readme = false
keywords = [
"async",
"orm",
"mysql",
"postgres",
"sqlite",
]
categories = ["database"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/SeaQL/sea-orm"
[features]
async = []
default = ["derive"]
derive = ["bae"]
entity-registry = []
postgres-array = []
seaography = ["proc-macro-crate"]
sqlx-postgres = []
strum = []
with-arrow = []
with-json = []
[lib]
name = "sea_orm_macros"
path = "src/lib.rs"
proc-macro = true
[[test]]
name = "derive_active_enum_test"
path = "tests/derive_active_enum_test.rs"
[[test]]
name = "derive_entity_model_auto_increment_test"
path = "tests/derive_entity_model_auto_increment_test.rs"
[[test]]
name = "derive_entity_model_column_name_test"
path = "tests/derive_entity_model_column_name_test.rs"
[[test]]
name = "derive_value_type_test"
path = "tests/derive_value_type_test.rs"
[dependencies.bae]
version = "0.2"
optional = true
default-features = false
package = "sea-bae"
[dependencies.heck]
version = "0.5"
default-features = false
[dependencies.itertools]
version = "0.14"
[dependencies.pluralizer]
version = "0.5"
[dependencies.proc-macro-crate]
version = "3.2.0"
optional = true
[dependencies.proc-macro2]
version = "1"
default-features = false
[dependencies.quote]
version = "1"
default-features = false
[dependencies.syn]
version = "2"
features = [
"parsing",
"proc-macro",
"derive",
"printing",
"extra-traits",
]
default-features = false
[dependencies.unicode-ident]
version = "1"
[dev-dependencies.serde]
version = "1.0"
features = ["derive"]
+54
View File
@@ -0,0 +1,54 @@
[package]
authors = ["Billy Chan <ccw.billy.123@gmail.com>"]
categories = ["database"]
description = "Derive macros for SeaORM"
documentation = "https://docs.rs/sea-orm"
edition = "2024"
homepage = "https://www.sea-ql.org/SeaORM"
keywords = ["async", "orm", "mysql", "postgres", "sqlite"]
license = "MIT OR Apache-2.0"
name = "sea-orm-macros"
repository = "https://github.com/SeaQL/sea-orm"
rust-version = "1.85.0"
version = "2.0.0"
[lib]
name = "sea_orm_macros"
path = "src/lib.rs"
proc-macro = true
[dependencies]
bae = { version = "0.2", package = "sea-bae", default-features = false, optional = true }
heck = { version = "0.5", default-features = false }
itertools = "0.14"
pluralizer = { version = "0.5" }
proc-macro-crate = { version = "3.2.0", optional = true }
proc-macro2 = { version = "1", default-features = false }
quote = { version = "1", default-features = false }
syn = { version = "2", default-features = false, features = [
"parsing",
"proc-macro",
"derive",
"printing",
"extra-traits",
] }
unicode-ident = { version = "1" }
[dev-dependencies]
sea-orm = { path = "../", default-features = false, features = [
"macros",
"tests-cfg",
] }
serde = { version = "1.0", features = ["derive"] }
[features]
async = []
default = ["derive"]
derive = ["bae"]
entity-registry = []
postgres-array = []
seaography = ["proc-macro-crate"]
sqlx-postgres = []
strum = []
with-arrow = []
with-json = []
@@ -0,0 +1,837 @@
use super::case_style::{CaseStyle, CaseStyleHelpers};
use super::util::camel_case_with_escaped_non_uax31;
use heck::ToUpperCamelCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use syn::{Expr, Lit, LitInt, LitStr, UnOp, parse};
struct ActiveEnum {
ident: syn::Ident,
enum_name: String,
rs_type: RsType,
db_type: DbType,
is_string: bool,
variants: Vec<ActiveEnumVariant>,
variant_idents: Vec<syn::Ident>,
variant_values: Vec<TokenStream>,
rename_all: Option<CaseStyle>,
}
enum RsType {
String,
Enum,
Other(TokenStream),
}
impl RsType {
fn from_attr(
ident_span: proc_macro2::Span,
rs_type: Option<String>,
db_type: &DbType,
) -> Result<Self, Error> {
if db_type.is_enum() {
match rs_type.as_deref() {
None => Ok(RsType::Enum),
Some(value) => RsType::from_database_enum_attr_value(value).ok_or_else(|| {
Error::TT(quote_spanned! {
ident_span => compile_error!("`db_type = \"Enum\"` only supports `rs_type = \"String\"` or `rs_type = \"Enum\"` (or omit `rs_type`)");
})
}),
}
} else {
let rs_type = match rs_type {
Some(rs_type) => rs_type,
None => {
return Err(Error::TT(quote_spanned! {
ident_span => compile_error!("Missing macro attribute `rs_type`");
}));
}
};
if rs_type == "Enum" {
return Err(Error::TT(quote_spanned! {
ident_span => compile_error!("`rs_type = \"Enum\"` requires `db_type = \"Enum\"`");
}));
}
RsType::from_str(&rs_type).map_err(Error::Syn)
}
}
fn from_str(value: &str) -> syn::Result<Self> {
Ok(Self::Other(syn::parse_str::<TokenStream>(value)?))
}
fn from_database_enum_attr_value(value: &str) -> Option<Self> {
match value {
"Enum" => Some(Self::Enum),
"String" => Some(Self::String),
_ => None,
}
}
}
impl quote::ToTokens for RsType {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
RsType::String => tokens.extend(quote! { String }),
RsType::Enum => tokens.extend(quote! { sea_orm::sea_query::Enum }),
RsType::Other(rs_type) => tokens.extend(rs_type.clone()),
}
}
}
enum DbType {
Enum,
Other(TokenStream),
}
impl DbType {
fn from_attr(ident_span: proc_macro2::Span, db_type: Option<String>) -> Result<Self, Error> {
let db_type = match db_type {
Some(db_type) => db_type,
None => {
return Err(Error::TT(quote_spanned! {
ident_span => compile_error!("Missing macro attribute `db_type`");
}));
}
};
DbType::from_str(&db_type).map_err(Error::Syn)
}
fn from_str(value: &str) -> syn::Result<Self> {
match value {
"Enum" => Ok(Self::Enum),
_ => Ok(Self::Other(syn::parse_str::<TokenStream>(value)?)),
}
}
fn is_enum(&self) -> bool {
matches!(self, DbType::Enum)
}
}
struct ActiveEnumVariant {
ident: syn::Ident,
string_value: Option<LitStr>,
num_value: Option<LitInt>,
rename: Option<CaseStyle>,
}
enum Error {
InputNotEnum,
Syn(syn::Error),
TT(TokenStream),
}
impl ActiveEnum {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let ident_span = input.ident.span();
let ident = input.ident;
let mut enum_name = ident.to_string().to_upper_camel_case();
let mut rs_type = None;
let mut db_type = None;
let mut rename_all = None;
input
.attrs
.iter()
.filter(|attr| attr.path().is_ident("sea_orm"))
.try_for_each(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rs_type") {
let litstr: LitStr = meta.value()?.parse()?;
rs_type = Some(litstr.value());
} else if meta.path.is_ident("db_type") {
let litstr: LitStr = meta.value()?.parse()?;
db_type = Some(litstr.value());
} else if meta.path.is_ident("enum_name") {
let litstr: LitStr = meta.value()?.parse()?;
enum_name = litstr.value();
} else if meta.path.is_ident("rename_all") {
rename_all = Some((&meta).try_into()?);
} else {
return Err(meta.error(format!(
"Unknown attribute parameter found: {:?}",
meta.path.get_ident()
)));
}
Ok(())
})
.map_err(Error::Syn)
})?;
let db_type = DbType::from_attr(ident_span, db_type)?;
let rs_type = RsType::from_attr(ident_span, rs_type, &db_type)?;
let variant_vec = match input.data {
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => return Err(Error::InputNotEnum),
};
let mut is_string = rename_all.is_some();
let mut is_int = false;
let mut variants = Vec::new();
for variant in variant_vec {
let variant_span = variant.ident.span();
let mut string_value = None;
let mut num_value = None;
let mut rename_rule = None;
for attr in variant.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("string_value") {
is_string = true;
string_value = Some(meta.value()?.parse::<LitStr>()?);
} else if meta.path.is_ident("num_value") {
is_int = true;
num_value = Some(meta.value()?.parse::<LitInt>()?);
} else if meta.path.is_ident("display_value") {
// This is a placeholder to prevent the `display_value` proc_macro attribute of `DeriveDisplay`
// to be considered unknown attribute parameter
meta.value()?.parse::<LitStr>()?;
} else if meta.path.is_ident("rename") {
is_string = true;
rename_rule = Some((&meta).try_into()?);
} else {
return Err(meta.error(format!(
"Unknown attribute parameter found: {:?}",
meta.path.get_ident()
)));
}
Ok(())
})
.map_err(Error::Syn)?;
}
if is_string && is_int {
return Err(Error::TT(quote_spanned! {
ident_span => compile_error!("All enum variants should specify the same `*_value` macro attribute, either `string_value` or `num_value` but not both");
}));
}
if string_value.is_none() && num_value.is_none() && rename_rule.or(rename_all).is_none()
{
match variant.discriminant {
Some((_, Expr::Lit(exprlit))) => {
if let Lit::Int(litint) = exprlit.lit {
is_int = true;
num_value = Some(litint);
} else {
return Err(Error::TT(quote_spanned! {
variant_span => compile_error!("Enum variant discriminant is not an integer");
}));
}
}
//rust doesn't provide negative variants in enums as a single LitInt, this workarounds that
Some((_, Expr::Unary(exprnlit))) => {
if let UnOp::Neg(_) = exprnlit.op {
if let Expr::Lit(exprlit) = *exprnlit.expr {
if let Lit::Int(litint) = exprlit.lit {
let negative_token = quote! { -#litint };
let litint = parse(negative_token.into()).unwrap();
is_int = true;
num_value = Some(litint);
}
}
} else {
return Err(Error::TT(quote_spanned! {
variant_span => compile_error!("Only - token is supported in enum variants, not ! and *");
}));
}
}
_ => {
return Err(Error::TT(quote_spanned! {
variant_span => compile_error!("Missing macro attribute, either `string_value`, `num_value` or `rename` should be specified or specify repr[X] and have a value for every entry");
}));
}
}
}
variants.push(ActiveEnumVariant {
ident: variant.ident,
string_value,
num_value,
rename: rename_rule,
});
}
if db_type.is_enum() && is_int {
return Err(Error::TT(quote_spanned! {
ident_span => compile_error!("`db_type = \"Enum\"` does not support `num_value` or numeric discriminants");
}));
}
let variant_idents: Vec<syn::Ident> = variants
.iter()
.map(|variant| variant.ident.clone())
.collect();
let variant_values: Vec<TokenStream> = variants
.iter()
.map(|variant| {
let variant_span = variant.ident.span();
if let Some(string_value) = &variant.string_value {
let string = string_value.value();
Ok(quote! { #string })
} else if let Some(num_value) = &variant.num_value {
Ok(quote! { #num_value })
} else if let Some(rename_rule) = variant.rename.or(rename_all) {
let variant_ident = variant.ident.convert_case(Some(rename_rule));
Ok(quote! { #variant_ident })
} else {
Err(Error::TT(quote_spanned! {
variant_span => compile_error!("Missing macro attribute, either `string_value`, `num_value` or `rename_all` should be specified");
}))
}
})
.collect::<Result<_, _>>()?;
Ok(Self {
ident,
enum_name,
rs_type,
db_type,
is_string,
variants,
variant_idents,
variant_values,
rename_all,
})
}
fn generate_enum_impls(&self) -> bool {
self.db_type.is_enum() && matches!(self.rs_type, RsType::Enum)
}
fn to_value_impl(&self) -> TokenStream {
let enum_name = &self.enum_name;
let variant_idents = &self.variant_idents;
let variant_values = &self.variant_values;
if self.generate_enum_impls() {
quote! {
let value = match self {
#( Self::#variant_idents => #variant_values, )*
};
sea_orm::sea_query::Enum {
type_name: #enum_name.into(),
value: value.into(),
}
}
} else {
quote! {
match self {
#( Self::#variant_idents => #variant_values, )*
}
.to_owned()
}
}
}
fn value_type_try_from_impl(&self) -> TokenStream {
if self.generate_enum_impls() {
quote! {
use sea_orm::sea_query::{OptionEnum, Value, ValueTypeErr};
match v {
Value::Enum(value) => match value {
OptionEnum::Some(value) => <Self as sea_orm::ActiveEnum>::try_from_value(value.as_ref())
.map_err(|_| ValueTypeErr),
OptionEnum::None(_) => Err(ValueTypeErr),
},
_ => Err(ValueTypeErr),
}
}
} else {
quote! {
use sea_orm::sea_query::{ValueType, ValueTypeErr};
let value = <<Self as sea_orm::ActiveEnum>::Value as ValueType>::try_from(v)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(|_| ValueTypeErr)
}
}
}
fn nullable_impl(&self) -> TokenStream {
let ident = &self.ident;
let enum_name = &self.enum_name;
let nullable_value_impl = if self.generate_enum_impls() {
quote! {
use sea_orm::sea_query::{OptionEnum, Value};
Value::Enum(OptionEnum::None(#enum_name.into()))
}
} else {
quote! {
use sea_orm::sea_query;
<<Self as sea_orm::ActiveEnum>::Value as sea_query::Nullable>::null()
}
};
quote! {
#[automatically_derived]
#[allow(unexpected_cfgs)]
impl sea_orm::sea_query::Nullable for #ident {
fn null() -> sea_orm::sea_query::Value {
#nullable_value_impl
}
}
}
}
fn value_type_impl(&self) -> TokenStream {
let ident = &self.ident;
let value_type_try_from_impl = self.value_type_try_from_impl();
let enum_name = &self.enum_name;
let type_name_impl = quote! { stringify!(#ident).to_owned() };
let value_type_array_type = if self.generate_enum_impls() {
quote! {
sea_orm::sea_query::ArrayType::Enum(Box::new(#enum_name.into()))
}
} else {
quote! {
<<Self as sea_orm::ActiveEnum>::Value as sea_orm::sea_query::ValueType>::array_type()
}
};
let enum_type_name = if self.db_type.is_enum() {
quote! { Some(#enum_name) }
} else {
quote! { None }
};
quote! {
#[automatically_derived]
#[allow(unexpected_cfgs)]
impl sea_orm::sea_query::ValueType for #ident {
fn try_from(v: sea_orm::sea_query::Value) -> std::result::Result<Self, sea_orm::sea_query::ValueTypeErr> {
#value_type_try_from_impl
}
fn type_name() -> String {
#type_name_impl
}
fn array_type() -> sea_orm::sea_query::ArrayType {
#value_type_array_type
}
fn column_type() -> sea_orm::sea_query::ColumnType {
<Self as sea_orm::ActiveEnum>::db_type()
.get_column_type()
.to_owned()
.into()
}
fn enum_type_name() -> Option<&'static str> {
#enum_type_name
}
}
}
}
fn try_getable_impl(&self) -> TokenStream {
let ident = &self.ident;
let sqlx_postgres_try_get = if cfg!(feature = "sqlx-postgres") && self.db_type.is_enum() {
quote! {
if let Some(result) = res.try_get_from_sqlx_postgres::<Self, I>(idx) {
return result;
}
}
} else {
quote!()
};
let try_get_by_impl = {
let enum_name = &self.enum_name;
if self.generate_enum_impls() {
quote! {
#sqlx_postgres_try_get
let value: String = <String as sea_orm::TryGetable>::try_get_by(res, idx)?;
let value = sea_orm::sea_query::Enum {
type_name: #enum_name.into(),
value: value.into(),
};
<Self as sea_orm::ActiveEnum>::try_from_value(&value)
.map_err(sea_orm::TryGetError::DbErr)
}
} else {
quote! {
#sqlx_postgres_try_get
let value = <<Self as sea_orm::ActiveEnum>::Value as sea_orm::TryGetable>::try_get_by(res, idx)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value)
.map_err(sea_orm::TryGetError::DbErr)
}
}
};
quote! {
#[automatically_derived]
impl sea_orm::TryGetable for #ident {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, idx: I) -> std::result::Result<Self, sea_orm::TryGetError> {
#try_get_by_impl
}
}
}
}
fn active_enum_impl(&self) -> TokenStream {
let ident = &self.ident;
let enum_name_iden = format_ident!("{}Enum", ident);
let rs_type = &self.rs_type;
let variant_idents = &self.variant_idents;
let variant_values = &self.variant_values;
let to_value_body = self.to_value_impl();
let column_type = {
match &self.db_type {
DbType::Enum => quote! {
Enum {
name: <Self as sea_orm::ActiveEnum>::name(),
variants: Self::iden_values(),
}
},
DbType::Other(db_type) => db_type.clone(),
}
};
let val = if self.generate_enum_impls() {
quote! { v.value.as_ref() }
} else if self.is_string {
quote! { <<Self as sea_orm::ActiveEnum>::Value as std::convert::AsRef<str>>::as_ref(v) }
} else {
quote! { v }
};
quote! {
#[automatically_derived]
impl sea_orm::ActiveEnum for #ident {
type Value = #rs_type;
type ValueVec = Vec<#rs_type>;
fn name() -> sea_orm::sea_query::DynIden {
#enum_name_iden.into()
}
fn to_value(&self) -> <Self as sea_orm::ActiveEnum>::Value {
#to_value_body
}
fn try_from_value(v: &<Self as sea_orm::ActiveEnum>::Value) -> std::result::Result<Self, sea_orm::DbErr> {
match #val {
#( #variant_values => Ok(Self::#variant_idents), )*
_ => Err(sea_orm::DbErr::Type(format!(
"unexpected value for {} enum: {}",
stringify!(#ident),
#val
))),
}
}
fn db_type() -> sea_orm::ColumnDef {
sea_orm::prelude::ColumnTypeTrait::def(sea_orm::ColumnType::#column_type)
}
}
}
}
fn convert_impls(&self) -> TokenStream {
let ident = &self.ident;
let variant_idents = &self.variant_idents;
let variant_values = &self.variant_values;
let try_from_str_impl = if self.db_type.is_enum() {
quote! {
#[automatically_derived]
impl std::convert::TryFrom<&str> for #ident {
type Error = sea_orm::DbErr;
fn try_from(source: &str) -> std::result::Result<Self, Self::Error> {
match source {
#( #variant_values => Ok(Self::#variant_idents), )*
_ => Err(sea_orm::DbErr::Type(format!(
"unexpected value for {} enum: {}",
stringify!(#ident),
source
))),
}
}
}
}
} else {
quote!()
};
if self.generate_enum_impls() {
let enum_name = &self.enum_name;
quote! {
#[automatically_derived]
impl std::convert::From<#ident> for sea_orm::sea_query::Enum {
fn from(source: #ident) -> Self {
let value = match source {
#( #ident::#variant_idents => #variant_values, )*
};
Self {
type_name: #enum_name.into(),
value: value.into(),
}
}
}
#[automatically_derived]
impl std::convert::From<#ident> for sea_orm::sea_query::Value {
fn from(source: #ident) -> Self {
let enum_value = sea_orm::sea_query::Enum::from(source);
sea_orm::sea_query::Value::from(enum_value)
}
}
#try_from_str_impl
}
} else {
quote! {
#[automatically_derived]
impl std::convert::From<#ident> for sea_orm::sea_query::Value {
fn from(source: #ident) -> Self {
<#ident as sea_orm::ActiveEnum>::to_value(&source).into()
}
}
#try_from_str_impl
}
}
}
fn sqlx_postgres_impl(&self) -> TokenStream {
if !cfg!(feature = "sqlx-postgres") || !self.db_type.is_enum() {
return quote!();
}
let ident = &self.ident;
let enum_name = &self.enum_name;
let ident_s = ident.to_string();
let variant_idents = &self.variant_idents;
let variant_values = &self.variant_values;
quote! {
#[automatically_derived]
impl sea_orm::sqlx::Type<sea_orm::sqlx::Postgres> for #ident {
fn type_info() -> sea_orm::sqlx::postgres::PgTypeInfo {
sea_orm::sqlx::postgres::PgTypeInfo::with_name(#enum_name)
}
}
#[automatically_derived]
impl<'r> sea_orm::sqlx::decode::Decode<'r, sea_orm::sqlx::Postgres> for #ident {
fn decode(
value: sea_orm::sqlx::postgres::PgValueRef<'r>,
) -> std::result::Result<
Self,
std::boxed::Box<
dyn std::error::Error + 'static + std::marker::Send + std::marker::Sync,
>,
> {
let value = <&'r str as sea_orm::sqlx::decode::Decode<
'r,
sea_orm::sqlx::Postgres,
>>::decode(value)?;
match value {
#( #variant_values => Ok(Self::#variant_idents), )*
_ => Err(format!("invalid value {:?} for enum {}", value, #ident_s).into()),
}
}
}
#[automatically_derived]
impl sea_orm::sqlx::postgres::PgHasArrayType for #ident {
fn array_type_info() -> sea_orm::sqlx::postgres::PgTypeInfo {
sea_orm::sqlx::postgres::PgTypeInfo::array_of(#enum_name)
}
}
}
}
fn try_getable_array_impl(&self) -> TokenStream {
let ident = &self.ident;
if cfg!(feature = "postgres-array") {
let sqlx_postgres_try_get = if cfg!(feature = "sqlx-postgres") && self.db_type.is_enum()
{
quote! {
if let Some(result) = res.try_get_from_sqlx_postgres::<Vec<Self>, I>(index) {
return result;
}
}
} else {
quote!()
};
quote!(
#[automatically_derived]
impl sea_orm::TryGetableArray for #ident {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, index: I) -> std::result::Result<Vec<Self>, sea_orm::TryGetError> {
#sqlx_postgres_try_get
<<Self as sea_orm::ActiveEnum>::Value as sea_orm::ActiveEnumValue>::try_get_vec_by(res, index)?
.into_iter()
.map(|value| <Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(Into::into))
.collect()
}
}
)
} else {
quote!()
}
}
fn expand(&self) -> TokenStream {
let Self {
ident,
enum_name,
variants,
rename_all,
..
} = self;
let enum_name_iden = format_ident!("{}Enum", ident);
let str_variants: Vec<String> = variants
.iter()
.filter_map(|variant| {
variant
.string_value
.as_ref()
.map(|string_value| string_value.value())
.or(variant
.rename
.map(|rename| variant.ident.convert_case(Some(rename))))
.or_else(|| rename_all.map(|rule| variant.ident.convert_case(Some(rule))))
})
.collect();
let impl_enum_variant_iden = if !str_variants.is_empty() {
let enum_variant_iden = format_ident!("{}Variant", ident);
let enum_variants: Vec<syn::Ident> = str_variants
.iter()
.map(|v| {
let v_cleaned = camel_case_with_escaped_non_uax31(v);
format_ident!("{}", v_cleaned)
})
.collect();
quote!(
#[doc = " Generated by sea-orm-macros"]
#[derive(Debug, Clone, PartialEq, Eq, sea_orm::EnumIter)]
pub enum #enum_variant_iden {
#(
#[doc = " Generated by sea-orm-macros"]
#enum_variants,
)*
}
#[automatically_derived]
impl sea_orm::Iden for #enum_variant_iden {
fn unquoted(&self) -> &str {
match self {
#(
Self::#enum_variants => #str_variants,
)*
}
}
}
#[automatically_derived]
impl #ident {
#[doc = " Generated by sea-orm-macros"]
pub fn iden_values() -> Vec<sea_orm::sea_query::DynIden> {
<#enum_variant_iden as sea_orm::strum::IntoEnumIterator>::iter()
// TODO: Use DynIden constructor
.map(|v| sea_orm::sea_query::SeaRc::new(v) as sea_orm::sea_query::DynIden)
.collect()
}
}
)
} else {
quote!()
};
let not_u8_impl = if cfg!(feature = "postgres-array") {
quote!(
#[automatically_derived]
impl sea_orm::sea_query::postgres_array::NotU8 for #ident {}
)
} else {
quote!()
};
let value_type_impl = self.value_type_impl();
let convert_impls = self.convert_impls();
let nullable_impl = self.nullable_impl();
let impl_try_getable_array = self.try_getable_array_impl();
let active_enum_impl = self.active_enum_impl();
let try_getable_impl = self.try_getable_impl();
let sqlx_postgres_impl = self.sqlx_postgres_impl();
quote!(
#[doc = " Generated by sea-orm-macros"]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct #enum_name_iden;
#[automatically_derived]
impl sea_orm::Iden for #enum_name_iden {
fn unquoted(&self) -> &str {
#enum_name
}
}
#impl_enum_variant_iden
#active_enum_impl
#impl_try_getable_array
#convert_impls
#try_getable_impl
#sqlx_postgres_impl
#value_type_impl
#nullable_impl
#[automatically_derived]
impl sea_orm::IntoActiveValue<#ident> for #ident {
fn into_active_value(self) -> sea_orm::ActiveValue<#ident> {
sea_orm::ActiveValue::set(self)
}
}
#not_u8_impl
)
}
}
pub fn expand_derive_active_enum(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match ActiveEnum::new(input) {
Ok(model) => Ok(model.expand()),
Err(Error::InputNotEnum) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive ActiveEnum on enums");
}),
Err(Error::TT(token_stream)) => Ok(token_stream),
Err(Error::Syn(e)) => Err(e),
}
}
@@ -0,0 +1,108 @@
use super::case_style::CaseStyle;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote, quote_spanned};
use syn::{LitInt, LitStr};
enum Error {
InputNotEnum,
Syn(syn::Error),
}
struct Display {
ident: syn::Ident,
variants: Vec<DisplayVariant>,
}
struct DisplayVariant {
ident: syn::Ident,
display_value: TokenStream,
}
impl Display {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let ident = input.ident;
let variant_vec = match input.data {
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => return Err(Error::InputNotEnum),
};
let mut variants = Vec::new();
for variant in variant_vec {
let mut display_value = variant.ident.to_string().to_token_stream();
for attr in variant.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("string_value") {
meta.value()?.parse::<LitStr>()?;
} else if meta.path.is_ident("num_value") {
meta.value()?.parse::<LitInt>()?;
} else if meta.path.is_ident("display_value") {
display_value = meta.value()?.parse::<LitStr>()?.to_token_stream();
} else if meta.path.is_ident("rename") {
CaseStyle::try_from(&meta)?;
} else {
return Err(meta.error(format!(
"Unknown attribute parameter found: {:?}",
meta.path.get_ident()
)));
}
Ok(())
})
.map_err(Error::Syn)?;
}
variants.push(DisplayVariant {
ident: variant.ident,
display_value,
});
}
Ok(Display { ident, variants })
}
fn expand(&self) -> syn::Result<TokenStream> {
let expanded_impl_active_enum_display = self.impl_active_enum_display();
Ok(expanded_impl_active_enum_display)
}
fn impl_active_enum_display(&self) -> TokenStream {
let Self { ident, variants } = self;
let variant_idents: Vec<_> = variants
.iter()
.map(|variant| variant.ident.clone())
.collect();
let variant_display: Vec<_> = variants
.iter()
.map(|variant| variant.display_value.to_owned())
.collect();
quote!(
#[automatically_derived]
impl std::fmt::Display for #ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
#( Self::#variant_idents => #variant_display, )*
})
}
}
)
}
}
pub fn expand_derive_active_enum_display(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match Display::new(input) {
Ok(model) => model.expand(),
Err(Error::InputNotEnum) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive EnumDisplay on enums");
}),
Err(Error::Syn(e)) => Err(e),
}
}
@@ -0,0 +1,304 @@
use super::util::{escape_rust_keyword, field_not_ignored, trim_starting_raw_identifier};
use heck::ToUpperCamelCase;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{Data, DataStruct, Expr, Fields, LitStr, Type, Visibility};
pub(crate) struct DeriveActiveModel {
model: Ident,
vis: Visibility,
fields: Vec<Ident>,
names: Vec<Ident>,
types: Vec<Type>,
}
impl DeriveActiveModel {
pub fn new(vis: &Visibility, ident: &Ident, data: &Data) -> syn::Result<Self> {
let all_fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(named),
..
}) => &named.named,
_ => {
return Err(syn::Error::new_spanned(
ident,
"You can only derive DeriveActiveModel on structs",
));
}
};
let mut fields = Vec::new();
let mut names = Vec::new();
let mut types = Vec::new();
for field in all_fields.iter().filter(|f| field_not_ignored(f)) {
let field_ident = field.ident.as_ref().expect("named fields have identifiers");
fields.push(field_ident.clone());
let ident = field_ident.to_string();
let ident = trim_starting_raw_identifier(ident).to_upper_camel_case();
let ident = escape_rust_keyword(ident);
let mut ident = format_ident!("{}", &ident);
field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("sea_orm"))
.try_for_each(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("enum_name") {
let litstr: LitStr = meta.value()?.parse()?;
ident = syn::parse_str(&litstr.value()).unwrap();
} else {
// Reads the value expression to advance the parse stream.
// Some parameters, such as `primary_key`, do not have any value,
// so ignoring an error occurred here.
let _: Option<Expr> = meta.value().and_then(|v| v.parse()).ok();
}
Ok(())
})
})?;
names.push(ident);
types.push(field.ty.clone());
}
Ok(DeriveActiveModel {
model: ident.clone(),
vis: vis.clone(),
fields,
names,
types,
})
}
}
impl DeriveActiveModel {
fn define_active_model(&self) -> TokenStream {
let vis = &self.vis;
let fields = &self.fields;
let types = &self.types;
quote!(
#[doc = " Generated by sea-orm-macros"]
#[derive(Clone, Debug, PartialEq)]
#vis struct ActiveModel {
#(
#[doc = " Generated by sea-orm-macros"]
pub #fields: sea_orm::ActiveValue<#types>
),*
}
)
}
fn impl_active_model(&self) -> TokenStream {
let mut ts = self.impl_active_model_convert();
ts.extend(self.impl_active_model_trait());
ts
}
fn impl_active_model_convert(&self) -> TokenStream {
let model = &self.model;
let fields = &self.fields;
quote!(
#[automatically_derived]
impl std::default::Default for ActiveModel {
fn default() -> Self {
<Self as sea_orm::ActiveModelBehavior>::new()
}
}
#[automatically_derived]
impl std::convert::From<#model> for ActiveModel {
fn from(m: #model) -> Self {
Self {
#(#fields: sea_orm::ActiveValue::Unchanged(m.#fields)),*
}
}
}
#[automatically_derived]
impl sea_orm::IntoActiveModel<ActiveModel> for #model {
fn into_active_model(self) -> ActiveModel {
self.into()
}
}
)
}
fn impl_active_model_trait(&self) -> TokenStream {
let fields = &self.fields;
let methods = self.impl_active_model_trait_methods();
quote! {
#[automatically_derived]
impl sea_orm::ActiveModelTrait for ActiveModel {
type Entity = Entity;
#methods
fn default() -> Self {
Self {
#(#fields: sea_orm::ActiveValue::NotSet),*
}
}
}
}
}
pub fn impl_active_model_trait_methods(&self) -> TokenStream {
let fields = &self.fields;
let names = &self.names;
quote!(
fn take(&mut self, c: <Self::Entity as sea_orm::EntityTrait>::Column) -> sea_orm::ActiveValue<sea_orm::Value> {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => {
let mut value = sea_orm::ActiveValue::NotSet;
std::mem::swap(&mut value, &mut self.#fields);
value.into_wrapped_value()
},)*
_ => sea_orm::ActiveValue::NotSet,
}
}
fn get(&self, c: <Self::Entity as sea_orm::EntityTrait>::Column) -> sea_orm::ActiveValue<sea_orm::Value> {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => self.#fields.clone().into_wrapped_value(),)*
_ => sea_orm::ActiveValue::NotSet,
}
}
fn set_if_not_equals(&mut self, c: <Self::Entity as sea_orm::EntityTrait>::Column, v: sea_orm::Value) {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => self.#fields.set_if_not_equals(v.unwrap()),)*
_ => (),
}
}
fn try_set(&mut self, c: <Self::Entity as sea_orm::EntityTrait>::Column, v: sea_orm::Value) -> Result<(), sea_orm::DbErr> {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => self.#fields = sea_orm::ActiveValue::Set(sea_orm::sea_query::ValueType::try_from(v).map_err(|e| sea_orm::DbErr::Type(e.to_string()))?),)*
_ => return Err(sea_orm::DbErr::Type(format!("ActiveModel does not have this field: {:?}", sea_orm::ColumnTrait::as_column_ref(&c)))),
}
Ok(())
}
fn not_set(&mut self, c: <Self::Entity as sea_orm::EntityTrait>::Column) {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => self.#fields = sea_orm::ActiveValue::NotSet,)*
_ => (),
}
}
fn is_not_set(&self, c: <Self::Entity as sea_orm::EntityTrait>::Column) -> bool {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => self.#fields.is_not_set(),)*
_ => panic!("This ActiveModel does not have this field"),
}
}
fn reset(&mut self, c: <Self::Entity as sea_orm::EntityTrait>::Column) {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#names => self.#fields.reset(),)*
_ => panic!("This ActiveModel does not have this field"),
}
}
fn default_values() -> Self {
use sea_orm::value::{DefaultActiveValue, DefaultActiveValueNone, DefaultActiveValueNotSet};
let mut default = <Self as sea_orm::ActiveModelTrait>::default();
#(default.#fields = (&default.#fields).default_value();)*
default
}
)
}
}
fn derive_into_model(ident: &Ident, data: &Data) -> syn::Result<TokenStream> {
let model_fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(named),
..
}) => &named.named,
_ => {
return Err(syn::Error::new_spanned(
ident,
"You can only derive DeriveActiveModel on structs",
));
}
};
let active_model_field: Vec<Ident> = model_fields
.iter()
.filter(|f| field_not_ignored(f))
.map(|field| field.ident.clone().expect("named fields have identifiers"))
.collect();
let model_field: Vec<Ident> = model_fields
.iter()
.map(|field| field.ident.clone().expect("named fields have identifiers"))
.collect();
let ignore_attr: Vec<bool> = model_fields.iter().map(|f| !field_not_ignored(f)).collect();
let model_field_value: Vec<TokenStream> = model_field
.iter()
.zip(ignore_attr)
.map(|(field, ignore)| {
if ignore {
quote! {
Default::default()
}
} else {
quote! {
a.#field.unwrap()
}
}
})
.collect();
Ok(quote!(
#[automatically_derived]
impl std::convert::TryFrom<ActiveModel> for #ident {
type Error = sea_orm::DbErr;
fn try_from(a: ActiveModel) -> Result<Self, sea_orm::DbErr> {
#(if a.#active_model_field.is_not_set() {
return Err(sea_orm::DbErr::AttrNotSet(stringify!(#active_model_field).to_owned()));
})*
Ok(
Self {
#(#model_field: #model_field_value),*
}
)
}
}
#[automatically_derived]
impl sea_orm::TryIntoModel<#ident> for ActiveModel {
fn try_into_model(self) -> Result<#ident, sea_orm::DbErr> {
self.try_into()
}
}
))
}
pub fn expand_derive_active_model(
vis: &Visibility,
ident: &Ident,
data: &Data,
) -> syn::Result<TokenStream> {
let derive_active_model = DeriveActiveModel::new(vis, ident, data)?;
let define_active_model = derive_active_model.define_active_model();
let impl_active_model = derive_active_model.impl_active_model();
let derive_into_model = derive_into_model(ident, data)?;
Ok(quote!(
#define_active_model
#impl_active_model
#derive_into_model
))
}
@@ -0,0 +1,11 @@
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::Data;
/// Method to derive an implementation of [ActiveModelBehavior](sea_orm::ActiveModelBehavior)
pub fn expand_derive_active_model_behavior(_ident: Ident, _data: Data) -> syn::Result<TokenStream> {
Ok(quote!(
#[automatically_derived]
impl sea_orm::ActiveModelBehavior for ActiveModel {}
))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,368 @@
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{Attribute, Data, Fields, LitInt, LitStr, Type};
/// Expand the DeriveArrowSchema derive macro
pub fn expand_derive_arrow_schema(
_ident: Ident,
data: Data,
_attrs: Vec<Attribute>,
) -> syn::Result<TokenStream> {
if !cfg!(feature = "with-arrow") {
return Ok(quote!());
}
let mut fields_info = Vec::new();
// Parse fields
if let Data::Struct(item_struct) = data {
if let Fields::Named(fields) = &item_struct.fields {
for field in &fields.named {
if let Some(field_ident) = &field.ident {
let field_name = field_ident.to_string();
let field_type = &field.ty;
// Detect if field is Option<T> for nullability
let type_string: String = quote! { #field_type }
.to_string()
.split_whitespace()
.collect();
let is_nullable = type_string.starts_with("Option<");
// Parse field attributes
let mut arrow_attrs = ArrowFieldAttrs::default();
let mut column_type_str: Option<String> = None;
let mut column_name_override: Option<String> = None;
let mut arrow_field_override: Option<String> = None;
let mut skip = false;
for attr in field.attrs.iter() {
if attr.path().is_ident("sea_orm") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("arrow_skip") {
skip = true;
} else if meta.path.is_ident("arrow_field") {
let lit: LitStr = meta.value()?.parse()?;
arrow_field_override = Some(lit.value());
} else if meta.path.is_ident("column_name") {
let lit: LitStr = meta.value()?.parse()?;
column_name_override = Some(lit.value());
} else if meta.path.is_ident("arrow_precision") {
let lit: LitInt = meta.value()?.parse()?;
arrow_attrs.precision = Some(lit.base10_parse()?);
} else if meta.path.is_ident("arrow_scale") {
let lit: LitInt = meta.value()?.parse()?;
arrow_attrs.scale = Some(lit.base10_parse()?);
} else if meta.path.is_ident("arrow_timestamp_unit") {
let lit: LitStr = meta.value()?.parse()?;
arrow_attrs.timestamp_unit = Some(lit.value());
} else if meta.path.is_ident("arrow_timezone") {
let lit: LitStr = meta.value()?.parse()?;
arrow_attrs.timezone = Some(lit.value());
} else if meta.path.is_ident("arrow_comment") {
let lit: LitStr = meta.value()?.parse()?;
arrow_attrs.comment = Some(lit.value());
} else if meta.path.is_ident("arrow_byte_width") {
let lit: LitInt = meta.value()?.parse()?;
arrow_attrs.byte_width = Some(lit.base10_parse()?);
} else if meta.path.is_ident("column_type") {
let lit: LitStr = meta.value()?.parse()?;
column_type_str = Some(lit.value());
} else if meta.path.is_ident("nullable") {
arrow_attrs.nullable_attr = true;
} else {
let _ = meta.value().and_then(|v| v.parse::<syn::Expr>());
}
Ok(())
})?;
}
}
if skip {
continue; // Skip this field
}
// Determine final nullability
let nullable = is_nullable || arrow_attrs.nullable_attr;
// Priority: arrow_field > column_name > Rust field name
let resolved_name = arrow_field_override
.or(column_name_override)
.unwrap_or(field_name);
fields_info.push(ArrowFieldInfo {
name: resolved_name,
field_type: field_type.clone(),
column_type_str,
nullable,
arrow_attrs,
});
}
}
}
}
// Generate arrow_schema() method
let field_definitions = fields_info.iter().map(generate_field_definition);
let entity_name = format_ident!("Entity");
Ok(quote! {
#[automatically_derived]
impl sea_orm::ArrowSchema for #entity_name {
fn arrow_schema() -> sea_orm::arrow::datatypes::Schema {
use sea_orm::arrow::datatypes::{DataType, Field, Schema, TimeUnit};
Schema::new(vec![
#(#field_definitions),*
])
}
}
})
}
#[derive(Default)]
struct ArrowFieldAttrs {
precision: Option<u8>,
scale: Option<i8>,
timestamp_unit: Option<String>,
timezone: Option<String>,
comment: Option<String>,
nullable_attr: bool,
byte_width: Option<i32>,
}
struct ArrowFieldInfo {
name: String,
field_type: Type,
column_type_str: Option<String>,
#[allow(dead_code)]
nullable: bool,
arrow_attrs: ArrowFieldAttrs,
}
fn generate_field_definition(info: &ArrowFieldInfo) -> TokenStream {
let field_name = &info.name;
let nullable = true; // we use ActiveModel, where fields can be NotSet.
// Generate DataType based on column_type or field type
let data_type = if let Some(col_type_str) = &info.column_type_str {
column_type_to_arrow_datatype(col_type_str, &info.arrow_attrs)
} else {
rust_type_to_arrow_datatype(&info.field_type, &info.arrow_attrs)
};
// Add metadata if comment is present
if let Some(comment) = &info.arrow_attrs.comment {
quote! {
Field::new(#field_name, #data_type, #nullable)
.with_metadata([(
"comment".into(),
#comment.into()
)].into())
}
} else {
quote! {
Field::new(#field_name, #data_type, #nullable)
}
}
}
/// Map SeaORM ColumnType string to Arrow DataType
fn column_type_to_arrow_datatype(col_type: &str, arrow_attrs: &ArrowFieldAttrs) -> TokenStream {
// Parse ColumnType variants
if col_type.starts_with("Decimal(") {
// Extract precision and scale from Decimal(Some((p, s)))
let (precision, scale) = if col_type.contains("Some((") {
// Parse "Decimal(Some((20, 4)))"
if let Some(inner) = col_type
.strip_prefix("Decimal(Some((")
.and_then(|s| s.strip_suffix(")))"))
{
let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
if parts.len() == 2 {
let p = parts[0].parse().unwrap_or(38);
let s = parts[1].parse().unwrap_or(10);
(p, s)
} else {
(38, 10)
}
} else {
(38, 10)
}
} else {
(38, 10) // Default for Decimal(None)
};
// Allow arrow_precision/arrow_scale to override
let final_precision = arrow_attrs.precision.unwrap_or(precision);
let final_scale = arrow_attrs.scale.unwrap_or(scale);
if final_precision <= 18 {
quote! { DataType::Decimal64(#final_precision, #final_scale) }
} else if final_precision <= 38 {
quote! { DataType::Decimal128(#final_precision, #final_scale) }
} else {
quote! { DataType::Decimal256(#final_precision, #final_scale) }
}
} else if col_type.starts_with("Money(") {
let precision = arrow_attrs.precision.unwrap_or(19);
let scale = arrow_attrs.scale.unwrap_or(4);
if precision <= 18 {
quote! { DataType::Decimal64(#precision, #scale) }
} else {
quote! { DataType::Decimal128(#precision, #scale) }
}
} else if col_type == "TinyInteger" {
quote! { DataType::Int8 }
} else if col_type == "SmallInteger" {
quote! { DataType::Int16 }
} else if col_type == "Integer" {
quote! { DataType::Int32 }
} else if col_type == "BigInteger" {
quote! { DataType::Int64 }
} else if col_type == "TinyUnsigned" {
quote! { DataType::UInt8 }
} else if col_type == "SmallUnsigned" {
quote! { DataType::UInt16 }
} else if col_type == "Unsigned" {
quote! { DataType::UInt32 }
} else if col_type == "BigUnsigned" {
quote! { DataType::UInt64 }
} else if col_type == "Float" {
quote! { DataType::Float32 }
} else if col_type == "Double" {
quote! { DataType::Float64 }
} else if col_type == "Boolean" {
quote! { DataType::Boolean }
} else if col_type == "Text" {
quote! { DataType::LargeUtf8 }
} else if col_type.starts_with("String(") {
// Parse String(StringLen::N(255)) or String(StringLen::None)
if col_type.contains("None") || col_type.contains("Max") {
quote! { DataType::LargeUtf8 }
} else {
// Try to extract length
if let Some(inner) = col_type
.strip_prefix("String(StringLen::N(")
.and_then(|s| s.strip_suffix("))"))
{
if let Ok(n) = inner.parse::<u32>() {
if n <= 32767 {
return quote! { DataType::Utf8 };
}
}
}
quote! { DataType::LargeUtf8 }
}
} else if col_type.starts_with("Char(") {
quote! { DataType::Utf8 }
} else if col_type == "Date" {
quote! { DataType::Date32 }
} else if col_type == "Time" {
quote! { DataType::Time64(TimeUnit::Microsecond) }
} else if col_type == "DateTime" || col_type == "Timestamp" {
generate_timestamp_datatype(arrow_attrs, false)
} else if col_type == "TimestampWithTimeZone" {
generate_timestamp_datatype(arrow_attrs, true)
} else if col_type.starts_with("Binary(") || col_type.starts_with("VarBinary(") {
if let Some(bw) = arrow_attrs.byte_width {
quote! { DataType::FixedSizeBinary(#bw) }
} else {
quote! { DataType::Binary }
}
} else if col_type == "Json" || col_type == "JsonBinary" {
quote! { DataType::Utf8 }
} else if col_type == "Uuid" {
quote! { DataType::Binary }
} else if col_type.starts_with("Enum {") {
quote! { DataType::Utf8 }
} else {
// Default fallback
quote! { DataType::Binary }
}
}
/// Map Rust type to Arrow DataType (when no column_type specified)
fn rust_type_to_arrow_datatype(field_type: &Type, arrow_attrs: &ArrowFieldAttrs) -> TokenStream {
let type_string: String = quote! { #field_type }
.to_string()
.split_whitespace()
.collect();
// Strip Option<> wrapper if present
let inner_type = if type_string.starts_with("Option<") {
type_string
.strip_prefix("Option<")
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(&type_string)
} else {
&type_string
};
match inner_type {
"i8" => quote! { DataType::Int8 },
"i16" => quote! { DataType::Int16 },
"i32" => quote! { DataType::Int32 },
"i64" => quote! { DataType::Int64 },
"u8" => quote! { DataType::UInt8 },
"u16" => quote! { DataType::UInt16 },
"u32" => quote! { DataType::UInt32 },
"u64" => quote! { DataType::UInt64 },
"f32" => quote! { DataType::Float32 },
"f64" => quote! { DataType::Float64 },
"bool" => quote! { DataType::Boolean },
"String" => quote! { DataType::Utf8 },
s if s.contains("Decimal") => {
let precision = arrow_attrs.precision.unwrap_or(38);
let scale = arrow_attrs.scale.unwrap_or(10);
if precision <= 18 {
quote! { DataType::Decimal64(#precision, #scale) }
} else if precision <= 38 {
quote! { DataType::Decimal128(#precision, #scale) }
} else {
quote! { DataType::Decimal256(#precision, #scale) }
}
}
s if (s.contains("DateTime") && s.contains("Offset"))
|| (s.contains("DateTime") && s.contains("Utc"))
|| (s.contains("DateTime") && s.contains("TimeZone"))
|| s.contains("Timestamp") =>
{
generate_timestamp_datatype(arrow_attrs, true)
}
s if s.contains("DateTime") => {
generate_timestamp_datatype(arrow_attrs, arrow_attrs.timezone.is_some())
}
s if s.contains("Date") => quote! { DataType::Date32 },
s if s.contains("Time") => quote! { DataType::Time64(TimeUnit::Microsecond) },
"Vec<u8>" => {
if let Some(bw) = arrow_attrs.byte_width {
quote! { DataType::FixedSizeBinary(#bw) }
} else {
quote! { DataType::Binary }
}
}
_ => quote! { DataType::Binary }, // Safe fallback
}
}
/// Generate timestamp DataType with optional timezone
fn generate_timestamp_datatype(arrow_attrs: &ArrowFieldAttrs, has_timezone: bool) -> TokenStream {
let unit = match arrow_attrs.timestamp_unit.as_deref() {
Some("Second") => quote! { TimeUnit::Second },
Some("Millisecond") => quote! { TimeUnit::Millisecond },
Some("Microsecond") => quote! { TimeUnit::Microsecond },
Some("Nanosecond") => quote! { TimeUnit::Nanosecond },
_ => quote! { TimeUnit::Microsecond }, // Default
};
if has_timezone {
let tz = arrow_attrs.timezone.as_deref().unwrap_or("UTC");
quote! { DataType::Timestamp(#unit, Some(#tz.into())) }
} else if let Some(tz) = &arrow_attrs.timezone {
quote! { DataType::Timestamp(#unit, Some(#tz.into())) }
} else {
quote! { DataType::Timestamp(#unit, None) }
}
}
@@ -0,0 +1,109 @@
pub mod derive_attr {
use bae::FromAttributes;
/// Attributes for Models and ActiveModels
#[derive(Default, FromAttributes)]
#[allow(dead_code)]
pub struct SeaOrm {
pub column: Option<syn::Ident>,
pub entity: Option<syn::Ident>,
pub model: Option<syn::Ident>,
pub model_ex: Option<syn::Ident>,
pub active_model: Option<syn::Ident>,
pub active_model_ex: Option<syn::Ident>,
pub primary_key: Option<syn::Ident>,
pub relation: Option<syn::Ident>,
pub schema_name: Option<syn::LitStr>,
pub table_name: Option<syn::LitStr>,
pub comment: Option<syn::LitStr>,
pub table_iden: Option<()>,
pub rename_all: Option<syn::LitStr>,
}
}
pub mod relation_attr {
use bae::FromAttributes;
/// Attributes for Relation enum
#[derive(Default, FromAttributes)]
pub struct SeaOrm {
pub belongs_to: Option<syn::Lit>,
pub has_one: Option<syn::Lit>,
pub has_many: Option<syn::Lit>,
pub via_rel: Option<syn::Lit>,
pub on_update: Option<syn::Lit>,
pub on_delete: Option<syn::Lit>,
pub on_condition: Option<syn::Lit>,
pub from: Option<syn::Lit>,
pub to: Option<syn::Lit>,
pub fk_name: Option<syn::Lit>,
pub skip_fk: Option<()>,
pub condition_type: Option<syn::Lit>,
}
}
pub mod compound_attr {
use bae::FromAttributes;
/// Attributes for compound model fields
#[derive(Default, FromAttributes)]
pub struct SeaOrm {
pub has_one: Option<()>,
pub has_many: Option<()>,
pub belongs_to: Option<()>,
pub self_ref: Option<()>,
pub skip_fk: Option<()>,
pub via: Option<syn::LitStr>,
pub via_rel: Option<syn::LitStr>,
pub from: Option<syn::LitStr>,
pub to: Option<syn::LitStr>,
pub relation_enum: Option<syn::LitStr>,
pub relation_reverse: Option<syn::LitStr>,
pub reverse: Option<()>,
pub on_update: Option<syn::LitStr>,
pub on_delete: Option<syn::LitStr>,
}
}
pub mod value_type_attr {
use bae::FromAttributes;
/// Attributes for compound model fields
#[derive(Default, FromAttributes)]
pub struct SeaOrm {
pub column_type: Option<syn::LitStr>,
pub array_type: Option<syn::LitStr>,
pub value_type: Option<syn::LitStr>,
pub from_str: Option<syn::LitStr>,
pub to_str: Option<syn::LitStr>,
pub try_from_u64: Option<()>,
pub try_getable_array: Option<()>,
}
}
#[cfg(feature = "seaography")]
pub mod related_attr {
use bae::FromAttributes;
/// Attributes for RelatedEntity enum
#[derive(Default, FromAttributes)]
pub struct SeaOrm {
///
/// Allows to modify target entity
///
/// Required on enumeration variants
///
/// If used on enumeration attributes
/// it allows to specify different
/// Entity ident
pub entity: Option<syn::Lit>,
///
/// Allows to specify RelationDef
///
/// Optional
///
/// If not supplied the generated code
/// will utilize `impl Related` trait
pub def: Option<syn::Lit>,
}
}
@@ -0,0 +1,133 @@
//! Copied from https://github.com/Peternator7/strum/blob/master/strum_macros/src/helpers/case_style.rs
use heck::{
ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase,
};
use std::str::FromStr;
use syn::{
Ident, LitStr,
meta::ParseNestedMeta,
parse::{Parse, ParseStream},
};
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CaseStyle {
CamelCase,
KebabCase,
MixedCase,
ShoutySnakeCase,
SnakeCase,
TitleCase,
UpperCase,
LowerCase,
ScreamingKebabCase,
PascalCase,
}
const VALID_CASE_STYLES: &[&str] = &[
"camelCase",
"PascalCase",
"kebab-case",
"snake_case",
"SCREAMING_SNAKE_CASE",
"SCREAMING-KEBAB-CASE",
"lowercase",
"UPPERCASE",
"title_case",
"mixed_case",
];
impl Parse for CaseStyle {
fn parse(input: ParseStream) -> syn::Result<Self> {
let text = input.parse::<LitStr>()?;
let val = text.value();
val.as_str().parse().map_err(|_| {
syn::Error::new_spanned(
&text,
format!(
"Unexpected case style for serialize_all: `{val}`. Valid values are: `{VALID_CASE_STYLES:?}`",
),
)
})
}
}
impl FromStr for CaseStyle {
type Err = ();
fn from_str(text: &str) -> Result<Self, ()> {
Ok(match text {
"camel_case" | "PascalCase" => CaseStyle::PascalCase,
"camelCase" => CaseStyle::CamelCase,
"snake_case" | "snek_case" => CaseStyle::SnakeCase,
"kebab_case" | "kebab-case" => CaseStyle::KebabCase,
"SCREAMING-KEBAB-CASE" => CaseStyle::ScreamingKebabCase,
"shouty_snake_case" | "shouty_snek_case" | "SCREAMING_SNAKE_CASE" => {
CaseStyle::ShoutySnakeCase
}
"title_case" => CaseStyle::TitleCase,
"mixed_case" => CaseStyle::MixedCase,
"lowercase" => CaseStyle::LowerCase,
"UPPERCASE" => CaseStyle::UpperCase,
_ => return Err(()),
})
}
}
pub trait CaseStyleHelpers {
fn convert_case(&self, case_style: Option<CaseStyle>) -> String;
}
impl CaseStyleHelpers for Ident {
fn convert_case(&self, case_style: Option<CaseStyle>) -> String {
let ident_string = self.to_string();
if let Some(case_style) = case_style {
match case_style {
CaseStyle::PascalCase => ident_string.to_upper_camel_case(),
CaseStyle::KebabCase => ident_string.to_kebab_case(),
CaseStyle::MixedCase => ident_string.to_lower_camel_case(),
CaseStyle::ShoutySnakeCase => ident_string.to_shouty_snake_case(),
CaseStyle::SnakeCase => ident_string.to_snake_case(),
CaseStyle::TitleCase => ident_string.to_title_case(),
CaseStyle::UpperCase => ident_string.to_uppercase(),
CaseStyle::LowerCase => ident_string.to_lowercase(),
CaseStyle::ScreamingKebabCase => ident_string.to_kebab_case().to_uppercase(),
CaseStyle::CamelCase => {
let camel_case = ident_string.to_upper_camel_case();
let mut pascal = String::with_capacity(camel_case.len());
let mut it = camel_case.chars();
if let Some(ch) = it.next() {
pascal.extend(ch.to_lowercase());
}
pascal.extend(it);
pascal
}
}
} else {
ident_string
}
}
}
impl TryFrom<&ParseNestedMeta<'_>> for CaseStyle {
type Error = syn::Error;
fn try_from(value: &ParseNestedMeta) -> Result<Self, Self::Error> {
let meta_string_literal: LitStr = value.value()?.parse()?;
let value_string = meta_string_literal.value();
match CaseStyle::from_str(value_string.as_str()) {
Ok(rule) => Ok(rule),
Err(()) => Err(value.error(format!(
"Unknown value for attribute parameter: `{value_string}`. Valid values are: `{VALID_CASE_STYLES:?}`"
))),
}
}
}
#[test]
fn test_convert_case() {
let id = Ident::new("test_me", proc_macro2::Span::call_site());
assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase)));
assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase)));
}
@@ -0,0 +1,150 @@
use super::is_static_iden;
use heck::{ToLowerCamelCase, ToSnakeCase};
use proc_macro2::{Ident, TokenStream};
use quote::{quote, quote_spanned};
use syn::{Data, DataEnum, Expr, Fields, LitStr, Variant};
/// Derive a Column name for an enum type
pub fn impl_iden(ident: &Ident, data: &Data) -> syn::Result<TokenStream> {
let variants = match data {
syn::Data::Enum(DataEnum { variants, .. }) => variants,
_ => {
return Ok(quote_spanned! {
ident.span() => compile_error!("you can only derive DeriveColumn on enums");
});
}
};
let variant: Vec<TokenStream> = variants
.iter()
.map(|Variant { ident, fields, .. }| match fields {
Fields::Named(_) => quote! { #ident{..} },
Fields::Unnamed(_) => quote! { #ident(..) },
Fields::Unit => quote! { #ident },
})
.collect();
let mut all_static = true;
let name: Vec<TokenStream> = variants
.iter()
.map(|v| {
let mut column_name = v.ident.to_string().to_snake_case();
for attr in v.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("column_name") {
column_name = meta.value()?.parse::<LitStr>()?.value();
} else {
// Reads the value expression to advance the parse stream.
// Some parameters, such as `primary_key`, do not have any value,
// so ignoring an error occurred here.
let _: Option<Expr> = meta.value().and_then(|v| v.parse()).ok();
}
Ok(())
})?;
}
all_static &= is_static_iden(&column_name);
Ok::<TokenStream, syn::Error>(quote! { #column_name })
})
.collect::<Result<_, _>>()?;
let quoted = if all_static {
quote! {
fn quoted(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(sea_orm::IdenStatic::as_str(self))
}
}
} else {
quote! {}
};
Ok(quote!(
#[automatically_derived]
impl sea_orm::IdenStatic for #ident {
fn as_str(&self) -> &'static str {
match self {
#(Self::#variant => #name),*
}
}
}
#[automatically_derived]
impl sea_orm::Iden for #ident {
#quoted
fn unquoted(&self) -> &str {
sea_orm::IdenStatic::as_str(self)
}
}
))
}
/// Implement a column for an enum using [DeriveColumn](sea_orm::DeriveColumn)
pub fn impl_col_from_str(ident: &Ident, data: &Data) -> syn::Result<TokenStream> {
let data_enum = match data {
Data::Enum(data_enum) => data_enum,
_ => {
return Ok(quote_spanned! {
ident.span() => compile_error!("you can only derive DeriveColumn on enums");
});
}
};
let columns = data_enum
.variants
.iter()
.map(|column| {
let column_iden = column.ident.clone();
let column_str_snake = column_iden.to_string().to_snake_case();
let column_str_mixed = column_iden.to_string().to_lower_camel_case();
let mut column_name = column_str_snake.clone();
for attr in column.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("column_name") {
column_name = meta.value()?.parse::<LitStr>()?.value();
} else {
// Reads the value expression to advance the parse stream.
// Some parameters, such as `primary_key`, do not have any value,
// so ignoring an error occurred here.
let _: Option<Expr> = meta.value().and_then(|v| v.parse()).ok();
}
Ok(())
})?;
}
Ok::<TokenStream, syn::Error>(quote!(
#column_str_snake | #column_str_mixed | #column_name => Ok(#ident::#column_iden)
))
})
.collect::<Result<Vec<_>, _>>()?;
Ok(quote!(
#[automatically_derived]
impl std::str::FromStr for #ident {
type Err = sea_orm::ColumnFromStrErr;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
#(#columns),*,
_ => Err(sea_orm::ColumnFromStrErr(s.to_owned())),
}
}
}
))
}
pub fn expand_derive_column(ident: &Ident, data: &Data) -> syn::Result<TokenStream> {
let impl_col_from_str = impl_col_from_str(ident, data)?;
let impl_iden = impl_iden(ident, data)?;
Ok(quote!(
#impl_col_from_str
#impl_iden
))
}
@@ -0,0 +1,148 @@
use heck::ToSnakeCase;
use proc_macro2::{self, TokenStream};
use quote::{quote, quote_spanned};
use syn::{
DataEnum, DataStruct, DeriveInput, Expr, Fields, LitStr, Variant, punctuated::Punctuated,
};
pub(super) fn is_static_iden(name: &str) -> bool {
// can only begin with [a-z_]
name.chars()
.take(1)
.all(|c| c == '_' || c.is_ascii_alphabetic())
&& name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric())
}
pub(super) fn impl_iden_for_unit_struct(
ident: &syn::Ident,
iden_str: &str,
) -> proc_macro2::TokenStream {
let quoted = if is_static_iden(iden_str) {
quote! {
fn quoted(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(#iden_str)
}
}
} else {
quote! {}
};
quote! {
#[automatically_derived]
impl sea_orm::Iden for #ident {
#quoted
fn unquoted(&self) -> &str {
#iden_str
}
}
}
}
fn impl_iden_for_enum(
ident: &syn::Ident,
variants: Punctuated<Variant, syn::token::Comma>,
) -> proc_macro2::TokenStream {
let variants = variants.iter();
let mut all_static = true;
let match_pair: Vec<TokenStream> = variants
.map(|v| {
let var_ident = &v.ident;
let var_name = if var_ident == "Table" {
ident
} else {
var_ident
};
let mut var_name = var_name.to_string().to_snake_case();
v.attrs
.iter()
.filter(|attr| attr.path().is_ident("sea_orm"))
.try_for_each(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("iden") {
let litstr: LitStr = meta.value()?.parse()?;
var_name = litstr.value();
} else {
// Reads the value expression to advance the parse stream.
// Some parameters do not have any value,
// so ignoring an error occurred here.
let _: Option<Expr> = meta.value().and_then(|v| v.parse()).ok();
}
Ok(())
})
})
.expect("something something");
all_static &= is_static_iden(&var_name);
quote! { Self::#var_ident => #var_name }
})
.collect();
let match_arms: TokenStream = quote! { #(#match_pair),* };
let quoted = if all_static {
quote! {
fn quoted(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(match self {
#match_arms
})
}
}
} else {
quote! {}
};
quote! {
#[automatically_derived]
impl sea_orm::Iden for #ident {
#quoted
fn unquoted(&self) -> &str {
match self {
#match_arms
}
}
}
}
}
pub fn expand_derive_iden(input: DeriveInput) -> syn::Result<TokenStream> {
let DeriveInput { ident, data, .. } = input;
let mut new_iden: String = ident.to_string().to_snake_case();
input
.attrs
.iter()
.filter(|attr| attr.path().is_ident("sea_orm"))
.try_for_each(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("iden") {
let litstr: LitStr = meta.value()?.parse()?;
new_iden = litstr.value();
} else {
// Reads the value expression to advance the parse stream.
// Some parameters do not have any value,
// so ignoring an error occurred here.
let _: Option<Expr> = meta.value().and_then(|v| v.parse()).ok();
}
Ok(())
})
})?;
// Currently we only support enums and unit structs
match data {
syn::Data::Enum(DataEnum { variants, .. }) => {
if variants.is_empty() {
Ok(TokenStream::new())
} else {
Ok(impl_iden_for_enum(&ident, variants))
}
}
syn::Data::Struct(DataStruct {
fields: Fields::Unit,
..
}) => Ok(impl_iden_for_unit_struct(&ident, &new_iden)),
_ => Ok(quote_spanned! {
ident.span() => compile_error!("you can only derive DeriveIden on unit struct or enum");
}),
}
}
@@ -0,0 +1,181 @@
use std::iter::FromIterator;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use super::{attributes::derive_attr, impl_iden_for_unit_struct};
struct DeriveEntity {
column_ident: syn::Ident,
ident: syn::Ident,
model_ident: syn::Ident,
model_ex_ident: syn::Ident,
active_model_ident: syn::Ident,
active_model_ex_ident: syn::Ident,
primary_key_ident: syn::Ident,
relation_ident: syn::Ident,
schema_name: Option<syn::LitStr>,
table_name: Option<syn::LitStr>,
}
impl DeriveEntity {
fn new(input: syn::DeriveInput) -> Result<Self, syn::Error> {
let sea_attr = derive_attr::SeaOrm::try_from_attributes(&input.attrs)?.unwrap_or_default();
let ident = input.ident;
let column_ident = sea_attr.column.unwrap_or_else(|| format_ident!("Column"));
let model_ident = sea_attr.model.unwrap_or_else(|| format_ident!("Model"));
let model_ex_ident = sea_attr.model_ex.unwrap_or_else(|| format_ident!("Model"));
let active_model_ident = sea_attr
.active_model
.unwrap_or_else(|| format_ident!("ActiveModel"));
let active_model_ex_ident = sea_attr
.active_model_ex
.unwrap_or_else(|| format_ident!("ActiveModel"));
let primary_key_ident = sea_attr
.primary_key
.unwrap_or_else(|| format_ident!("PrimaryKey"));
let relation_ident = sea_attr
.relation
.unwrap_or_else(|| format_ident!("Relation"));
let table_name = sea_attr.table_name;
let schema_name = sea_attr.schema_name;
Ok(DeriveEntity {
column_ident,
ident,
model_ident,
model_ex_ident,
active_model_ident,
active_model_ex_ident,
primary_key_ident,
relation_ident,
schema_name,
table_name,
})
}
fn expand(&self) -> TokenStream {
let expanded_impl_entity_name = self.impl_entity_name();
let expanded_impl_entity_trait = self.impl_entity_trait();
let expanded_impl_iden = self.impl_iden();
let expanded_impl_iden_static = self.impl_iden_static();
let expanded_impl_entity_registry = self.impl_entity_registry();
TokenStream::from_iter([
expanded_impl_entity_name,
expanded_impl_entity_trait,
expanded_impl_iden,
expanded_impl_iden_static,
expanded_impl_entity_registry,
])
}
fn impl_entity_name(&self) -> TokenStream {
let ident = &self.ident;
let table_name = match &self.table_name {
Some(table_name) => table_name,
None => return TokenStream::new(), // No table name, do not derive EntityName
};
let expanded_schema_name = self
.schema_name
.as_ref()
.map(|schema| quote!(Some(#schema)))
.unwrap_or_else(|| quote!(None));
quote!(
#[automatically_derived]
impl sea_orm::entity::EntityName for #ident {
fn schema_name(&self) -> Option<&str> {
#expanded_schema_name
}
fn table_name(&self) -> &'static str {
#table_name
}
}
)
}
fn impl_entity_trait(&self) -> TokenStream {
let Self {
ident,
model_ident,
model_ex_ident,
active_model_ident,
active_model_ex_ident,
column_ident,
primary_key_ident,
relation_ident,
..
} = self;
quote!(
#[automatically_derived]
impl sea_orm::entity::EntityTrait for #ident {
type Model = #model_ident;
type ModelEx = #model_ex_ident;
type ActiveModel = #active_model_ident;
type ActiveModelEx = #active_model_ex_ident;
type Column = #column_ident;
type PrimaryKey = #primary_key_ident;
type Relation = #relation_ident;
}
)
}
fn impl_iden(&self) -> TokenStream {
let ident = &self.ident;
match &self.table_name {
Some(table_name) => impl_iden_for_unit_struct(ident, &table_name.value()),
None => quote!(
#[automatically_derived]
impl sea_orm::Iden for #ident {
fn unquoted(&self) -> &str {
<Self as sea_orm::IdenStatic>::as_str(self)
}
}
),
}
}
fn impl_iden_static(&self) -> TokenStream {
let ident = &self.ident;
quote!(
#[automatically_derived]
impl sea_orm::IdenStatic for #ident {
fn as_str(&self) -> &'static str {
<Self as sea_orm::EntityName>::table_name(self)
}
}
)
}
fn impl_entity_registry(&self) -> TokenStream {
if cfg!(feature = "entity-registry") {
quote! {
sea_orm::register_entity! {
sea_orm::EntityRegistry {
module_path: module_path!(),
schema_info: |schema| sea_orm::EntitySchemaInfo::new(Entity, schema),
}
}
}
} else {
quote!()
}
}
}
pub fn expand_derive_entity(input: syn::DeriveInput) -> syn::Result<TokenStream> {
Ok(DeriveEntity::new(input)?.expand())
}
@@ -0,0 +1,996 @@
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::{HashMap, HashSet};
use syn::{Ident, TypePath, Visibility, punctuated::Punctuated, token::Comma};
#[derive(Default)]
pub struct EntityLoaderSchema {
pub fields: Vec<EntityLoaderField>,
}
pub enum EntityLoaderFieldKind {
HasOne,
HasOneSelf,
HasMany,
HasManySelf,
ManyToMany,
ManyToManySelf {
junction_module: Ident,
reverse: bool,
},
}
pub struct EntityLoaderField {
pub field: Ident,
/// super::bakery::Entity
pub entity: TypePath,
pub relation_enum: Option<syn::LitStr>,
pub kind: EntityLoaderFieldKind,
}
#[derive(Default)]
struct EntityLoaderOutput {
loader_with_fields: Punctuated<TokenStream, Comma>,
loader_nest_fields: Punctuated<TokenStream, Comma>,
select_tuple_fields: Punctuated<TokenStream, Comma>,
loader_with_set_impl: TokenStream,
loader_with_2_impl: TokenStream,
fetch_select_impl: TokenStream,
assemble_one: TokenStream,
load_one: TokenStream,
load_many: TokenStream,
load_one_nest: TokenStream,
load_many_nest: TokenStream,
load_one_nest_nest: TokenStream,
load_many_nest_nest: TokenStream,
with_param_impls: TokenStream,
}
impl EntityLoaderField {
fn expand_loader_with_field_into(&self, output: &mut EntityLoaderOutput) {
let field = &self.field;
output.loader_with_fields.push(quote! {
#[doc = " Generated by sea-orm-macros"]
pub #field: bool
});
}
fn expand_loader_nest_field_into(&self, output: &mut EntityLoaderOutput) {
let field = &self.field;
let nest_type = match &self.kind {
EntityLoaderFieldKind::HasOne
| EntityLoaderFieldKind::HasMany
| EntityLoaderFieldKind::ManyToMany => {
let mut entity_module = self.entity.path.clone();
entity_module.segments.pop();
entity_module
.segments
.push(syn::parse_quote!(EntityLoaderWith));
quote!(#entity_module)
}
EntityLoaderFieldKind::HasOneSelf
| EntityLoaderFieldKind::HasManySelf
| EntityLoaderFieldKind::ManyToManySelf { .. } => quote!(EntityLoaderWith),
};
output.loader_nest_fields.push(quote! {
#[doc = " Generated by sea-orm-macros"]
pub #field: #nest_type
});
}
fn expand_relation_tuple_with_param_into(&self, output: &mut EntityLoaderOutput) {
let mut entity_module = self.entity.path.clone();
entity_module.segments.pop();
let mut related_entity = entity_module.clone();
related_entity.segments.push(syn::parse_quote!(Entity));
let mut related_relation = entity_module;
related_relation.segments.push(syn::parse_quote!(Relation));
output.with_param_impls.extend(quote! {
impl EntityLoaderWithParam for (Relation, #related_entity) {
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(
sea_orm::compound::LoadTarget::Relation(sea_orm::RelationTrait::name(&self.0)),
Some(sea_orm::compound::LoadTarget::TableRef(self.1.table_ref())),
)
}
}
impl EntityLoaderWithParam for (Relation, #related_relation) {
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(
sea_orm::compound::LoadTarget::Relation(sea_orm::RelationTrait::name(&self.0)),
Some(sea_orm::compound::LoadTarget::Relation(sea_orm::RelationTrait::name(&self.1))),
)
}
}
});
}
fn expand_many_to_many_self_with_param_into(
&self,
output: &mut EntityLoaderOutput,
junction_module: &Ident,
reverse: bool,
) {
let target_type = if !reverse {
Ident::new("TableRef", junction_module.span())
} else {
Ident::new("TableRefRev", junction_module.span())
};
let target_entity = if !reverse {
quote!(super::#junction_module::Entity)
} else {
quote!(super::#junction_module::EntityReverse)
};
output.with_param_impls.extend(quote! {
impl EntityLoaderWithParam for #target_entity {
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()), None)
}
}
impl<S> EntityLoaderWithParam for (#target_entity, S)
where
S: EntityTrait,
Entity: Related<S>,
{
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(
sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()),
Some(sea_orm::compound::LoadTarget::TableRef(self.1.table_ref())),
)
}
}
});
}
fn expand_loader_with_set_impl_into(
&self,
output: &mut EntityLoaderOutput,
duplicate_entity: bool,
) {
let field = &self.field;
let entity = &self.entity;
match &self.kind {
EntityLoaderFieldKind::HasOne
| EntityLoaderFieldKind::HasMany
| EntityLoaderFieldKind::ManyToMany => {
if !duplicate_entity {
output.loader_with_set_impl.extend(quote! {
if target == sea_orm::compound::LoadTarget::TableRef(#entity.table_ref()) {
self.#field = true;
}
});
} else if let Some(relation_enum) = &self.relation_enum {
output.loader_with_set_impl.extend(quote! {
if let sea_orm::compound::LoadTarget::Relation(relation_enum) = &target {
if relation_enum == #relation_enum {
self.#field = true;
}
}
});
}
}
EntityLoaderFieldKind::HasOneSelf
| EntityLoaderFieldKind::HasManySelf
| EntityLoaderFieldKind::ManyToManySelf { .. } => {
if let Some(relation_enum) = &self.relation_enum {
output.loader_with_set_impl.extend(quote! {
if let sea_orm::compound::LoadTarget::Relation(relation_enum) = &target {
if relation_enum == #relation_enum {
self.#field = true;
}
}
});
}
if let EntityLoaderFieldKind::ManyToManySelf {
junction_module,
reverse,
} = &self.kind
{
let target_type = if !reverse {
Ident::new("TableRef", junction_module.span())
} else {
Ident::new("TableRefRev", junction_module.span())
};
output.loader_with_set_impl.extend(quote! {
if target == sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()) {
self.#field = true;
}
});
}
}
}
}
fn expand_loader_with_2_impl_into(
&self,
output: &mut EntityLoaderOutput,
duplicate_entity: bool,
) {
let field = &self.field;
let entity = &self.entity;
match &self.kind {
EntityLoaderFieldKind::HasOne
| EntityLoaderFieldKind::HasMany
| EntityLoaderFieldKind::ManyToMany => {
if !duplicate_entity {
output.loader_with_2_impl.extend(quote! {
if left == sea_orm::compound::LoadTarget::TableRef(#entity.table_ref()) {
self.with.#field = true;
self.nest.#field.set(right);
return self;
}
});
} else if let Some(relation_enum) = &self.relation_enum {
output.loader_with_2_impl.extend(quote! {
if let sea_orm::compound::LoadTarget::Relation(relation_enum) = &left {
if relation_enum == #relation_enum {
self.with.#field = true;
self.nest.#field.set(right);
return self;
}
}
});
}
}
EntityLoaderFieldKind::HasOneSelf | EntityLoaderFieldKind::HasManySelf => {}
EntityLoaderFieldKind::ManyToManySelf {
junction_module,
reverse,
} => {
let target_type = if !reverse {
Ident::new("TableRef", junction_module.span())
} else {
Ident::new("TableRefRev", junction_module.span())
};
output.loader_with_2_impl.extend(quote! {
if left == sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()) {
self.with.#field = true;
self.nest.#field.set(right);
return self;
}
});
}
}
}
fn expand_select_one_into(&self, output: &mut EntityLoaderOutput) {
let field = &self.field;
let entity = &self.entity;
output.select_tuple_fields.push(quote!(#field));
output.fetch_select_impl.extend(quote! {
let select = if self.with.#field && self.nest.#field.is_empty() {
self.with.#field = false;
loaded.#field = true;
select.find_also(Entity, #entity)
} else {
select.select_also_fake(#entity)
};
});
output.assemble_one.extend(quote! {
if loaded.#field {
model.#field = #field.map(Into::into).map(Box::new).into();
}
});
}
fn expand_load_one_into(&self, output: &mut EntityLoaderOutput) {
let field = &self.field;
let entity = &self.entity;
let await_ = if cfg!(feature = "async") {
quote!(.await)
} else {
quote!()
};
let mut entity_module = self.entity.path.clone();
entity_module.segments.pop();
entity_module.segments.push(syn::parse_quote!(EntityLoader));
output.load_one.extend(quote! {
if with.#field {
let #field = models.as_slice().load_one_ex(#entity, db)#await_?;
let #field = <#entity_module>::load_nest(#field, &nest.#field, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.map(Into::into).map(Box::new).into();
}
}
});
output.load_one_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_one_ex(#entity, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
if let Some(model) = model.as_mut() {
model.#field = #field.map(Into::into).map(Box::new).into();
}
}
}
});
output.load_one_nest_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_one_ex(#entity, db)#await_?;
for (models, #field) in models.iter_mut().zip(#field) {
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.map(Into::into).map(Box::new).into();
}
}
}
});
}
fn expand_load_one_with_rel_into(
&self,
output: &mut EntityLoaderOutput,
relation_enum: &syn::LitStr,
) {
let field = &self.field;
let entity = &self.entity;
let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span());
let await_ = if cfg!(feature = "async") {
quote!(.await)
} else {
quote!()
};
let mut entity_module = self.entity.path.clone();
entity_module.segments.pop();
entity_module.segments.push(syn::parse_quote!(EntityLoader));
output.load_one.extend(quote! {
if with.#field {
let #field = models.as_slice().load_one_ex_with_rel(
#entity,
sea_orm::RelationTrait::def(&Relation::#relation_enum),
db,
)#await_?;
let #field = <#entity_module>::load_nest(#field, &nest.#field, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.map(Into::into).map(Box::new).into();
}
}
});
output.load_one_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_one_ex_with_rel(
#entity,
sea_orm::RelationTrait::def(&Relation::#relation_enum),
db,
)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
if let Some(model) = model.as_mut() {
model.#field = #field.map(Into::into).map(Box::new).into();
}
}
}
});
output.load_one_nest_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_one_ex_with_rel(
#entity,
sea_orm::RelationTrait::def(&Relation::#relation_enum),
db,
)#await_?;
for (models, #field) in models.iter_mut().zip(#field) {
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.map(Into::into).map(Box::new).into();
}
}
}
});
}
fn expand_load_many_into(&self, output: &mut EntityLoaderOutput) {
let field = &self.field;
let entity = &self.entity;
let await_ = if cfg!(feature = "async") {
quote!(.await)
} else {
quote!()
};
let mut entity_module = self.entity.path.clone();
entity_module.segments.pop();
entity_module.segments.push(syn::parse_quote!(EntityLoader));
output.load_many.extend(quote! {
if with.#field {
let #field = models.as_slice().load_many_ex(#entity, db)#await_?;
let #field = <#entity_module>::load_nest_nest(#field, &nest.#field, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.into();
}
}
});
output.load_many_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_many_ex(#entity, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
if let Some(model) = model.as_mut() {
model.#field = #field.into();
}
}
}
});
output.load_many_nest_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_many_ex(#entity, db)#await_?;
for (models, #field) in models.iter_mut().zip(#field) {
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.into();
}
}
}
});
}
fn expand_load_many_with_rel_into(
&self,
output: &mut EntityLoaderOutput,
relation_enum: &syn::LitStr,
) {
let field = &self.field;
let entity = &self.entity;
let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span());
let await_ = if cfg!(feature = "async") {
quote!(.await)
} else {
quote!()
};
let mut entity_module = self.entity.path.clone();
entity_module.segments.pop();
entity_module.segments.push(syn::parse_quote!(EntityLoader));
output.load_many.extend(quote! {
if with.#field {
let #field = models.as_slice().load_many_ex_with_rel(
#entity,
sea_orm::RelationTrait::def(&Relation::#relation_enum),
db,
)#await_?;
let #field = <#entity_module>::load_nest_nest(#field, &nest.#field, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.into();
}
}
});
output.load_many_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_many_ex_with_rel(
#entity,
sea_orm::RelationTrait::def(&Relation::#relation_enum),
db,
)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
if let Some(model) = model.as_mut() {
model.#field = #field.into();
}
}
}
});
output.load_many_nest_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_many_ex_with_rel(
#entity,
sea_orm::RelationTrait::def(&Relation::#relation_enum),
db,
)#await_?;
for (models, #field) in models.iter_mut().zip(#field) {
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.into();
}
}
}
});
}
fn expand_load_one_self_into(
&self,
output: &mut EntityLoaderOutput,
relation_enum: &syn::LitStr,
) {
let field = &self.field;
let entity = &self.entity;
let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span());
let await_ = if cfg!(feature = "async") {
quote!(.await)
} else {
quote!()
};
output.load_one.extend(quote! {
if with.#field {
let #field = models.as_slice().load_self_ex(#entity, Relation::#relation_enum, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.map(Into::into).map(Box::new).into();
}
}
});
}
fn expand_load_many_self_into(
&self,
output: &mut EntityLoaderOutput,
relation_enum: &syn::LitStr,
) {
let field = &self.field;
let entity = &self.entity;
let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span());
let await_ = if cfg!(feature = "async") {
quote!(.await)
} else {
quote!()
};
output.load_many.extend(quote! {
if with.#field {
let #field = models.as_slice().load_self_many_ex(#entity, Relation::#relation_enum, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.into();
}
}
});
}
fn expand_load_many_to_many_self_into(
&self,
output: &mut EntityLoaderOutput,
junction_module: &Ident,
reverse: bool,
) {
let field = &self.field;
let await_ = if cfg!(feature = "async") {
quote!(.await)
} else {
quote!()
};
output.load_many.extend(quote! {
if with.#field {
let #field = models.as_slice().load_self_via_ex(super::#junction_module::Entity, #reverse, db)#await_?;
let #field = EntityLoader::load_nest_nest(#field, &nest.#field, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.into();
}
}
});
output.load_many_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_self_via_ex(super::#junction_module::Entity, #reverse, db)#await_?;
for (model, #field) in models.iter_mut().zip(#field) {
if let Some(model) = model.as_mut() {
model.#field = #field.into();
}
}
}
});
output.load_many_nest_nest.extend(quote! {
if with.#field {
let #field = models.as_slice().load_self_via_ex(super::#junction_module::Entity, #reverse, db)#await_?;
for (models, #field) in models.iter_mut().zip(#field) {
for (model, #field) in models.iter_mut().zip(#field) {
model.#field = #field.into();
}
}
}
});
}
fn expand_load_into(&self, output: &mut EntityLoaderOutput, duplicate_entity: bool) {
match &self.kind {
EntityLoaderFieldKind::HasOne if !duplicate_entity => {
self.expand_load_one_into(output);
}
EntityLoaderFieldKind::HasOne => {
if let Some(relation_enum) = &self.relation_enum {
self.expand_load_one_with_rel_into(output, relation_enum);
}
}
EntityLoaderFieldKind::HasMany | EntityLoaderFieldKind::ManyToMany
if !duplicate_entity =>
{
self.expand_load_many_into(output);
}
EntityLoaderFieldKind::HasMany => {
if let Some(relation_enum) = &self.relation_enum {
self.expand_load_many_with_rel_into(output, relation_enum);
}
}
EntityLoaderFieldKind::ManyToMany => {}
EntityLoaderFieldKind::HasOneSelf => {
if let Some(relation_enum) = &self.relation_enum {
self.expand_load_one_self_into(output, relation_enum);
}
}
EntityLoaderFieldKind::HasManySelf => {
if let Some(relation_enum) = &self.relation_enum {
self.expand_load_many_self_into(output, relation_enum);
}
}
EntityLoaderFieldKind::ManyToManySelf {
junction_module,
reverse,
} => {
if let Some(relation_enum) = &self.relation_enum {
self.expand_load_many_self_into(output, relation_enum);
}
self.expand_load_many_to_many_self_into(output, junction_module, *reverse);
}
}
}
}
impl EntityLoaderOutput {
fn from_schema(schema: &EntityLoaderSchema) -> Self {
let total_count = schema.fields.iter().fold(
HashMap::<&TypePath, usize>::new(),
|mut total_count, field| {
*total_count.entry(&field.entity).or_insert(0) += 1;
total_count
},
);
let mut relation_tuple_impls = HashSet::<&TypePath>::new();
let mut select_arity = 1;
let mut output = Self::default();
output.select_tuple_fields.push(quote!(model));
for field in &schema.fields {
let duplicate_entity = total_count
.get(&field.entity)
.is_some_and(|count| *count != 1);
field.expand_loader_with_field_into(&mut output);
field.expand_loader_nest_field_into(&mut output);
field.expand_loader_with_set_impl_into(&mut output, duplicate_entity);
field.expand_loader_with_2_impl_into(&mut output, duplicate_entity);
if let EntityLoaderFieldKind::ManyToManySelf {
junction_module,
reverse,
} = &field.kind
{
field.expand_many_to_many_self_with_param_into(
&mut output,
junction_module,
*reverse,
);
}
if matches!(
&field.kind,
EntityLoaderFieldKind::HasOne
| EntityLoaderFieldKind::HasMany
| EntityLoaderFieldKind::ManyToMany
) && duplicate_entity
&& field.relation_enum.is_some()
&& relation_tuple_impls.insert(&field.entity)
{
field.expand_relation_tuple_with_param_into(&mut output);
}
if matches!(&field.kind, EntityLoaderFieldKind::HasOne) && !duplicate_entity {
select_arity += 1;
if select_arity <= 3 {
field.expand_select_one_into(&mut output);
}
}
field.expand_load_into(&mut output, duplicate_entity);
}
output
}
}
pub fn expand_entity_loader(vis: &Visibility, schema: EntityLoaderSchema) -> TokenStream {
let EntityLoaderOutput {
loader_with_fields,
loader_nest_fields,
select_tuple_fields,
loader_with_set_impl,
loader_with_2_impl,
fetch_select_impl,
assemble_one,
load_one,
load_many,
load_one_nest,
load_many_nest,
load_one_nest_nest,
load_many_nest_nest,
with_param_impls,
} = EntityLoaderOutput::from_schema(&schema);
let (async_, await_) = if cfg!(feature = "async") {
(quote!(async), quote!(.await))
} else {
(quote!(), quote!())
};
let async_trait = if cfg!(feature = "async") {
quote!(#[async_trait::async_trait])
} else {
quote!()
};
quote! {
#[doc = " Generated by sea-orm-macros"]
#[derive(Clone)]
#vis struct EntityLoader {
select: sea_orm::Select<Entity>,
with: EntityLoaderWith,
nest: EntityLoaderNest,
}
#[doc = " Generated by sea-orm-macros"]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#vis struct EntityReverse;
impl sea_orm::compound::EntityReverse for EntityReverse {
type Entity = Entity;
}
#[doc = " Generated by sea-orm-macros"]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#vis struct EntityLoaderWith {
#loader_with_fields
}
#[doc = " Generated by sea-orm-macros"]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#vis struct EntityLoaderNest {
#loader_nest_fields
}
impl Entity {
#[doc = " Generated by sea-orm-macros"]
pub const REVERSE: EntityReverse = EntityReverse;
}
impl EntityLoaderWith {
#[doc = " Generated by sea-orm-macros"]
pub fn is_empty(&self) -> bool {
self == &Self::default()
}
#[doc = " Generated by sea-orm-macros"]
pub fn set(&mut self, target: sea_orm::compound::LoadTarget) {
#loader_with_set_impl
}
}
#[doc = " Parameters for EntityLoader"]
#vis trait EntityLoaderWithParam {
#[doc = " Generated by sea-orm-macros"]
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>);
}
#[automatically_derived]
impl<R> EntityLoaderWithParam for R
where
R: EntityTrait,
Entity: Related<R>,
{
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(sea_orm::compound::LoadTarget::TableRef(self.table_ref()), None)
}
}
#[automatically_derived]
impl<R, S> EntityLoaderWithParam for (R, S)
where
R: EntityTrait,
Entity: Related<R>,
S: EntityTrait,
R: Related<S>,
{
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(
sea_orm::compound::LoadTarget::TableRef(self.0.table_ref()),
Some(sea_orm::compound::LoadTarget::TableRef(self.1.table_ref())),
)
}
}
#[automatically_derived]
impl<R, S> EntityLoaderWithParam for sea_orm::compound::EntityLoaderWithSelf<R, S>
where
R: EntityTrait,
Entity: Related<R>,
S: EntityTrait,
R: RelatedSelfVia<S>,
{
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(
sea_orm::compound::LoadTarget::TableRef(self.0.table_ref()),
Some(sea_orm::compound::LoadTarget::TableRef(self.1.table_ref())),
)
}
}
#[automatically_derived]
impl<R, S, SR> EntityLoaderWithParam for sea_orm::compound::EntityLoaderWithSelfRev<R, SR>
where
R: EntityTrait,
Entity: Related<R>,
S: EntityTrait,
R: RelatedSelfVia<S>,
SR: sea_orm::compound::EntityReverse<Entity = S>,
{
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(
sea_orm::compound::LoadTarget::TableRef(self.0.table_ref()),
Some(sea_orm::compound::LoadTarget::TableRefRev(S::default().table_ref())),
)
}
}
#[automatically_derived]
impl EntityLoaderWithParam for Relation {
fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option<sea_orm::compound::LoadTarget>) {
(sea_orm::compound::LoadTarget::Relation(self.name()), None)
}
}
#with_param_impls
#[automatically_derived]
impl std::fmt::Debug for EntityLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntityLoader")
.field("select", &match (Entity::default().schema_name(), Entity::default().table_name()) {
(Some(s), t) => format!("{s}.{t}"),
(None, t) => t.to_owned(),
})
.field("with", &self.with)
.field("nest", &self.nest)
.finish()
}
}
#[automatically_derived]
impl sea_orm::QueryFilter for EntityLoader {
type QueryStatement = <sea_orm::Select<Entity> as sea_orm::QueryFilter>::QueryStatement;
fn query(&mut self) -> &mut Self::QueryStatement {
sea_orm::QueryFilter::query(&mut self.select)
}
}
#[automatically_derived]
impl sea_orm::QueryOrder for EntityLoader {
type QueryStatement = <sea_orm::Select<Entity> as sea_orm::QueryOrder>::QueryStatement;
fn query(&mut self) -> &mut Self::QueryStatement {
sea_orm::QueryOrder::query(&mut self.select)
}
}
#[automatically_derived]
#async_trait
impl sea_orm::compound::EntityLoaderTrait<Entity> for EntityLoader {
type ModelEx = ModelEx;
#async_ fn fetch<C: sea_orm::ConnectionTrait>(self, db: &C, page: u64, page_size: u64) -> Result<Vec<Self::ModelEx>, sea_orm::DbErr> {
self.fetch(db, page, page_size)#await_
}
#async_ fn num_items<C: sea_orm::ConnectionTrait>(self, db: &C, page_size: u64) -> Result<u64, sea_orm::DbErr> {
self.select.paginate(db, page_size).num_items()#await_
}
}
impl Entity {
#[doc = " Generated by sea-orm-macros"]
pub fn load() -> EntityLoader {
EntityLoader {
select: Entity::find(),
with: Default::default(),
nest: Default::default(),
}
}
}
impl EntityLoader {
#[doc = " Generated by sea-orm-macros"]
pub #async_ fn one<C: sea_orm::ConnectionTrait>(mut self, db: &C) -> Result<Option<ModelEx>, sea_orm::DbErr> {
use sea_orm::QuerySelect;
self.select = self.select.limit(1);
Ok(self.all(db)#await_?.into_iter().next())
}
#[doc = " Generated by sea-orm-macros"]
pub #async_ fn all<C: sea_orm::ConnectionTrait>(self, db: &C) -> Result<Vec<ModelEx>, sea_orm::DbErr> {
self.fetch(db, 0, 0)#await_
}
#[doc = " Generated by sea-orm-macros"]
pub fn with<T: EntityLoaderWithParam>(mut self, param: T) -> Self {
match param.into_with_param() {
(left, None) => self.with_1(left),
(left, Some(right)) => self.with_2(left, right),
}
}
fn with_1(mut self, load_target: sea_orm::compound::LoadTarget) -> Self {
self.with.set(load_target);
self
}
fn with_2(mut self, left: sea_orm::compound::LoadTarget, right: sea_orm::compound::LoadTarget) -> Self {
#loader_with_2_impl
self
}
#[doc = " Generated by sea-orm-macros"]
#async_ fn fetch<C: sea_orm::ConnectionTrait>(mut self, db: &C, page: u64, page_size: u64) -> Result<Vec<ModelEx>, sea_orm::DbErr> {
let select = self.select;
let mut loaded = EntityLoaderWith::default();
#fetch_select_impl
let models = if page_size != 0 {
select.paginate(db, page_size).fetch_page(page)#await_?
} else {
select.all(db)#await_?
};
let models = models.into_iter().map(|(#select_tuple_fields)| {
let mut model = model.into_ex();
#assemble_one
model
}).collect::<Vec<_>>();
let models = Self::load(models, &self.with, &self.nest, db)#await_?;
Ok(models)
}
#[doc = " Generated by sea-orm-macros"]
pub #async_ fn load<C: sea_orm::ConnectionTrait>(mut models: Vec<ModelEx>, with: &EntityLoaderWith, nest: &EntityLoaderNest, db: &C) -> Result<Vec<ModelEx>, DbErr> {
use sea_orm::LoaderTraitEx;
#load_one
#load_many
Ok(models)
}
#[doc = " Generated by sea-orm-macros"]
pub #async_ fn load_nest<C: sea_orm::ConnectionTrait>(mut models: Vec<Option<ModelEx>>, with: &EntityLoaderWith, db: &C) -> Result<Vec<Option<ModelEx>>, DbErr> {
use sea_orm::LoaderTraitEx;
#load_one_nest
#load_many_nest
Ok(models)
}
#[doc = " Generated by sea-orm-macros"]
pub #async_ fn load_nest_nest<C: sea_orm::ConnectionTrait>(mut models: Vec<Vec<ModelEx>>, with: &EntityLoaderWith, db: &C) -> Result<Vec<Vec<ModelEx>>, DbErr> {
use sea_orm::NestedLoaderTrait;
#load_one_nest_nest
#load_many_nest_nest
Ok(models)
}
}
}
}
@@ -0,0 +1,634 @@
use super::case_style::{CaseStyle, CaseStyleHelpers};
use super::util::{consume_meta, escape_rust_keyword, trim_starting_raw_identifier};
use heck::{
ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase,
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use std::str::FromStr;
use syn::{
Attribute, Data, Fields, Lit, LitStr, Visibility, punctuated::Punctuated, spanned::Spanned,
token::Comma,
};
const NOT_AUTO_INCRE_TYPE_SUFFIX: [&str; 2] = ["String", "Uuid"];
#[allow(dead_code)]
fn convert_case(s: &str, case_style: CaseStyle) -> String {
match case_style {
CaseStyle::PascalCase => s.to_upper_camel_case(),
CaseStyle::KebabCase => s.to_kebab_case(),
CaseStyle::MixedCase => s.to_lower_camel_case(),
CaseStyle::ShoutySnakeCase => s.to_shouty_snake_case(),
CaseStyle::SnakeCase => s.to_snake_case(),
CaseStyle::TitleCase => s.to_title_case(),
CaseStyle::UpperCase => s.to_uppercase(),
CaseStyle::LowerCase => s.to_lowercase(),
CaseStyle::ScreamingKebabCase => s.to_kebab_case().to_uppercase(),
CaseStyle::CamelCase => {
let camel_case = s.to_upper_camel_case();
let mut result = String::with_capacity(camel_case.len());
let mut it = camel_case.chars();
if let Some(ch) = it.next() {
result.extend(ch.to_lowercase());
}
result.extend(it);
result
}
}
}
#[cfg(feature = "with-json")]
fn serde_deserialize_name(
orig: &str,
serde_rename: Option<&str>,
serde_rename_all: Option<CaseStyle>,
) -> String {
if let Some(rename) = serde_rename.as_ref() {
return rename.to_string();
}
if let Some(case_style) = serde_rename_all {
convert_case(orig, case_style)
} else {
orig.to_string()
}
}
/// Method to derive an Model
pub fn expand_derive_entity_model(
vis: &Visibility,
data: &Data,
attrs: &[Attribute],
) -> syn::Result<TokenStream> {
// if #[sea_orm(table_name = "foo", schema_name = "bar")] specified, create Entity struct
let mut table_name = None;
let mut comment = quote! {None};
let mut schema_name = quote! { None };
let mut table_iden = false;
let mut model_ex = false;
let mut rename_all: Option<CaseStyle> = None;
let mut serde_rename_all: Option<CaseStyle> = None;
// Parse #[serde(rename_all = "...")] at struct level
attrs
.iter()
.filter(|attr| attr.path().is_ident("serde"))
.try_for_each(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
if let Ok(lit) = meta.value().and_then(|v| v.parse::<LitStr>()) {
// #[serde(rename_all = "camelCase")]
serde_rename_all = CaseStyle::from_str(&lit.value()).ok();
} else {
// #[serde(rename_all(serialize = "...", deserialize = "..."))]
meta.parse_nested_meta(|nested| {
if nested.path.is_ident("deserialize") {
let lit: LitStr = nested.value()?.parse()?;
serde_rename_all = CaseStyle::from_str(&lit.value()).ok();
} else {
consume_meta(nested);
}
Ok(())
})?;
}
} else {
consume_meta(meta);
}
Ok(())
})
})?;
attrs
.iter()
.filter(|attr| attr.path().is_ident("sea_orm"))
.try_for_each(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("comment") {
let name: Lit = meta.value()?.parse()?;
comment = quote! { Some(#name) };
} else if meta.path.is_ident("table_name") {
table_name = Some(meta.value()?.parse::<LitStr>()?);
} else if meta.path.is_ident("schema_name") {
let name: Lit = meta.value()?.parse()?;
schema_name = quote! { Some(#name) };
} else if meta.path.is_ident("table_iden") {
table_iden = true;
} else if meta.path.is_ident("model_ex") {
model_ex = true;
} else if meta.path.is_ident("rename_all") {
rename_all = Some((&meta).try_into()?);
} else {
consume_meta(meta);
}
Ok(())
})
})?;
let entity_def = table_name
.as_ref()
.map(|table_name| {
let entity_extra_attr = if model_ex {
quote!(#[sea_orm(model_ex = ModelEx, active_model_ex = ActiveModelEx)])
} else {
quote!()
};
quote! {
#[doc = " Generated by sea-orm-macros"]
#[derive(Copy, Clone, Default, Debug, sea_orm::prelude::DeriveEntity)]
#entity_extra_attr
#vis struct Entity;
#[automatically_derived]
impl sea_orm::prelude::EntityName for Entity {
fn schema_name(&self) -> Option<&str> {
#schema_name
}
fn table_name(&self) -> &'static str {
#table_name
}
fn comment(&self) -> Option<&str> {
#comment
}
}
}
})
.unwrap_or_default();
// generate Column enum and it's ColumnTrait impl
let mut columns_enum: Punctuated<_, Comma> = Punctuated::new();
let mut columns_trait: Punctuated<_, Comma> = Punctuated::new();
let mut columns_enum_type_name: Punctuated<_, Comma> = Punctuated::new();
let mut columns_select_as: Punctuated<_, Comma> = Punctuated::new();
let mut columns_save_as: Punctuated<_, Comma> = Punctuated::new();
let mut primary_keys: Punctuated<_, Comma> = Punctuated::new();
let mut primary_key_types: Punctuated<_, Comma> = Punctuated::new();
let mut auto_increment: Option<bool> = None;
#[cfg(feature = "with-json")]
let mut columns_json_keys: Punctuated<_, Comma> = Punctuated::new();
if table_iden {
if let Some(table_name) = &table_name {
let table_field_name = Ident::new("Table", Span::call_site());
columns_enum.push(quote! {
#[doc = " Generated by sea-orm-macros"]
#[sea_orm(table_name=#table_name)]
#[strum(disabled)]
#table_field_name
});
columns_trait.push(
quote! { Self::#table_field_name => panic!("Table cannot be used as a column") },
);
}
}
if let Data::Struct(item_struct) = data {
if let Fields::Named(fields) = &item_struct.fields {
for field in &fields.named {
if let Some(ident) = &field.ident {
let original_field_name = trim_starting_raw_identifier(ident);
let mut field_name =
Ident::new(&original_field_name.to_upper_camel_case(), ident.span());
let mut nullable = false;
let mut default_value = None;
let mut comment = None;
let mut default_expr = None;
let mut select_as = None;
let mut save_as = None;
let mut unique_key = None;
let mut renamed_from = None;
let mut indexed = false;
let mut ignore = false;
let mut unique = false;
let mut sql_type = None;
let mut enum_name = None;
let mut is_primary_key = false;
let mut is_auto_increment = false;
let mut extra = None;
let mut seaography_ignore = false;
#[cfg(feature = "with-json")]
let mut serde_rename: Option<String> = None;
let mut column_name = if let Some(case_style) = rename_all {
Some(field_name.convert_case(Some(case_style)))
} else if original_field_name
!= original_field_name.to_upper_camel_case().to_snake_case()
{
// `to_snake_case` was used to trim prefix and tailing underscore
Some(original_field_name.to_snake_case())
} else {
None
};
// search for #[sea_orm(primary_key, auto_increment = false, column_type = "String(StringLen::N(255))", default_value = "new user", default_expr = "gen_random_uuid()", column_name = "name", enum_name = "Name", nullable, indexed, unique)]
for attr in field.attrs.iter() {
if attr.path().is_ident("sea_orm") {
// single param
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("column_type") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
let ty: TokenStream = syn::parse_str(&litstr.value())?;
sql_type = Some(ty);
} else {
return Err(
meta.error(format!("Invalid column_type {lit:?}"))
);
}
} else if meta.path.is_ident("auto_increment") {
let lit = meta.value()?.parse()?;
if let Lit::Bool(litbool) = lit {
is_auto_increment = litbool.value();
auto_increment = Some(litbool.value());
} else {
return Err(
meta.error(format!("Invalid auto_increment = {lit:?}"))
);
}
} else if meta.path.is_ident("comment") {
comment = Some(meta.value()?.parse::<Lit>()?);
} else if meta.path.is_ident("default_value") {
default_value = Some(meta.value()?.parse::<Lit>()?);
} else if meta.path.is_ident("default_expr") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
let value_expr: TokenStream =
syn::parse_str(&litstr.value())?;
default_expr = Some(value_expr);
} else {
return Err(
meta.error(format!("Invalid column_type {lit:?}"))
);
}
} else if meta.path.is_ident("column_name") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
column_name = Some(litstr.value());
} else {
return Err(
meta.error(format!("Invalid column_name {lit:?}"))
);
}
} else if meta.path.is_ident("enum_name") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
let ty: Ident = syn::parse_str(&litstr.value())?;
enum_name = Some(ty);
} else {
return Err(
meta.error(format!("Invalid enum_name {lit:?}"))
);
}
} else if meta.path.is_ident("select_as") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
select_as = Some(litstr.value());
} else {
return Err(
meta.error(format!("Invalid select_as {lit:?}"))
);
}
} else if meta.path.is_ident("save_as") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
save_as = Some(litstr.value());
} else {
return Err(meta.error(format!("Invalid save_as {lit:?}")));
}
} else if meta.path.is_ident("ignore") {
ignore = true;
} else if meta.path.is_ident("primary_key") {
is_primary_key = true;
primary_key_types.push(field.ty.clone());
} else if meta.path.is_ident("nullable") {
nullable = true;
} else if meta.path.is_ident("indexed") {
indexed = true;
} else if meta.path.is_ident("unique") {
unique = true;
} else if meta.path.is_ident("unique_key") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
unique_key = Some(litstr.value());
} else {
return Err(
meta.error(format!("Invalid unique_key {lit:?}"))
);
}
} else if meta.path.is_ident("renamed_from") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
renamed_from = Some(litstr.value());
} else {
return Err(
meta.error(format!("Invalid renamed_from {lit:?}"))
);
}
} else if meta.path.is_ident("extra") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
extra = Some(litstr.value());
} else {
return Err(meta.error(format!("Invalid extra {lit:?}")));
}
} else {
consume_meta(meta);
}
Ok(())
})?;
} else if attr.path().is_ident("seaography") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("ignore") {
seaography_ignore = true;
}
Ok(())
})?;
} else if cfg!(feature = "with-json") && attr.path().is_ident("serde") {
#[cfg(feature = "with-json")]
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
if let Ok(lit) = meta.value().and_then(|v| v.parse::<LitStr>())
{
// #[serde(rename = "xxx")]
serde_rename = Some(lit.value());
} else {
// #[serde(rename(serialize = "...", deserialize = "..."))]
meta.parse_nested_meta(|nested| {
if nested.path.is_ident("deserialize") {
let lit: LitStr = nested.value()?.parse()?;
serde_rename = Some(lit.value());
} else {
consume_meta(nested);
}
Ok(())
})?;
}
} else {
consume_meta(meta);
}
Ok(())
})?;
}
}
#[cfg(feature = "with-json")]
let json_key_name = serde_deserialize_name(
&original_field_name,
serde_rename.as_deref(),
serde_rename_all,
);
if let Some(enum_name) = enum_name {
field_name = enum_name;
}
field_name = Ident::new(&escape_rust_keyword(field_name), ident.span());
let variant_attrs = match &column_name {
Some(column_name) => quote! {
#[sea_orm(column_name = #column_name)]
#[doc = " Generated by sea-orm-macros"]
},
None => quote! {
#[doc = " Generated by sea-orm-macros"]
},
};
let field_type = &field.ty;
let field_type: String = quote! { #field_type }
.to_string() // e.g.: "Option < String >"
.split_whitespace()
.collect(); // Remove all whitespace
if ignore {
continue;
} else {
columns_enum.push(quote! {
#variant_attrs
#field_name
});
#[cfg(feature = "with-json")]
columns_json_keys.push(quote! {
Self::#field_name => #json_key_name
});
}
if is_primary_key {
primary_keys.push(quote! {
#variant_attrs
#field_name
});
}
if !is_primary_key && is_auto_increment {
return Err(syn::Error::new_spanned(
ident,
"auto_increment can only be used on primary_key",
));
}
if primary_key_types.len() > 1 && is_auto_increment {
return Err(syn::Error::new_spanned(
ident,
"auto_increment cannot be used on composite primary_key",
));
}
if let Some(select_as) = select_as {
columns_select_as.push(quote! {
Self::#field_name => sea_orm::sea_query::ExprTrait::cast_as(expr, #select_as)
});
}
if let Some(save_as) = save_as {
columns_save_as.push(quote! {
Self::#field_name => sea_orm::sea_query::ExprTrait::cast_as(val, #save_as)
});
}
let field_type = if field_type.starts_with("Option<") {
nullable = true;
&field_type["Option<".len()..(field_type.len() - 1)] // Extract `T` out of `Option<T>`
} else {
field_type.as_str()
};
let field_span = field.span();
if is_primary_key && auto_increment.is_none() {
for suffix in NOT_AUTO_INCRE_TYPE_SUFFIX {
if field_type.ends_with(suffix) {
auto_increment = Some(false);
break;
}
}
}
let sea_query_col_type =
super::value_type_match::column_type_expr(sql_type, field_type, field_span);
let col_def =
quote! { sea_orm::prelude::ColumnTypeTrait::def(#sea_query_col_type) };
let mut match_row = quote! { Self::#field_name => #col_def };
if nullable {
match_row = quote! { #match_row.nullable() };
}
if indexed {
match_row = quote! { #match_row.indexed() };
}
if unique {
match_row = quote! { #match_row.unique() };
}
if seaography_ignore {
match_row = quote! { #match_row.seaography_ignore() };
}
if unique_key.is_some() {
match_row = quote! { #match_row.unique_key(#unique_key) };
}
if renamed_from.is_some() {
match_row = quote! { #match_row.renamed_from(#renamed_from) };
}
if let Some(default_value) = default_value {
match_row = quote! { #match_row.default_value(#default_value) };
}
if let Some(comment) = comment {
match_row = quote! { #match_row.comment(#comment) };
}
if let Some(default_expr) = default_expr {
match_row = quote! { #match_row.default(#default_expr) };
}
if let Some(extra) = extra {
match_row = quote! { #match_row.extra(#extra) };
}
// match_row = quote! { #match_row.comment() };
columns_trait.push(match_row);
let ty: syn::Type = syn::LitStr::new(field_type, field_span)
.parse()
.expect("field type error");
let enum_type_name = quote::quote_spanned! { field_span =>
<#ty as sea_orm::sea_query::ValueType>::enum_type_name()
};
columns_enum_type_name.push(quote! {
Self::#field_name => #enum_type_name
});
}
}
}
}
// Add tailing comma
if !columns_select_as.is_empty() {
columns_select_as.push_punct(Comma::default());
}
if !columns_save_as.is_empty() {
columns_save_as.push_punct(Comma::default());
}
let primary_key = {
let auto_increment = match auto_increment {
Some(value) => value && primary_keys.len() == 1,
None => primary_keys.len() == 1,
};
let primary_key_types = if primary_key_types.len() == 1 {
let first = primary_key_types.first();
quote! { #first }
} else {
quote! { (#primary_key_types) }
};
quote! {
#[doc = " Generated by sea-orm-macros"]
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
#vis enum PrimaryKey {
#primary_keys
}
#[automatically_derived]
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = #primary_key_types;
fn auto_increment() -> bool {
#auto_increment
}
}
}
};
let impl_model_ex = if model_ex {
quote!()
} else {
quote! {
impl Model {
#[doc = " Generated by sea-orm-macros"]
pub fn into_ex(self) -> Self {
self
}
}
}
};
let with_json_impls = {
#[cfg(feature = "with-json")]
quote! {
fn json_key(&self) -> &'static str {
match self {
#columns_json_keys
}
}
}
#[cfg(not(feature = "with-json"))]
quote! {}
};
Ok(quote! {
#impl_model_ex
#[doc = " Generated by sea-orm-macros"]
#[derive(Copy, Clone, Debug, sea_orm::prelude::EnumIter, sea_orm::prelude::DeriveColumn)]
#vis enum Column {
#columns_enum
}
#[automatically_derived]
impl sea_orm::prelude::ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> sea_orm::prelude::ColumnDef {
match self {
#columns_trait
}
}
fn enum_type_name(&self) -> Option<&'static str> {
match self {
#columns_enum_type_name
}
}
fn select_as(&self, expr: sea_orm::sea_query::Expr) -> sea_orm::sea_query::SimpleExpr {
match self {
#columns_select_as
_ => sea_orm::prelude::ColumnTrait::select_enum_as(self, expr),
}
}
fn save_as(&self, val: sea_orm::sea_query::Expr) -> sea_orm::sea_query::SimpleExpr {
match self {
#columns_save_as
_ => sea_orm::prelude::ColumnTrait::save_enum_as(self, val),
}
}
#with_json_impls
}
#entity_def
#primary_key
})
}
@@ -0,0 +1,244 @@
use std::collections::{HashMap, hash_map::Entry};
use super::util::GetMeta;
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, quote};
use syn::{
Data, DataStruct, DeriveInput, Error, Fields, Generics, Meta, ext::IdentExt,
punctuated::Punctuated, token::Comma,
};
pub(super) enum ItemType {
Flat,
Skip,
Nested { prefix: Option<String> },
}
pub(super) struct DeriveFromQueryResult {
pub ident: syn::Ident,
pub generics: Generics,
pub fields: Vec<FromQueryResultItem>,
}
pub(super) struct FromQueryResultItem {
pub typ: ItemType,
pub ident: Ident,
pub alias: Option<String>,
}
/// Initially, we try to obtain the value for each field and check if it is an ordinary DB error
/// (which we return immediatly), or a null error.
///
/// ### Background
///
/// Null errors do not necessarily mean that the deserialization as a whole fails,
/// since structs embedding the current one might have wrapped the current one in an `Option`.
/// In this case, we do not want to swallow other errors, which are very likely to actually be
/// programming errors that should be noticed (and fixed).
struct TryFromQueryResultCheck<'a>(bool, &'a FromQueryResultItem);
impl ToTokens for TryFromQueryResultCheck<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let FromQueryResultItem { ident, typ, alias } = self.1;
match typ {
ItemType::Flat => {
let name = alias
.to_owned()
.unwrap_or_else(|| ident.unraw().to_string());
tokens.extend(quote! {
let #ident = match row.try_get_nullable(pre, #name) {
Err(v @ sea_orm::TryGetError::DbErr(_)) => {
return Err(v);
}
v => v,
};
});
}
ItemType::Skip => {
tokens.extend(quote! {
let #ident = std::default::Default::default();
});
}
ItemType::Nested { prefix } => {
let prefix = match (self.0, prefix) {
(_, Some(p)) => quote! { &format!("{pre}{}", #p) },
(true, None) => {
let name = ident.unraw().to_string();
quote! { &format!("{pre}{}_", #name) }
}
(false, None) => quote! { pre },
};
tokens.extend(quote! {
let #ident = match sea_orm::FromQueryResult::from_query_result_nullable(row, #prefix) {
Err(v @ sea_orm::TryGetError::DbErr(_)) => {
return Err(v);
}
v => v,
};
});
}
}
}
}
struct TryFromQueryResultAssignment<'a>(&'a FromQueryResultItem);
impl ToTokens for TryFromQueryResultAssignment<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let FromQueryResultItem { ident, typ, .. } = self.0;
match typ {
ItemType::Flat | ItemType::Nested { .. } => {
tokens.extend(quote! {
#ident: #ident?,
});
}
ItemType::Skip => {
tokens.extend(quote! {
#ident,
});
}
}
}
}
impl DeriveFromQueryResult {
fn new(
DeriveInput {
ident,
data,
generics,
..
}: DeriveInput,
) -> Result<Self, Error> {
let parsed_fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(named),
..
}) => named.named,
_ => {
return Err(Error::new(
ident.span(),
"you can only derive `FromQueryResult` on named struct",
));
}
};
let mut fields = Vec::with_capacity(parsed_fields.len());
let mut seen_nested: HashMap<(syn::Type, Option<String>), TokenStream> = HashMap::new();
for parsed_field in parsed_fields {
let mut typ = ItemType::Flat;
let mut alias = None;
for attr in parsed_field.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
{
for meta in list.iter() {
if meta.exists("skip") {
typ = ItemType::Skip;
} else if meta.exists("nested") {
typ = ItemType::Nested { prefix: None };
} else if let Some(list) = meta.get_list_args("nested") {
let mut prefix = None;
for m in list.iter() {
match m.get_as_kv("prefix") {
Some(p) => prefix = (!p.is_empty()).then_some(p),
None => {
return Err(Error::new_spanned(
m,
"invalid nested attribute, expected `prefix = \"...\"`",
));
}
}
}
typ = ItemType::Nested { prefix };
} else {
alias = meta
.get_as_kv("from_alias")
.or_else(|| meta.get_as_kv("alias"));
}
}
}
}
let field_tokens = parsed_field.to_token_stream();
let ident = parsed_field.ident.unwrap();
if let ItemType::Nested {
prefix: Some(prefix),
} = &typ
{
let key = (parsed_field.ty, Some(prefix.clone()));
match seen_nested.entry(key) {
Entry::Occupied(e) => {
let msg = format!(
"multiple nested fields with the same type share prefix \"{prefix}\""
);
let mut err = Error::new_spanned(&field_tokens, msg);
err.combine(Error::new_spanned(e.get(), "first defined here"));
return Err(err);
}
Entry::Vacant(e) => {
e.insert(field_tokens);
}
}
}
fields.push(FromQueryResultItem { typ, ident, alias });
}
Ok(Self {
ident,
generics,
fields,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
Ok(self.impl_from_query_result(false))
}
pub(super) fn impl_from_query_result(&self, prefix: bool) -> TokenStream {
let Self {
ident,
generics,
fields,
} = self;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let ident_try_init: Vec<_> = fields
.iter()
.map(|s| TryFromQueryResultCheck(prefix, s))
.collect();
let ident_try_assign: Vec<_> = fields.iter().map(TryFromQueryResultAssignment).collect();
quote!(
#[automatically_derived]
impl #impl_generics sea_orm::FromQueryResult for #ident #ty_generics #where_clause {
fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result<Self, sea_orm::DbErr> {
Ok(Self::from_query_result_nullable(row, pre)?)
}
fn from_query_result_nullable(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result<Self, sea_orm::TryGetError> {
#(#ident_try_init)*
Ok(Self {
#(#ident_try_assign)*
})
}
}
)
}
}
pub fn expand_derive_from_query_result(input: DeriveInput) -> syn::Result<TokenStream> {
DeriveFromQueryResult::new(input)?.expand()
}
@@ -0,0 +1,287 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use syn::{Meta, PathArguments, PathSegment, punctuated::Punctuated, token::Comma};
use super::util::GetMeta;
enum Error {
InputNotStruct,
Syn(syn::Error),
}
/// Matches all potential ways to convert struct fields into ActiveModel ones
pub(super) enum IntoActiveModelField {
/// `IntoActiveValue::into_active_value(self.field).into()`
Normal(syn::Ident),
/// Option<T> with fallback: `Some(v) => Set(v).into(), None => Set(expr).into()`
WithDefault { ident: syn::Ident, expr: syn::Expr },
}
impl IntoActiveModelField {
pub(super) fn ident(&self) -> &syn::Ident {
match self {
IntoActiveModelField::Normal(ident) => ident,
IntoActiveModelField::WithDefault { ident, .. } => ident,
}
}
}
/// Contains all the information extracted from the input struct and its attributes
/// needed to generate the `IntoActiveModel` trait implementation.
pub(super) struct DeriveIntoActiveModel {
/// The identifier of the input struct
pub ident: syn::Ident,
/// Optional explicit ActiveModel type specified via `#[sea_orm(active_model = "Type")]`
pub active_model: Option<syn::Type>,
/// handles provided struct fields
pub fields: Vec<IntoActiveModelField>,
/// handles fields set by #[sea_orm(set(field = expr))]
pub set_fields: Vec<(syn::Ident, syn::Expr)>,
/// require all fields specified, no `..default::Default()`
pub exhaustive: bool,
}
impl DeriveIntoActiveModel {
/// This function finds attributes relevant for this macros:
/// Container attributes (#[sea_orm(...)]) on the struct for:
/// - active_model: explicit ActiveModel type
/// - exhaustive: require all fields to be set
/// - set(...): provided values for ommited fields
///
/// Field attributes (#[sea_orm(...)]) with:
/// - ignore/skip: exclude from conversion
/// - default: fallback value for Option<T> fields
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let fields = match input.data {
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { named, .. }),
..
}) => named,
_ => return Err(Error::InputNotStruct),
};
let mut active_model = None;
let mut set_fields = Vec::new();
let mut exhaustive = false;
for attr in input.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
// Parse container attributes: #[sea_orm(...)]
// Supports:
// - active_model = "Type": explicitly specify the ActiveModel type
// - exhaustive: require all ActiveModel fields to be explicitly set
// - set(field = expr, ...): provide default values for fields not in the input struct
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated) {
for meta in list {
// Parse active_model attribute: #[sea_orm(active_model = "MyActiveModel")]
if let Some(s) = meta.get_as_kv("active_model") {
active_model = Some(syn::parse_str::<syn::Type>(&s).map_err(Error::Syn)?);
}
// Parse exhaustive flag: #[sea_orm(exhaustive)]
// When set, prevents using Default::default() for unspecified fields
if meta.exists("exhaustive") {
exhaustive = true;
}
// Parse set attribute: #[sea_orm(set(field1 = expr1, field2 = expr2, ...))]
// Collects field assignments to be included in the generated ActiveModel
if let Meta::List(meta_list) = &meta {
if meta_list.path.is_ident("set") {
let nested = meta_list
.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
.map_err(Error::Syn)?;
for nested_meta in nested {
if let Some(val) = nested_meta.get_as_kv_with_ident() {
let (ident, expr_str) = val;
let expr = syn::parse_str::<syn::Expr>(&expr_str)
.map_err(Error::Syn)?;
set_fields.push((ident, expr));
}
}
}
}
}
}
}
// Field attributes
let mut field_idents: Vec<IntoActiveModelField> = Vec::new();
for field in fields.iter() {
if let Some(f) = parse_field(field)? {
field_idents.push(f);
}
}
Ok(Self {
ident: input.ident,
active_model,
fields: field_idents,
set_fields,
exhaustive,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
let expanded_impl_into_active_model = self.impl_into_active_model();
Ok(expanded_impl_into_active_model)
}
/// Generates the implementation of `IntoActiveModel` trait for the input struct
pub(super) fn impl_into_active_model(&self) -> TokenStream {
let Self {
ident,
active_model,
fields,
set_fields,
exhaustive,
} = self;
let mut active_model_ident = active_model
.clone()
.unwrap_or_else(|| syn::parse_str::<syn::Type>("ActiveModel").unwrap());
// Create a type alias for qualified types
let type_alias_definition = if is_qualified_type(&active_model_ident) {
let type_alias = format_ident!("ActiveModelFor{ident}");
let type_def = quote!( type #type_alias = #active_model_ident; );
active_model_ident = syn::Type::Path(syn::TypePath {
qself: None,
path: syn::Path {
leading_colon: None,
segments: [PathSegment {
ident: type_alias,
arguments: PathArguments::None,
}]
.into_iter()
.collect(),
},
});
type_def
} else {
quote!()
};
let field_idents: Vec<_> = fields.iter().map(|f| f.ident()).collect();
// Generate field conversion code based on field type
let expanded_fields = fields.iter().map(|field| match field {
IntoActiveModelField::Normal(ident) => quote!(
sea_orm::IntoActiveValue::<_>::into_active_value(self.#ident).into()
),
IntoActiveModelField::WithDefault { ident, expr } => quote!({
match self.#ident.into() {
Some(v) => sea_orm::ActiveValue::Set(v).into(),
None => sea_orm::ActiveValue::Set(#expr).into(),
}
}),
});
// Add custom field assignments from #[sea_orm(set(field = expr))]
let (set_idents, set_exprs): (Vec<_>, Vec<_>) = set_fields.iter().cloned().unzip();
let expanded_sets = set_exprs.iter().map(|expr| {
quote!(
sea_orm::ActiveValue::Set(#expr)
)
});
// Add defaults(Unset) unless exhaustive mode is enabled
let rest = if *exhaustive {
quote!()
} else {
quote!(..::std::default::Default::default())
};
quote!(
#type_alias_definition
#[automatically_derived]
impl sea_orm::IntoActiveModel<#active_model_ident> for #ident {
fn into_active_model(self) -> #active_model_ident {
#active_model_ident {
#( #field_idents: #expanded_fields, )*
#( #set_idents: #expanded_sets, )*
#rest
}
}
}
)
}
}
/// Parse field-level attributes on each struct field
/// Supports:
/// - ignore or skip: exclude the field from conversion
/// - default = "expr": provide a fallback value for Option<T> fields (Some(v) => Set(v), None => Set(expr))
fn parse_field(field: &syn::Field) -> Result<Option<IntoActiveModelField>, Error> {
let ident = field.ident.as_ref().unwrap().clone();
// Default expression for this field
let mut default_expr: Option<syn::Expr> = None;
for attr in field.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
// Parse the attribute arguments: #[sea_orm(...)]
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated) {
for meta in list.iter() {
// Check for ignore/skip: #[sea_orm(ignore)] or #[sea_orm(skip)]
if meta.exists("ignore") || meta.exists("skip") {
return Ok(None);
}
// Check for bare default: #[sea_orm(default)]
if meta.exists("default") {
if default_expr.is_some() {
return Err(Error::Syn(syn::Error::new_spanned(
meta,
"duplicate `default` attribute",
)));
}
let expr: syn::Expr = syn::parse_quote!(::core::default::Default::default());
default_expr = Some(expr);
continue; // Skip next default check
}
// Check for default value: #[sea_orm(default = "expr")]
if let Some(expr_str) = meta.get_as_kv("default") {
// Error on duplicate `default`
if default_expr.is_some() {
return Err(Error::Syn(syn::Error::new_spanned(
meta,
"duplicate `default` attribute",
)));
}
// Parse the expression string into a syn::Expr
let expr = syn::parse_str::<syn::Expr>(&expr_str).map_err(Error::Syn)?;
default_expr = Some(expr);
}
}
}
}
// Finnaly match and return appropriate field type
if let Some(expr) = default_expr {
Ok(Some(IntoActiveModelField::WithDefault { ident, expr }))
} else {
Ok(Some(IntoActiveModelField::Normal(ident)))
}
}
/// Method to derive the ActiveModel from the [ActiveModelTrait](sea_orm::ActiveModelTrait)
pub fn expand_into_active_model(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match DeriveIntoActiveModel::new(input) {
Ok(model) => model.expand(),
Err(Error::InputNotStruct) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive IntoActiveModel on structs");
}),
Err(Error::Syn(err)) => Err(err),
}
}
fn is_qualified_type(ty: &syn::Type) -> bool {
matches!(ty, syn::Type::Path(syn::TypePath { qself: Some(_), .. }))
}
@@ -0,0 +1,32 @@
use proc_macro2::TokenStream;
use quote::quote;
struct DeriveMigrationName {
ident: syn::Ident,
}
impl DeriveMigrationName {
fn new(input: syn::DeriveInput) -> Self {
let ident = input.ident;
DeriveMigrationName { ident }
}
fn expand(&self) -> TokenStream {
let ident = &self.ident;
quote!(
#[automatically_derived]
impl sea_orm_migration::MigrationName for #ident {
fn name(&self) -> &str {
sea_orm_migration::util::get_file_stem(file!())
}
}
)
}
}
/// Method to derive a MigrationName
pub fn expand_derive_migration_name(input: syn::DeriveInput) -> syn::Result<TokenStream> {
Ok(DeriveMigrationName::new(input).expand())
}
+50
View File
@@ -0,0 +1,50 @@
mod active_enum;
mod active_enum_display;
mod active_model;
mod active_model_behavior;
mod active_model_ex;
mod arrow_schema;
mod attributes;
mod case_style;
mod column;
mod derive_iden;
mod entity;
mod entity_loader;
mod entity_model;
mod from_query_result;
mod into_active_model;
mod migration;
mod model;
mod model_ex;
mod partial_model;
mod primary_key;
mod related_entity;
mod relation;
mod try_getable_from_json;
mod typed_column;
mod util;
mod value_type;
mod value_type_match;
pub use active_enum::*;
pub use active_enum_display::*;
pub use active_model::*;
pub use active_model_behavior::*;
pub use active_model_ex::*;
pub use arrow_schema::*;
pub use column::*;
pub use derive_iden::*;
pub use entity::*;
pub use entity_model::*;
pub use from_query_result::*;
pub use into_active_model::*;
pub use migration::*;
pub use model::*;
pub use model_ex::*;
pub use partial_model::*;
pub use primary_key::*;
pub use related_entity::*;
pub use relation::*;
pub use try_getable_from_json::*;
pub use typed_column::*;
pub use value_type::*;
+324
View File
@@ -0,0 +1,324 @@
use super::{
attributes::derive_attr,
util::{escape_rust_keyword, field_not_ignored, trim_starting_raw_identifier},
};
use heck::ToUpperCamelCase;
use itertools::izip;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use std::iter::FromIterator;
use syn::{Attribute, Data, Expr, Ident, LitStr, Type};
pub(crate) struct DeriveModel {
column_idents: Vec<Ident>,
entity_ident: Ident,
field_idents: Vec<Ident>,
field_types: Vec<syn::Type>,
ident: Ident,
ignore_attrs: Vec<bool>,
}
impl DeriveModel {
pub fn new(ident: &Ident, data: &Data, attrs: &[Attribute]) -> syn::Result<Self> {
let fields = match data {
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { named, .. }),
..
}) => named,
_ => {
return Err(syn::Error::new_spanned(
ident,
"You can only derive DeriveModel on structs",
));
}
};
let sea_attr = derive_attr::SeaOrm::try_from_attributes(attrs)?.unwrap_or_default();
let entity_ident = sea_attr.entity.unwrap_or_else(|| format_ident!("Entity"));
let field_idents = fields
.iter()
.map(|field| field.ident.as_ref().unwrap().clone())
.collect();
let field_types = fields.iter().map(|field| field.ty.clone()).collect();
let column_idents = fields
.iter()
.map(|field| {
let ident = field.ident.as_ref().unwrap().to_string();
let ident = trim_starting_raw_identifier(ident).to_upper_camel_case();
let ident = escape_rust_keyword(ident);
let mut ident = format_ident!("{}", &ident);
field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("sea_orm"))
.try_for_each(|attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("enum_name") {
ident = syn::parse_str(&meta.value()?.parse::<LitStr>()?.value())
.unwrap();
} else {
// Reads the value expression to advance the parse stream.
// Some parameters, such as `primary_key`, do not have any value,
// so ignoring an error occurred here.
let _: Option<Expr> = meta.value().and_then(|v| v.parse()).ok();
}
Ok(())
})
})?;
Ok(ident)
})
.collect::<Result<_, syn::Error>>()?;
let ignore_attrs = fields
.iter()
.map(|field| !field_not_ignored(field))
.collect();
Ok(DeriveModel {
column_idents,
entity_ident,
field_idents,
field_types,
ident: ident.clone(),
ignore_attrs,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
let expanded_impl_from_query_result = self.impl_from_query_result();
let expanded_impl_model_trait = self.impl_model_trait();
Ok(TokenStream::from_iter([
expanded_impl_from_query_result,
expanded_impl_model_trait,
]))
}
fn impl_from_query_result(&self) -> TokenStream {
let ident = &self.ident;
let field_idents = &self.field_idents;
let column_idents = &self.column_idents;
let field_types = &self.field_types;
let ignore_attrs = &self.ignore_attrs;
let (field_readers, field_values): (Vec<TokenStream>, Vec<TokenStream>) = izip!(
field_idents.iter(),
column_idents,
field_types,
ignore_attrs,
)
.map(|(field_ident, column_ident, field_type, &ignore)| {
if ignore {
let reader = quote! {
let #field_ident: Option<()> = None;
};
let unwrapper = quote! {
#field_ident: Default::default()
};
(reader, unwrapper)
} else {
let reader = quote! {
let #field_ident =
row.try_get_nullable::<Option<#field_type>>(
pre,
sea_orm::IdenStatic::as_str(
&<<Self as sea_orm::ModelTrait>::Entity
as sea_orm::entity::EntityTrait>::Column::#column_ident
).into()
)?;
};
let unwrapper = quote! {
#field_ident: #field_ident.ok_or_else(|| sea_orm::DbErr::Type(
format!(
"Missing value for column '{}'",
sea_orm::IdenStatic::as_str(
&<<Self as sea_orm::ModelTrait>::Entity
as sea_orm::entity::EntityTrait>::Column::#column_ident
)
)
))?
};
(reader, unwrapper)
}
})
.unzip();
// When a nested model is loaded via LEFT JOIN, all its fields may be NULL.
// In that case we interpret it as "no nested row" (i.e., Option::None).
// This check detects that condition by testing if all non-ignored fields are NULL.
let all_null_check = {
let checks: Vec<_> = izip!(field_idents, field_types, ignore_attrs)
.filter_map(|(field_ident, field_type, &ignore)| {
if ignore {
None
} else {
Some(create_is_null_expr(field_ident, field_type))
}
})
.collect();
quote! { true #( && #checks )* }
};
quote!(
#[automatically_derived]
impl sea_orm::FromQueryResult for #ident {
fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result<Self, sea_orm::DbErr> {
Self::from_query_result_nullable(row, pre).map_err(Into::into)
}
fn from_query_result_nullable(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result<Self, sea_orm::TryGetError> {
#(#field_readers)*
if #all_null_check {
return Err(sea_orm::TryGetError::Null("All fields of nested model are null".into()));
}
Ok(Self {
#(#field_values),*
})
}
}
)
}
pub fn impl_model_trait<'a>(&'a self) -> TokenStream {
let ident = &self.ident;
let entity_ident = &self.entity_ident;
let ignore_attrs = &self.ignore_attrs;
let ignore = |(ident, ignore): (&'a Ident, &bool)| -> Option<&'a Ident> {
if *ignore { None } else { Some(ident) }
};
let field_idents: Vec<&Ident> = self
.field_idents
.iter()
.zip(ignore_attrs)
.filter_map(ignore)
.collect();
let column_idents: Vec<&Ident> = self
.column_idents
.iter()
.zip(ignore_attrs)
.filter_map(ignore)
.collect();
let get_field_type: Vec<TokenStream> = self
.field_types
.iter()
.zip(ignore_attrs)
.filter_map(|(ty, ignore)| {
if *ignore {
None
} else {
Some(quote!(<#ty as sea_orm::sea_query::ValueType>::array_type()))
}
})
.collect();
let missing_field_msg = format!("field does not exist on {ident}");
quote!(
#[automatically_derived]
impl sea_orm::ModelTrait for #ident {
type Entity = #entity_ident;
fn get(&self, c: <Self::Entity as sea_orm::entity::EntityTrait>::Column) -> sea_orm::Value {
match c {
#(<Self::Entity as sea_orm::entity::EntityTrait>::Column::#column_idents => self.#field_idents.clone().into(),)*
}
}
fn get_value_type(c: <Self::Entity as EntityTrait>::Column) -> sea_orm::sea_query::ArrayType {
match c {
#(<Self::Entity as sea_orm::entity::EntityTrait>::Column::#column_idents => #get_field_type,)*
}
}
fn try_set(&mut self, c: <Self::Entity as sea_orm::EntityTrait>::Column, v: sea_orm::Value) -> Result<(), sea_orm::DbErr> {
match c {
#(<Self::Entity as sea_orm::EntityTrait>::Column::#column_idents => self.#field_idents = sea_orm::sea_query::ValueType::try_from(v).map_err(|e| sea_orm::DbErr::Type(e.to_string()))?,)*
_ => return Err(sea_orm::DbErr::Type(#missing_field_msg.to_owned())),
}
Ok(())
}
}
)
}
}
pub fn expand_derive_model(
ident: &Ident,
data: &Data,
attrs: &[Attribute],
) -> syn::Result<TokenStream> {
DeriveModel::new(ident, data, attrs)?.expand()
}
/// Get the total nesting depth of `Option`.
///
/// For example:
/// - `Option<T>` => `1`
/// - `Option<Option<T>>` => `2`
/// - `Option<Option<Option<T>>>` => `3`
fn option_nesting_depth(ty: &Type) -> usize {
match ty {
Type::Path(type_path) if type_path.qself.is_none() => type_path
.path
.segments
.last()
.and_then(|segment| {
if segment.ident != "Option" {
return None;
}
match &segment.arguments {
syn::PathArguments::AngleBracketed(args) if args.args.len() == 1 => {
args.args.first().map(|arg| match arg {
syn::GenericArgument::Type(inner) => 1 + option_nesting_depth(inner),
_ => 1,
})
}
_ => Some(1),
}
})
.unwrap_or(0),
_ => 0,
}
}
/// Generate an expr that checks whether an optional field is nullish.
///
/// For a nested `Option`, the generated expression treats every partially
/// unwrapped `None` as null.
///
/// For example, for `Option<Option<Option<T>>>`, it will generate:
/// ```rust,ignore
/// matches!(
/// field,
/// None | Some(None) | Some(Some(None)) | Some(Some(Some(None)))
/// )
/// ```
fn create_is_null_expr(field_ident: &Ident, field_type: &Type) -> TokenStream {
let depth = option_nesting_depth(field_type);
if depth == 0 {
return quote! { #field_ident.is_none() };
}
let patterns: Vec<_> = (0..=depth)
.map(|depth| {
let mut pattern = quote! { None };
for _ in 0..depth {
pattern = quote! { Some(#pattern) };
}
pattern
})
.collect();
quote! { matches!(#field_ident, #( #patterns )|* ) }
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,602 @@
use std::collections::{HashMap, hash_map::Entry};
use heck::ToUpperCamelCase;
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, format_ident, quote, quote_spanned};
use syn::{
Expr, Meta, Type, ext::IdentExt, punctuated::Punctuated, spanned::Spanned, token::Comma,
};
use super::from_query_result::{
DeriveFromQueryResult, FromQueryResultItem, ItemType as FqrItemType,
};
use super::into_active_model::{DeriveIntoActiveModel, IntoActiveModelField};
use super::util::GetMeta;
#[derive(Debug)]
enum Error {
InputNotStruct,
EntityNotSpecified,
NotSupportGeneric(Span),
OverlappingAttributes(Span),
Syn(syn::Error),
}
#[derive(Debug, PartialEq, Eq)]
enum ColumnAs {
/// alias from a column in model
Col {
col: Option<syn::Ident>,
field: syn::Ident,
},
/// from an expr
Expr {
expr: syn::Expr,
field: syn::Ident,
},
/// nesting another struct
Nested {
typ: Type,
field: syn::Ident,
alias: Option<String>,
prefix: Option<String>,
},
Skip(syn::Ident),
}
struct DerivePartialModel {
entity: Option<syn::Type>,
active_model: Option<syn::Type>,
model_alias: Option<String>,
ident: syn::Ident,
fields: Vec<ColumnAs>,
from_query_result: bool,
into_active_model: bool,
}
impl DerivePartialModel {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
if !input.generics.params.is_empty() {
return Err(Error::NotSupportGeneric(input.generics.params.span()));
}
let fields = match input.data {
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { named, .. }),
..
}) => named,
_ => return Err(Error::InputNotStruct),
};
let mut entity = None;
let mut entity_string = String::new();
let mut active_model = None;
let mut model_alias = None;
let mut from_query_result = true;
let mut into_active_model = false;
for attr in input.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated) {
for meta in list {
if let Some(s) = meta.get_as_kv("entity") {
entity = Some(syn::parse_str::<syn::Type>(&s).map_err(Error::Syn)?);
entity_string = s;
} else if let Some(s) = meta.get_as_kv("alias") {
model_alias = Some(s);
} else if let Some(s) = meta.get_as_kv("from_query_result") {
if s == "false" {
from_query_result = false;
}
} else if meta.exists("into_active_model") {
into_active_model = true;
}
}
}
}
if into_active_model {
active_model = Some(
syn::parse_str::<syn::Type>(&format!(
"<{entity_string} as EntityTrait>::ActiveModel"
))
.map_err(Error::Syn)?,
);
}
let mut column_as_list = Vec::with_capacity(fields.len());
let mut seen_nested: HashMap<(syn::Type, Option<String>), TokenStream> = HashMap::new();
for field in fields {
let field_span = field.span();
let mut from_col = None;
let mut from_expr = None;
let mut nested = false;
let mut nested_alias = None;
let mut nested_prefix = None;
let mut skip = false;
for attr in field.attrs.iter() {
if !attr.path().is_ident("sea_orm") {
continue;
}
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
{
for meta in list.iter() {
if meta.exists("skip") {
skip = true;
} else if meta.exists("nested") {
nested = true;
} else if let Some(list) = meta.get_list_args("nested") {
nested = true;
for m in list.iter() {
match m.get_as_kv("prefix") {
Some(p) => nested_prefix = (!p.is_empty()).then_some(p),
None => {
return Err(Error::Syn(syn::Error::new_spanned(
m,
"invalid nested attribute, expected `prefix = \"...\"`",
)));
}
}
}
} else if let Some(s) = meta.get_as_kv("from_col") {
from_col = Some(format_ident!("{}", s.to_upper_camel_case()));
} else if let Some(s) = meta.get_as_kv("from_expr") {
from_expr = Some(syn::parse_str::<Expr>(&s).map_err(Error::Syn)?);
} else if let Some(s) = meta.get_as_kv("alias") {
nested_alias = Some(s);
}
}
}
}
let field_tokens = field.to_token_stream();
let field_name = field.ident.unwrap();
let col_as = match (from_col, from_expr, nested) {
(Some(col), None, false) => {
if entity.is_none() {
return Err(Error::EntityNotSpecified);
}
ColumnAs::Col {
col: Some(col),
field: field_name,
}
}
(None, Some(expr), false) => ColumnAs::Expr {
expr,
field: field_name,
},
(None, None, true) => {
if let Some(prefix) = &nested_prefix {
let key = (field.ty.clone(), Some(prefix.clone()));
match seen_nested.entry(key) {
Entry::Occupied(e) => {
let msg = format!(
"multiple nested fields with the same type share prefix \"{prefix}\""
);
let mut err = syn::Error::new_spanned(&field_tokens, msg);
err.combine(syn::Error::new_spanned(e.get(), "first defined here"));
return Err(Error::Syn(err));
}
Entry::Vacant(e) => {
e.insert(field_tokens);
}
}
}
ColumnAs::Nested {
typ: field.ty,
field: field_name,
alias: nested_alias,
prefix: nested_prefix,
}
}
(None, None, false) => {
if entity.is_none() {
return Err(Error::EntityNotSpecified);
}
if skip {
ColumnAs::Skip(field_name)
} else {
ColumnAs::Col {
col: None,
field: field_name,
}
}
}
(_, _, _) => return Err(Error::OverlappingAttributes(field_span)),
};
column_as_list.push(col_as);
}
Ok(Self {
entity,
active_model,
model_alias,
ident: input.ident,
fields: column_as_list,
from_query_result,
into_active_model,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
let impl_partial_model = self.impl_partial_model();
let impl_from_query_result = if self.from_query_result {
DeriveFromQueryResult {
ident: self.ident.clone(),
generics: Default::default(),
fields: self
.fields
.iter()
.map(|col_as| FromQueryResultItem {
typ: match col_as {
ColumnAs::Nested { prefix, .. } => FqrItemType::Nested {
prefix: prefix.clone(),
},
ColumnAs::Skip(_) => FqrItemType::Skip,
_ => FqrItemType::Flat,
},
ident: match col_as {
ColumnAs::Col { field, .. } => field,
ColumnAs::Expr { field, .. } => field,
ColumnAs::Nested { field, .. } => field,
ColumnAs::Skip(field) => field,
}
.to_owned(),
alias: None,
})
.collect(),
}
.impl_from_query_result(true)
} else {
quote!()
};
let impl_into_active_model = if self.into_active_model {
DeriveIntoActiveModel {
ident: self.ident.clone(),
active_model: self.active_model.clone(),
fields: self
.fields
.iter()
.filter_map(|col_as| {
match col_as {
ColumnAs::Col { field, .. } => Some(field),
ColumnAs::Expr { field, .. } => Some(field),
ColumnAs::Nested { .. } => None,
ColumnAs::Skip(_) => None,
}
.map(|f| IntoActiveModelField::Normal(f.clone()))
})
.collect(),
set_fields: Vec::new(),
exhaustive: false,
}
.impl_into_active_model()
} else {
quote!()
};
Ok(quote! {
#impl_partial_model
#impl_from_query_result
#impl_into_active_model
})
}
fn impl_partial_model(&self) -> TokenStream {
let select_ident = format_ident!("select");
let DerivePartialModel {
entity,
model_alias,
ident,
fields,
..
} = self;
let select_col_code_gen = fields.iter().map(|col_as| match col_as {
ColumnAs::Col { col, field } => {
let field = field.unraw().to_string();
let entity = entity.as_ref().unwrap();
let variant_name = if let Some(col) = col {
col
} else {
&format_ident!("{}", field.to_upper_camel_case())
};
// variant of the entity column
let column = quote! {
<#entity as sea_orm::EntityTrait>::Column::#variant_name
};
// We cast enum as text in select_as if the backend is postgres
let non_nested = match model_alias {
Some(model_alias) => quote! {
let col_expr = sea_orm::sea_query::Expr::col((#model_alias, #column));
let casted = sea_orm::ColumnTrait::select_as(&#column, col_expr);
sea_orm::QuerySelect::column_as(#select_ident, casted, col_alias)
},
None => quote! {
sea_orm::QuerySelect::column_as(#select_ident, #column, col_alias)
},
};
quote! {
let #select_ident = {
let col_alias = pre.map_or(#field.to_string(), |pre| format!("{pre}{}", #field));
if let Some(nested_alias) = nested_alias {
let alias = sea_orm::sea_query::SeaRc::new(nested_alias);
let col_expr = sea_orm::sea_query::Expr::col(
(alias, #column)
);
let casted = sea_orm::ColumnTrait::select_as(&#column, col_expr);
sea_orm::QuerySelect::column_as(#select_ident, casted, col_alias)
} else {
#non_nested
}
};
}
}
ColumnAs::Expr { expr, field } => {
let field = field.unraw().to_string();
quote!(let #select_ident =
if let Some(prefix) = pre {
let ident = format!("{prefix}{}", #field);
sea_orm::QuerySelect::column_as(#select_ident, #expr, ident)
} else {
sea_orm::QuerySelect::column_as(#select_ident, #expr, #field)
};
)
}
ColumnAs::Nested {
typ,
field,
alias,
prefix,
} => {
let field_str = field.unraw().to_string();
let alias_ref: Option<&str> = alias.as_deref();
let alias_arg = match alias_ref {
Some(s) => quote! { Some(#s) },
None => quote! { None },
};
let prefix_expr = match prefix {
Some(p) => quote! {
Some(&if let Some(prefix) = pre {
format!("{prefix}{}", #p)
} else {
#p.to_string()
})
},
None => quote! {
Some(&if let Some(prefix) = pre {
format!("{prefix}{}_", #field_str)
} else {
format!("{}_", #field_str)
})
},
};
quote!(let #select_ident =
<#typ as sea_orm::PartialModelTrait>::select_cols_nested(
#select_ident, #prefix_expr, #alias_arg
);
)
}
ColumnAs::Skip(_) => quote!(),
});
quote! {
#[automatically_derived]
impl sea_orm::PartialModelTrait for #ident {
fn select_cols_nested<S: sea_orm::QuerySelect>(#select_ident: S, pre: Option<&str>, nested_alias: Option<&'static str>) -> S {
#(#select_col_code_gen)*
#select_ident
}
}
}
}
}
pub fn expand_derive_partial_model(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match DerivePartialModel::new(input) {
Ok(partial_model) => partial_model.expand(),
Err(Error::NotSupportGeneric(span)) => Ok(quote_spanned! {
span => compile_error!("you can only derive `DerivePartialModel` on concrete struct");
}),
Err(Error::OverlappingAttributes(span)) => Ok(quote_spanned! {
span => compile_error!("you can only use one of `from_col`, `from_expr`, `nested`");
}),
Err(Error::EntityNotSpecified) => Ok(quote_spanned! {
ident_span => compile_error!("you need specific which entity you are using")
}),
Err(Error::InputNotStruct) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive `DerivePartialModel` on named struct");
}),
Err(Error::Syn(err)) => Err(err),
}
}
#[cfg(test)]
mod test {
use quote::format_ident;
use syn::{DeriveInput, Type, parse_str};
use crate::derives::partial_model::ColumnAs;
use super::DerivePartialModel;
type StdResult<T> = Result<T, Box<dyn std::error::Error>>;
const CODE_SNIPPET_1: &str = r#"
#[sea_orm(entity = "Entity")]
struct PartialModel {
default_field: i32,
#[sea_orm(from_col = "bar")]
alias_field: i32,
#[sea_orm(from_expr = "Expr::val(1).add(1)")]
expr_field : i32
}
"#;
#[test]
fn test_load_macro_input_1() -> StdResult<()> {
let input = parse_str::<DeriveInput>(CODE_SNIPPET_1)?;
let middle = DerivePartialModel::new(input).unwrap();
assert_eq!(middle.entity, Some(parse_str::<Type>("Entity").unwrap()));
assert_eq!(middle.ident, format_ident!("PartialModel"));
assert_eq!(middle.fields.len(), 3);
assert_eq!(
middle.fields[0],
ColumnAs::Col {
col: None,
field: format_ident!("default_field")
}
);
assert_eq!(
middle.fields[1],
ColumnAs::Col {
col: Some(format_ident!("Bar")),
field: format_ident!("alias_field"),
},
);
assert_eq!(
middle.fields[2],
ColumnAs::Expr {
expr: syn::parse_str("Expr::val(1).add(1)").unwrap(),
field: format_ident!("expr_field"),
}
);
assert_eq!(middle.from_query_result, true);
Ok(())
}
const CODE_SNIPPET_2: &str = r#"
#[sea_orm(entity = "MyEntity", from_query_result = "false")]
struct PartialModel {
default_field: i32,
}
"#;
#[test]
fn test_load_macro_input_2() -> StdResult<()> {
let input = parse_str::<DeriveInput>(CODE_SNIPPET_2)?;
let middle = DerivePartialModel::new(input).unwrap();
assert_eq!(middle.entity, Some(parse_str::<Type>("MyEntity").unwrap()));
assert_eq!(middle.ident, format_ident!("PartialModel"));
assert_eq!(middle.fields.len(), 1);
assert_eq!(
middle.fields[0],
ColumnAs::Col {
col: None,
field: format_ident!("default_field")
}
);
assert_eq!(middle.from_query_result, false);
Ok(())
}
const CODE_SNIPPET_3: &str = r#"
struct PartialModel {
#[sea_orm(nested(prefix = "mgr_"))]
manager: Person,
#[sea_orm(nested(prefix = "csh_"))]
cashier: Person,
}
"#;
#[test]
fn test_load_macro_input_3() -> StdResult<()> {
let input = parse_str::<DeriveInput>(CODE_SNIPPET_3)?;
let middle = DerivePartialModel::new(input).unwrap();
assert_eq!(middle.fields.len(), 2);
assert_eq!(
middle.fields[0],
ColumnAs::Nested {
typ: parse_str("Person").unwrap(),
field: format_ident!("manager"),
alias: None,
prefix: Some("mgr_".to_string()),
}
);
assert_eq!(
middle.fields[1],
ColumnAs::Nested {
typ: parse_str("Person").unwrap(),
field: format_ident!("cashier"),
alias: None,
prefix: Some("csh_".to_string()),
}
);
assert_eq!(middle.from_query_result, true);
Ok(())
}
const CODE_SNIPPET_4: &str = r#"
struct PartialModel {
#[sea_orm(nested(prefix = "x_"))]
manager: Person,
#[sea_orm(nested(prefix = "x_"))]
cashier: Person,
}
"#;
#[test]
fn test_duplicate_prefix_error() {
let input: DeriveInput = parse_str(CODE_SNIPPET_4).unwrap();
assert!(DerivePartialModel::new(input).is_err());
}
const CODE_SNIPPET_5: &str = r#"
struct PartialModel {
#[sea_orm(nested)]
manager: Person,
#[sea_orm(nested)]
cashier: Person,
}
"#;
#[test]
fn test_duplicate_nested_without_prefix_is_accepted() -> StdResult<()> {
let input = parse_str::<DeriveInput>(CODE_SNIPPET_5)?;
let middle = DerivePartialModel::new(input).unwrap();
assert_eq!(middle.fields.len(), 2);
assert_eq!(
middle.fields[0],
ColumnAs::Nested {
typ: parse_str("Person").unwrap(),
field: format_ident!("manager"),
alias: None,
prefix: None,
}
);
assert_eq!(
middle.fields[1],
ColumnAs::Nested {
typ: parse_str("Person").unwrap(),
field: format_ident!("cashier"),
alias: None,
prefix: None,
}
);
Ok(())
}
}
@@ -0,0 +1,62 @@
use super::impl_iden;
use proc_macro2::{Ident, TokenStream};
use quote::{quote, quote_spanned};
use syn::{Data, DataEnum, Fields, Variant};
fn impl_primary_key_to_column(ident: &Ident, data: &Data) -> syn::Result<TokenStream> {
let variants = match data {
syn::Data::Enum(DataEnum { variants, .. }) => variants,
_ => {
return Ok(quote_spanned! {
ident.span() => compile_error!("you can only derive DerivePrimaryKey on enums");
});
}
};
if variants.is_empty() {
return Ok(quote_spanned! {
ident.span() => compile_error!("Entity must have a primary key column. See <https://github.com/SeaQL/sea-orm/issues/485> for details.");
});
}
let variant: Vec<TokenStream> = variants
.iter()
.map(|Variant { ident, fields, .. }| match fields {
Fields::Named(_) => quote! { #ident{..} },
Fields::Unnamed(_) => quote! { #ident(..) },
Fields::Unit => quote! { #ident },
})
.collect();
Ok(quote!(
#[automatically_derived]
impl sea_orm::PrimaryKeyToColumn for #ident {
type Column = Column;
fn into_column(self) -> Self::Column {
match self {
#(Self::#variant => Self::Column::#variant,)*
}
}
fn from_column(col: Self::Column) -> Option<Self> {
match col {
#(Self::Column::#variant => Some(Self::#variant),)*
_ => None,
}
}
}
))
}
/// Method to derive a Primary Key for a Model using the [PrimaryKeyTrait](sea_orm::PrimaryKeyTrait)
pub fn expand_derive_primary_key(ident: &Ident, data: &Data) -> syn::Result<TokenStream> {
let impl_primary_key_to_column = impl_primary_key_to_column(ident, data)?;
let impl_iden = impl_iden(ident, data)?;
Ok(quote!(
#impl_primary_key_to_column
#impl_iden
))
}
@@ -0,0 +1,171 @@
#[cfg(feature = "seaography")]
mod private {
use heck::ToLowerCamelCase;
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use crate::derives::attributes::related_attr;
enum Error {
InputNotEnum,
InvalidEntityPath,
Syn(syn::Error),
}
struct DeriveRelatedEntity {
entity_ident: TokenStream,
ident: syn::Ident,
variants: syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
}
impl DeriveRelatedEntity {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let sea_attr = related_attr::SeaOrm::try_from_attributes(&input.attrs)
.map_err(Error::Syn)?
.unwrap_or_default();
let ident = input.ident;
let entity_ident = match sea_attr.entity.as_ref().map(Self::parse_lit_string) {
Some(entity_ident) => entity_ident.map_err(|_| Error::InvalidEntityPath)?,
None => quote! { Entity },
};
let variants = match input.data {
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => return Err(Error::InputNotEnum),
};
Ok(DeriveRelatedEntity {
entity_ident,
ident,
variants,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
let ident = &self.ident;
let entity_ident = &self.entity_ident;
let mut get_relation_impl = Vec::new();
let mut get_relation_name_impl = Vec::new();
let mut get_related_entity_filter_impl = Vec::new();
for variant in &self.variants {
let attr = related_attr::SeaOrm::from_attributes(&variant.attrs)?;
let enum_name = &variant.ident;
let target_entity = attr
.entity
.as_ref()
.map(Self::parse_lit_string)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'entity'")
})??;
let def = match attr.def {
Some(def) => Some(Self::parse_lit_string(&def).map_err(|_| {
syn::Error::new_spanned(variant, "Missing value for 'def'")
})?),
None => None,
};
let name = enum_name.to_string().to_lower_camel_case();
get_relation_impl.push(if let Some(def) = &def {
quote! { Self::#enum_name => builder.get_relation::<#entity_ident, #target_entity>(#name, #def) }
} else {
quote! { Self::#enum_name => via_builder.get_relation::<#entity_ident, #target_entity>(#name) }
});
get_relation_name_impl.push(if let Some(def) = &def {
quote! { Self::#enum_name => builder.get_relation_name::<#entity_ident, #target_entity>(#name, #def) }
} else {
quote! { Self::#enum_name => via_builder.get_relation_name::<#entity_ident, #target_entity>(#name) }
});
get_related_entity_filter_impl.push(if let Some(def) = &def {
quote! { Self::#enum_name => builder.get_relation::<#entity_ident, #target_entity>(#name, #def) }
} else {
quote! { Self::#enum_name => builder.get_relation_via::<#entity_ident, #target_entity>(#name) }
});
}
// Get the path of the `async-graphql` on the application's Cargo.toml
let async_graphql_crate = match crate_name("async-graphql") {
// if found, use application's `async-graphql`
Ok(FoundCrate::Name(name)) => {
let ident = Ident::new(&name, Span::call_site());
quote! { #ident }
}
Ok(FoundCrate::Itself) => quote! { async_graphql },
// if not, then use the `async-graphql` re-exported by `seaography`
Err(_) => quote! { seaography::async_graphql },
};
Ok(quote! {
impl seaography::RelationBuilder for #ident {
fn get_relation(&self, context: & 'static seaography::BuilderContext) -> #async_graphql_crate::dynamic::Field {
let builder = seaography::EntityObjectRelationBuilder { context };
let via_builder = seaography::EntityObjectViaRelationBuilder { context };
match self {
#(#get_relation_impl,)*
_ => panic!("No relations for this entity"),
}
}
fn get_relation_name(&self, context: & 'static seaography::BuilderContext) -> String {
let builder = seaography::EntityObjectRelationBuilder { context };
let via_builder = seaography::EntityObjectViaRelationBuilder { context };
match self {
#(#get_relation_name_impl,)*
_ => panic!("No relations for this entity"),
}
}
fn get_related_entity_filter(&self, context: & 'static seaography::BuilderContext) -> seaography::RelatedEntityFilterField {
let builder = seaography::RelatedEntityFilterBuilder { context };
match self {
#(#get_related_entity_filter_impl,)*
_ => panic!("No relations for this entity"),
}
}
}
})
}
fn parse_lit_string(lit: &syn::Lit) -> syn::Result<TokenStream> {
match lit {
syn::Lit::Str(lit_str) => lit_str
.value()
.parse()
.map_err(|_| syn::Error::new_spanned(lit, "attribute not valid")),
_ => Err(syn::Error::new_spanned(lit, "attribute must be a string")),
}
}
}
/// Method to derive a Related enumeration
pub fn expand_derive_related_entity(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match DeriveRelatedEntity::new(input) {
Ok(model) => model.expand(),
Err(Error::InputNotEnum) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive DeriveRelation on enums");
}),
Err(Error::InvalidEntityPath) => Ok(quote_spanned! {
ident_span => compile_error!("invalid attribute value for 'entity'");
}),
Err(Error::Syn(err)) => Err(err),
}
}
}
#[cfg(not(feature = "seaography"))]
mod private {
use proc_macro2::TokenStream;
pub fn expand_derive_related_entity(_: syn::DeriveInput) -> syn::Result<TokenStream> {
Ok(TokenStream::new())
}
}
pub use private::*;
@@ -0,0 +1,248 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use super::attributes::{derive_attr, relation_attr};
enum Error {
InputNotEnum,
Syn(syn::Error),
}
struct DeriveRelation {
entity_ident: syn::Ident,
ident: syn::Ident,
variants: syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
}
impl DeriveRelation {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let variants = match input.data {
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => return Err(Error::InputNotEnum),
};
let sea_attr = derive_attr::SeaOrm::try_from_attributes(&input.attrs)
.map_err(Error::Syn)?
.unwrap_or_default();
let ident = input.ident;
let entity_ident = sea_attr.entity.unwrap_or_else(|| format_ident!("Entity"));
Ok(DeriveRelation {
entity_ident,
ident,
variants,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
let expanded_impl_relation_trait = self.impl_relation_trait()?;
Ok(expanded_impl_relation_trait)
}
fn impl_relation_trait(&self) -> syn::Result<TokenStream> {
let ident = &self.ident;
let entity_ident = &self.entity_ident;
let no_relation_def_msg = format!("No RelationDef for {ident}");
fn lit_str(lit: &syn::Lit) -> syn::Result<String> {
match lit {
syn::Lit::Str(lit_str) => Ok(lit_str.value()),
_ => Err(syn::Error::new_spanned(lit, "attribute must be a string")),
}
}
fn parse_lit_str(lit: &syn::Lit) -> syn::Result<TokenStream> {
lit_str(lit)?
.parse()
.map_err(|_| syn::Error::new_spanned(lit, "attribute not valid"))
}
let variant_relation_defs: Vec<TokenStream> = self
.variants
.iter()
.map(|variant| {
let variant_ident = &variant.ident;
let attr = relation_attr::SeaOrm::from_attributes(&variant.attrs)?;
let mut relation_type = quote! { error };
let related_to = if attr.belongs_to.is_some() {
relation_type = quote! { belongs_to };
attr.belongs_to
.as_ref()
.map(parse_lit_str)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'belongs_to'")
})
} else if attr.has_one.is_some() {
relation_type = quote! { has_one };
attr.has_one
.as_ref()
.map(parse_lit_str)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'has_one'")
})
} else if attr.has_many.is_some() {
relation_type = quote! { has_many };
attr.has_many
.as_ref()
.map(parse_lit_str)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'has_many'")
})
} else {
Err(syn::Error::new_spanned(
variant,
"Missing one of 'has_one', 'has_many' or 'belongs_to'",
))
}??;
let mut result = if let (Some(has_many), Some(via)) = (&attr.has_many, &attr.via_rel) {
let has_many = lit_str(has_many)?;
let via: TokenStream = if has_many == "Entity" {
lit_str(via)?
} else {
format!("{}::{}", has_many.trim_end_matches("::Entity"), lit_str(via)?)
}.parse().unwrap();
quote!(
Self::#variant_ident => #entity_ident::has_many_via(#related_to, #via)
)
} else {
quote!(
Self::#variant_ident => #entity_ident::#relation_type(#related_to)
)
};
if attr.from.is_some() {
let from =
attr.from
.as_ref()
.map(parse_lit_str)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'from'")
})??;
result = quote! { #result.from(#from) };
} else if attr.belongs_to.is_some() {
return Err(syn::Error::new_spanned(variant, "Missing attribute 'from'"));
}
if attr.to.is_some() {
let to = attr
.to
.as_ref()
.map(parse_lit_str)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'to'")
})??;
result = quote! { #result.to(#to) };
} else if attr.belongs_to.is_some() {
return Err(syn::Error::new_spanned(variant, "Missing attribute 'to'"));
}
if attr.on_update.is_some() {
let on_update = attr
.on_update
.as_ref()
.map(parse_lit_str)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'on_update'")
})??;
result = quote! { #result.on_update(sea_orm::prelude::ForeignKeyAction::#on_update) };
}
if attr.on_delete.is_some() {
let on_delete = attr
.on_delete
.as_ref()
.map(parse_lit_str)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'on_delete'")
})??;
result = quote! { #result.on_delete(sea_orm::prelude::ForeignKeyAction::#on_delete) };
}
if attr.on_condition.is_some() {
let on_condition = attr
.on_condition
.as_ref()
.map(parse_lit_str)
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'on_condition'")
})??;
result = quote! { #result.on_condition(|_, _| sea_orm::sea_query::IntoCondition::into_condition(#on_condition)) };
}
if attr.fk_name.is_some() {
let fk_name = attr
.fk_name
.as_ref()
.map(|lit| {
match lit {
syn::Lit::Str(lit_str) => Ok(lit_str.value()),
_ => Err(syn::Error::new_spanned(lit, "attribute must be a string")),
}
})
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'fk_name'")
})??;
result = quote! { #result.fk_name(#fk_name) };
}
if attr.skip_fk.is_some() {
result = quote! { #result.skip_fk() };
}
if attr.condition_type.is_some() {
let condition_type = attr
.condition_type
.as_ref()
.map(|lit| {
match lit {
syn::Lit::Str(lit_str) => {
match lit_str.value().to_ascii_lowercase().as_str() {
"all" => Ok(quote!( sea_orm::sea_query::ConditionType::All )),
"any" => Ok(quote!( sea_orm::sea_query::ConditionType::Any )),
_ => Err(syn::Error::new_spanned(lit, "Condition type must be one of `all` or `any`")),
}
},
_ => Err(syn::Error::new_spanned(lit, "attribute must be a string")),
}
})
.ok_or_else(|| {
syn::Error::new_spanned(variant, "Missing value for 'condition_type'")
})??;
result = quote! { #result.condition_type(#condition_type) };
}
result = quote! { #result.into() };
Result::<_, syn::Error>::Ok(result)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(quote!(
#[automatically_derived]
impl sea_orm::entity::RelationTrait for #ident {
fn def(&self) -> sea_orm::entity::RelationDef {
match self {
#( #variant_relation_defs, )*
_ => panic!(#no_relation_def_msg)
}
}
}
))
}
}
/// Method to derive a Relation
pub fn expand_derive_relation(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match DeriveRelation::new(input) {
Ok(model) => model.expand(),
Err(Error::InputNotEnum) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive DeriveRelation on enums");
}),
Err(Error::Syn(err)) => Err(err),
}
}
@@ -0,0 +1,70 @@
use proc_macro2::{Ident, TokenStream};
use quote::quote;
pub fn expand_derive_from_json_query_result(ident: Ident) -> syn::Result<TokenStream> {
let impl_not_u8 = if cfg!(feature = "postgres-array") {
quote!(
#[automatically_derived]
impl sea_orm::sea_query::postgres_array::NotU8 for #ident {}
)
} else {
quote!()
};
Ok(quote!(
#[automatically_derived]
impl sea_orm::TryGetableFromJson for #ident {}
#[automatically_derived]
impl std::convert::From<#ident> for sea_orm::Value {
fn from(source: #ident) -> Self {
sea_orm::Value::Json(
Some(std::boxed::Box::new(
serde_json::to_value(&source)
.expect(concat!("Failed to serialize '", stringify!(#ident), "'"))
))
)
}
}
#[automatically_derived]
impl sea_orm::sea_query::ValueType for #ident {
fn try_from(v: sea_orm::Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
match v {
sea_orm::Value::Json(Some(json)) => Ok(
serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?,
),
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
fn type_name() -> String {
stringify!(#ident).to_owned()
}
fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::Json
}
fn column_type() -> sea_orm::sea_query::ColumnType {
sea_orm::sea_query::ColumnType::Json
}
}
#[automatically_derived]
impl sea_orm::sea_query::Nullable for #ident {
fn null() -> sea_orm::Value {
sea_orm::Value::Json(None)
}
}
#[automatically_derived]
impl sea_orm::IntoActiveValue<#ident> for #ident {
fn into_active_value(self) -> sea_orm::ActiveValue<#ident> {
sea_orm::ActiveValue::set(self)
}
}
#impl_not_u8
))
}
@@ -0,0 +1,106 @@
use crate::derives::util::consume_meta;
use super::util::{CompoundType, escape_rust_keyword, trim_starting_raw_identifier};
use heck::ToUpperCamelCase;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{Data, Fields, Lit, Visibility, spanned::Spanned};
/// First is `struct TypedColumn`, second is the `const COLUMN`
pub fn expand_typed_column(
vis: &Visibility,
data: &Data,
) -> syn::Result<(TokenStream, TokenStream)> {
let mut column_fields = Vec::new();
let mut column_types = Vec::new();
let mut column_values = Vec::new();
if let Data::Struct(item_struct) = &data
&& let Fields::Named(fields) = &item_struct.fields
{
for field in &fields.named {
let Some(ident) = &field.ident else { continue };
let field_name = trim_starting_raw_identifier(ident);
let mut field_name = Ident::new(&field_name.to_upper_camel_case(), ident.span());
let field_ty = &field.ty;
let mut ignore = false;
let mut column_type = None;
for attr in &field.attrs {
if !attr.path().is_ident("sea_orm") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("column_type") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
column_type = Some(litstr.value());
} else {
return Err(meta.error(format!("Invalid column_type {lit:?}")));
}
} else if meta.path.is_ident("enum_name") {
let lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
let ty: Ident = syn::parse_str(&litstr.value())?;
field_name = ty;
} else {
return Err(meta.error(format!("Invalid enum_name {lit:?}")));
}
} else if meta.path.is_ident("ignore") {
ignore = true;
} else {
consume_meta(meta);
}
Ok(())
})?;
}
let compound = if let syn::Type::Path(type_path) = field_ty {
CompoundType::matches_type(type_path)
} else {
false
};
if ignore || compound {
continue;
}
field_name = Ident::new(&escape_rust_keyword(field_name), ident.span());
column_fields.push(ident.clone());
let wrapper =
super::value_type_match::column_type_wrapper(&column_type, field_ty, field.span());
column_types.push(if let Some(wrapper) = &wrapper {
quote!(sea_orm::#wrapper<Entity>)
} else {
quote!(Column)
});
column_values.push(if let Some(wrapper) = &wrapper {
quote!(sea_orm::#wrapper(Column::#field_name))
} else {
quote!(Column::#field_name)
});
}
}
Ok((
quote! {
#[doc = " Generated by sea-orm-macros"]
#vis struct TypedColumn {
#(
#[doc = " Generated by sea-orm-macros"]
pub #column_fields: #column_types
),*
}
},
quote! {
#[doc = " Generated by sea-orm-macros"]
#vis const COLUMN: TypedColumn = TypedColumn {
#(#column_fields: #column_values),*
};
},
))
}
+541
View File
@@ -0,0 +1,541 @@
use heck::ToUpperCamelCase;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::{
Field, GenericArgument, LitStr, Meta, MetaNameValue, PathArguments, Type, TypePath,
meta::ParseNestedMeta, punctuated::Punctuated, token::Comma,
};
pub(crate) fn async_token() -> TokenStream {
if cfg!(feature = "async") {
quote!(async)
} else {
quote!()
}
}
pub(crate) fn await_token() -> TokenStream {
if cfg!(feature = "async") {
quote!(.await)
} else {
quote!()
}
}
pub(crate) struct RelationColumns {
pub(crate) columns: Vec<Ident>,
pub(crate) span: Span,
}
impl RelationColumns {
/// Parse relation columns in a `from` or `to` attribute.
/// For example:
/// `cake_id` or `Column::CakeId` -> `CakeId`;
/// `(user_id, post_id)` -> `UserId`, `PostId`.
pub(crate) fn from_lit(lit: LitStr) -> syn::Result<Self> {
let paths = if lit.value().starts_with('(') {
lit.parse_with(|input: syn::parse::ParseStream<'_>| {
let content;
syn::parenthesized!(content in input);
content.parse_terminated(syn::Path::parse_mod_style, Comma)
})?
} else {
let mut paths = Punctuated::new();
paths.push(lit.parse()?);
paths
};
if paths.is_empty() {
return Err(syn::Error::new(lit.span(), "expected at least one column"));
}
let columns = paths
.into_iter()
.map(|path| {
let Some(segment) = path.segments.last() else {
return Err(syn::Error::new_spanned(path, "expected column path"));
};
Ok(Ident::new(
&escape_rust_keyword(segment.ident.to_string().to_upper_camel_case()),
segment.ident.span(),
))
})
.collect::<syn::Result<Vec<_>>>()?;
Ok(Self {
columns,
span: lit.span(),
})
}
}
#[derive(Clone)]
pub(crate) struct Junction {
pub(crate) module: Ident,
pub(crate) relation: Option<Ident>,
}
impl Junction {
/// Parse the junction module and optional relation variant in a `via` attribute.
/// For example: `post_tag` -> module `post_tag`;
/// `cakes_bakers::Baker` -> module `cakes_bakers`, relation `Baker`.
pub(crate) fn from_lit(lit: &LitStr) -> syn::Result<Self> {
let path = lit.parse::<syn::Path>()?;
if path.leading_colon.is_some()
|| !(1..=2).contains(&path.segments.len())
|| path
.segments
.iter()
.any(|segment| !matches!(segment.arguments, PathArguments::None))
{
return Err(syn::Error::new(
lit.span(),
"`via` must be `junction` or `junction::Relation`",
));
}
let mut segments = path.segments.into_iter();
let module = segments.next().expect("validated junction path").ident;
let relation = segments.next().map(|segment| segment.ident);
Ok(Self { module, relation })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CardinalityKind {
Required,
Optional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CompoundKind {
BelongsTo(CardinalityKind),
HasOne,
HasMany,
}
#[derive(Clone)]
pub(crate) struct CompoundType {
pub(crate) kind: CompoundKind,
pub(crate) entity: TypePath,
}
impl CompoundType {
/// Returns whether the field uses the compound relation wrapper syntax.
pub(crate) fn matches_type(type_path: &TypePath) -> bool {
last_path_segment(type_path).is_ok_and(|segment| {
matches!(
segment.ident.to_string().as_str(),
"BelongsTo" | "HasOne" | "HasMany"
)
})
}
/// Parses `BelongsTo<Entity>`, `BelongsTo<Option<Entity>>`, `HasOne<Entity>`, and `HasMany<Entity>`.
pub(crate) fn from_type(type_path: &TypePath) -> syn::Result<Option<Self>> {
let segment = last_path_segment(type_path)?;
match segment.ident.to_string().as_str() {
"BelongsTo" => {
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return Err(syn::Error::new_spanned(
type_path,
"BelongsTo requires an Entity or Option<Entity> generic argument",
));
};
let mut args = args.args.iter();
let Some(GenericArgument::Type(ty)) = args.next() else {
return Err(syn::Error::new_spanned(
type_path,
"BelongsTo generic argument must be an Entity or Option<Entity>",
));
};
if args.next().is_some() {
return Err(syn::Error::new_spanned(
type_path,
"BelongsTo requires an Entity or Option<Entity> generic argument",
));
}
let Type::Path(ty_path) = ty else {
return Err(syn::Error::new_spanned(
ty,
"BelongsTo generic argument must be an Entity or Option<Entity>",
));
};
let target_segment = last_path_segment(ty_path)?;
match (
target_segment.ident.to_string().as_str(),
&target_segment.arguments,
) {
("Entity", _) => Ok(Some(Self {
kind: CompoundKind::BelongsTo(CardinalityKind::Required),
entity: ty_path.clone(),
})),
("Option", PathArguments::AngleBracketed(args)) => {
let Some(entity) = entity_generic_arg(&args.args) else {
return Err(syn::Error::new_spanned(
ty,
"BelongsTo optional target must be Option<Entity>",
));
};
Ok(Some(Self {
kind: CompoundKind::BelongsTo(CardinalityKind::Optional),
entity,
}))
}
_ => Err(syn::Error::new_spanned(
ty,
"BelongsTo generic argument must be an Entity or Option<Entity>",
)),
}
}
"HasOne" => {
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return Err(syn::Error::new_spanned(
type_path,
"HasOne requires an Entity generic argument",
));
};
let Some(entity) = entity_generic_arg(&args.args) else {
return Err(syn::Error::new_spanned(
type_path,
"HasOne requires an Entity generic argument",
));
};
Ok(Some(Self {
kind: CompoundKind::HasOne,
entity,
}))
}
"HasMany" => {
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return Err(syn::Error::new_spanned(
type_path,
"HasMany requires an Entity generic argument",
));
};
let Some(entity) = entity_generic_arg(&args.args) else {
return Err(syn::Error::new_spanned(
type_path,
"HasMany requires an Entity generic argument",
));
};
Ok(Some(Self {
kind: CompoundKind::HasMany,
entity,
}))
}
_ => Ok(None),
}
}
}
fn last_path_segment(type_path: &TypePath) -> syn::Result<&syn::PathSegment> {
type_path
.path
.segments
.last()
.ok_or_else(|| syn::Error::new_spanned(type_path, "expected path type"))
}
pub(crate) fn is_self_entity(entity: &TypePath) -> bool {
entity.path.segments.len() == 1
&& last_path_segment(entity).is_ok_and(|segment| segment.ident == "Entity")
}
pub(crate) fn consume_meta(meta: ParseNestedMeta<'_>) {
let _ = meta.value().and_then(|v| v.parse::<syn::Expr>());
}
/// Remove ignored fields and compound fields
pub(crate) fn field_not_ignored(field: &Field) -> bool {
if let Type::Path(type_path) = &field.ty
&& CompoundType::matches_type(type_path)
{
return false;
}
!field_ignored(field)
}
fn field_ignored(field: &Field) -> bool {
let mut ignored = false;
for attr in &field.attrs {
if !attr.path().is_ident("sea_orm") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("ignore") {
ignored = true;
} else {
consume_meta(meta);
}
Ok(())
});
}
ignored
}
fn entity_generic_arg(args: &Punctuated<GenericArgument, Comma>) -> Option<TypePath> {
if args.len() != 1 {
return None;
}
match args.first() {
Some(GenericArgument::Type(Type::Path(type_path))) if is_entity_type(type_path) => {
Some(type_path.clone())
}
_ => None,
}
}
fn is_entity_type(type_path: &TypePath) -> bool {
last_path_segment(type_path).is_ok_and(|segment| segment.ident == "Entity")
}
pub(crate) fn trim_starting_raw_identifier<T>(string: T) -> String
where
T: ToString,
{
string
.to_string()
.trim_start_matches(RAW_IDENTIFIER)
.to_string()
}
pub(crate) fn escape_rust_keyword<T>(string: T) -> String
where
T: ToString,
{
let string = string.to_string();
if RUST_KEYWORDS.iter().any(|s| s.eq(&string)) {
format!("r#{string}")
} else if RUST_SPECIAL_KEYWORDS.iter().any(|s| s.eq(&string)) {
format!("{string}_")
} else {
string
}
}
/// Turn a string to PascalCase while escaping all special characters in ASCII words.
///
/// (camel_case is used here to match naming of heck.)
///
/// In ActiveEnum, string_value will be PascalCased and made
/// an identifier in {Enum}Variant.
///
/// However Rust only allows for XID_Start char followed by
/// XID_Continue characters as identifiers; this causes a few
/// problems:
///
/// - `string_value = ""` will cause a panic;
/// - `string_value` containing only non-alphanumerics will become `""`
/// and cause the above panic;
/// - `string_values`:
/// - `"A B"`
/// - `"A B"`
/// - `"A_B"`
/// - `"A_ B"`
///
/// All shares the same identifier of `"AB"`;
///
/// This function does the PascelCase conversion with a few special escapes:
/// - Non-Unicode Standard Annex #31 compliant characters will converted to their hex notation;
/// - `"_"` into `"0x5F"`;
/// - `" "` into `"0x20"`;
/// - Empty strings will become special keyword of `"__Empty"`
///
/// Note that this does NOT address:
///
/// - case-sensitivity. String value "ABC" and "abc" remains
/// conflicted after .camel_case().
///
/// Example Conversions:
///
/// ```ignore
/// assert_eq!(camel_case_with_escaped_non_uax31(""), "__Empty");
/// assert_eq!(camel_case_with_escaped_non_uax31(" "), "_0x20");
/// assert_eq!(camel_case_with_escaped_non_uax31(" "), "_0x200x20");
/// assert_eq!(camel_case_with_escaped_non_uax31("_"), "_0x5F");
/// assert_eq!(camel_case_with_escaped_non_uax31("foobar"), "Foobar");
/// assert_eq!(camel_case_with_escaped_non_uax31("foo bar"), "Foo0x20bar");
/// ```
pub(crate) fn camel_case_with_escaped_non_uax31<T>(string: T) -> String
where
T: ToString,
{
let additional_chars_to_replace: [char; 2] = ['_', ' '];
let mut rebuilt = string
.to_string()
.chars()
.enumerate()
.map(|(pos, char_)| {
if !additional_chars_to_replace.contains(&char_)
&& match pos {
0 => unicode_ident::is_xid_start(char_),
_ => unicode_ident::is_xid_continue(char_),
}
{
char_.to_string()
} else {
format!("{:#X}", char_ as u32)
}
})
.reduce(
// Join the "characters" (now strings)
// back together
|lhs, rhs| lhs + rhs.as_str(),
)
.map_or(
// if string_value is ""
// Make sure the default does NOT go through camel_case,
// as the __ will be removed! The underscores are
// what guarantees this being special case avoiding
// all potential conflicts.
String::from("__Empty"),
|s| s.to_upper_camel_case(),
);
if rebuilt
.chars()
.next()
.map(char::is_numeric)
.unwrap_or(false)
{
rebuilt = String::from("_") + &rebuilt;
}
rebuilt
}
pub(crate) const RAW_IDENTIFIER: &str = "r#";
pub(crate) const RUST_KEYWORDS: [&str; 49] = [
"as", "async", "await", "break", "const", "continue", "dyn", "else", "enum", "extern", "false",
"fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
"return", "static", "struct", "super", "trait", "true", "type", "union", "unsafe", "use",
"where", "while", "abstract", "become", "box", "do", "final", "macro", "override", "priv",
"try", "typeof", "unsized", "virtual", "yield",
];
pub(crate) const RUST_SPECIAL_KEYWORDS: [&str; 3] = ["crate", "Self", "self"];
pub(crate) trait GetMeta {
fn exists(&self, k: &str) -> bool;
fn get_as_kv(&self, k: &str) -> Option<String>;
fn get_as_kv_with_ident(&self) -> Option<(Ident, String)>;
fn get_list_args(&self, name: &str) -> Option<Punctuated<Meta, Comma>>;
}
impl GetMeta for Meta {
fn exists(&self, key: &str) -> bool {
let Meta::Path(path) = self else {
return false;
};
path.is_ident(key)
}
fn get_as_kv(&self, key: &str) -> Option<String> {
let Meta::NameValue(MetaNameValue {
path,
value: syn::Expr::Lit(exprlit),
..
}) = self
else {
return None;
};
let syn::Lit::Str(litstr) = &exprlit.lit else {
return None;
};
if path.is_ident(key) {
Some(litstr.value())
} else {
None
}
}
fn get_as_kv_with_ident(&self) -> Option<(Ident, String)> {
let Meta::NameValue(MetaNameValue {
path,
value: syn::Expr::Lit(exprlit),
..
}) = self
else {
return None;
};
let syn::Lit::Str(litstr) = &exprlit.lit else {
return None;
};
path.get_ident()
.map(|ident| (ident.clone(), litstr.value()))
}
fn get_list_args(&self, name: &str) -> Option<Punctuated<Meta, Comma>> {
match self {
Meta::List(list) if list.path.is_ident(name) => list
.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
.ok(),
_ => None,
}
}
}
pub(crate) fn combine_error(acc: &mut Option<syn::Error>, error: syn::Error) {
if let Some(acc) = acc {
acc.combine(error);
} else {
*acc = Some(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_non_uax31_escape() {
// Test empty string
assert_eq!(camel_case_with_escaped_non_uax31(""), "__Empty");
// Test additional_chars_to_replace (to_camel_case related characters)
assert_eq!(camel_case_with_escaped_non_uax31(" "), "_0x20");
// Test additional_chars_to_replace (multiples. ensure distinct from single)
assert_eq!(camel_case_with_escaped_non_uax31(" "), "_0x200x20");
// Test additional_chars_to_replace (udnerscores)
assert_eq!(camel_case_with_escaped_non_uax31("_"), "_0x5F");
// Test typical use case
assert_eq!(camel_case_with_escaped_non_uax31("foobar"), "Foobar");
// Test spaced words distinct from non-spaced
assert_eq!(camel_case_with_escaped_non_uax31("foo bar"), "Foo0x20bar");
// Test underscored words distinct from non-spaced and spaced
assert_eq!(camel_case_with_escaped_non_uax31("foo_bar"), "Foo0x5Fbar");
// Test leading numeric characters
assert_eq!(camel_case_with_escaped_non_uax31("1"), "_0x31");
// Test escaping also works on full string following lead numeric character
// This was previously a fail condition.
assert_eq!(
camel_case_with_escaped_non_uax31("1 2 3"),
"_0x310x2020x203"
);
assert_eq!(camel_case_with_escaped_non_uax31("씨오알엠"), "씨오알엠");
assert_eq!(camel_case_with_escaped_non_uax31("A_B"), "A0x5Fb");
assert_eq!(camel_case_with_escaped_non_uax31("AB"), "Ab");
}
}
@@ -0,0 +1,376 @@
use super::attributes::value_type_attr;
use super::value_type_match::{array_type_expr, can_try_from_u64, column_type_expr};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Field, Ident, Type, punctuated::Punctuated, spanned::Spanned, token::Comma};
#[allow(clippy::large_enum_variant)]
enum DeriveValueType {
TupleStruct(DeriveValueTypeStruct),
StringLike(DeriveValueTypeString),
}
struct DeriveValueTypeStruct {
name: syn::Ident,
ty: Type,
column_type: TokenStream,
array_type: TokenStream,
can_try_from_u64: bool,
/// Opt-in `#[sea_orm(try_getable_array)]`: also implement `TryGetableArray`.
try_getable_array: bool,
}
#[derive(Default)]
struct DeriveValueTypeStructAttrs {
column_type: Option<TokenStream>,
array_type: Option<TokenStream>,
try_from_u64: bool,
try_getable_array: bool,
}
impl TryFrom<value_type_attr::SeaOrm> for DeriveValueTypeStructAttrs {
type Error = syn::Error;
fn try_from(attrs: value_type_attr::SeaOrm) -> syn::Result<Self> {
Ok(Self {
column_type: attrs.column_type.map(|s| s.parse()).transpose()?,
array_type: attrs.array_type.map(|s| s.parse()).transpose()?,
try_from_u64: attrs.try_from_u64.is_some(),
try_getable_array: attrs.try_getable_array.is_some(),
})
}
}
struct DeriveValueTypeString {
name: syn::Ident,
from_str: Option<TokenStream>,
to_str: Option<TokenStream>,
column_type: Option<TokenStream>,
}
struct DeriveValueTypeStringAttrs {
from_str: Option<TokenStream>,
to_str: Option<TokenStream>,
column_type: Option<TokenStream>,
}
impl TryFrom<value_type_attr::SeaOrm> for DeriveValueTypeStringAttrs {
type Error = syn::Error;
fn try_from(attrs: value_type_attr::SeaOrm) -> syn::Result<Self> {
let value_type = attrs.value_type.map(|s| s.value());
assert_eq!(value_type.as_deref(), Some("String"));
Ok(Self {
from_str: attrs.from_str.map(|s| s.parse()).transpose()?,
to_str: attrs.to_str.map(|s| s.parse()).transpose()?,
column_type: attrs.column_type.map(|s| s.parse()).transpose()?,
})
}
}
impl DeriveValueType {
fn new(input: syn::DeriveInput) -> syn::Result<Self> {
// Produce an error if the macro attributes are malformed
let value_type_attr = value_type_attr::SeaOrm::try_from_attributes(&input.attrs)?;
// If some attributes were set, inspect the optional `value_type`
let value_type = if let Some(ref value_type_attr) = value_type_attr {
value_type_attr.value_type.as_ref().map(|s| s.value())
} else {
None
};
// If either `value_type` is unset, or no attributes were passed, assume
// `DeriveValueTypeStruct`. If no attrs were set, use default values.
if value_type.is_none() || value_type_attr.is_none() {
let value_type_attr = if let Some(value_type_attr) = value_type_attr {
value_type_attr.try_into()?
} else {
DeriveValueTypeStructAttrs::default()
};
match input.data {
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed, .. }),
..
}) => {
return DeriveValueTypeStruct::new(input.ident, value_type_attr, unnamed)
.map(Self::TupleStruct);
}
_ => {
return Err(syn::Error::new_spanned(
input,
"You can only derive `DeriveValueType` on a struct with a single unnamed field, unless `value_type` is set.",
));
}
}
}
let value_type_attr = value_type_attr.unwrap();
let value_type = value_type.unwrap();
match value_type.as_str() {
"String" => DeriveValueTypeString::new(input.ident, value_type_attr.try_into()?)
.map(Self::StringLike),
_ => Err(syn::Error::new_spanned(
input.ident,
r#"Please specify value_type = "String""#,
)),
}
}
fn expand(&self) -> syn::Result<TokenStream> {
Ok(match self {
Self::TupleStruct(s) => s.impl_value_type(),
Self::StringLike(s) => s.impl_value_type(),
})
}
}
impl DeriveValueTypeStruct {
fn new(
name: Ident,
attrs: DeriveValueTypeStructAttrs,
fields: Punctuated<Field, Comma>,
) -> syn::Result<Self> {
let Some(field) = fields.into_iter().next() else {
return Err(syn::Error::new_spanned(
name,
"You can only derive `DeriveValueType` on tuple struct with 1 inner value",
));
};
let field_span = field.span();
let ty = field.ty;
let field_type: String = quote! { #ty }
.to_string() //E.g.: "Option < String >"
.split_whitespace()
.collect(); // Remove all whitespace
let field_type = if field_type.starts_with("Option<") {
&field_type[7..(field_type.len() - 1)] // Extract `T` out of `Option<T>`
} else {
field_type.as_str()
};
let column_type = column_type_expr(attrs.column_type, field_type, field_span);
let array_type = array_type_expr(attrs.array_type, field_type, field_span);
let can_try_from_u64 = attrs.try_from_u64 || can_try_from_u64(field_type);
Ok(Self {
name,
ty,
column_type,
array_type,
can_try_from_u64,
try_getable_array: attrs.try_getable_array,
})
}
fn impl_value_type(&self) -> TokenStream {
let name = &self.name;
let field_type = &self.ty;
let column_type = &self.column_type;
let array_type = &self.array_type;
let try_from_u64_impl = if self.can_try_from_u64 {
quote!(
#[automatically_derived]
impl sea_orm::TryFromU64 for #name {
fn try_from_u64(n: u64) -> Result<Self, sea_orm::DbErr> {
use std::convert::TryInto;
Ok(Self(n.try_into().map_err(|e| sea_orm::DbErr::TryIntoErr {
from: stringify!(u64),
into: stringify!(#name),
source: std::sync::Arc::new(e),
})?))
}
}
)
} else {
quote!()
};
let impl_not_u8 = if cfg!(feature = "postgres-array") {
quote!(
#[automatically_derived]
impl sea_orm::sea_query::postgres_array::NotU8 for #name {}
)
} else {
quote!()
};
let impl_try_getable_array = if cfg!(feature = "postgres-array") && self.try_getable_array {
quote!(
#[automatically_derived]
impl sea_orm::TryGetableArray for #name {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, index: I)
-> std::result::Result<Vec<Self>, sea_orm::TryGetError> {
Ok(<Vec<#field_type> as sea_orm::TryGetable>::try_get_by(res, index)?
.into_iter()
.map(#name)
.collect())
}
}
)
} else {
quote!()
};
quote!(
#[automatically_derived]
impl std::convert::From<#name> for sea_orm::Value {
fn from(source: #name) -> Self {
source.0.into()
}
}
#[automatically_derived]
impl sea_orm::TryGetable for #name {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, idx: I)
-> std::result::Result<Self, sea_orm::TryGetError> {
<#field_type as sea_orm::TryGetable>::try_get_by(res, idx).map(|v| #name(v))
}
}
#[automatically_derived]
impl sea_orm::sea_query::ValueType for #name {
fn try_from(v: sea_orm::Value) -> std::result::Result<Self, sea_orm::sea_query::ValueTypeErr> {
<#field_type as sea_orm::sea_query::ValueType>::try_from(v).map(|v| #name(v))
}
fn type_name() -> std::string::String {
stringify!(#name).to_owned()
}
fn array_type() -> sea_orm::sea_query::ArrayType {
#array_type
}
fn column_type() -> sea_orm::sea_query::ColumnType {
#column_type
}
}
#[automatically_derived]
impl sea_orm::sea_query::Nullable for #name {
fn null() -> sea_orm::Value {
<#field_type as sea_orm::sea_query::Nullable>::null()
}
}
#[automatically_derived]
impl sea_orm::IntoActiveValue<#name> for #name {
fn into_active_value(self) -> sea_orm::ActiveValue<#name> {
sea_orm::ActiveValue::Set(self)
}
}
#try_from_u64_impl
#impl_not_u8
#impl_try_getable_array
)
}
}
impl DeriveValueTypeString {
fn new(name: Ident, attrs: DeriveValueTypeStringAttrs) -> syn::Result<Self> {
Ok(Self {
name,
from_str: attrs.from_str,
to_str: attrs.to_str,
column_type: attrs.column_type,
})
}
fn impl_value_type(&self) -> TokenStream {
let name = &self.name;
let from_str = match &self.from_str {
Some(from_str) => from_str,
None => &quote!(std::str::FromStr::from_str),
};
let to_str = match &self.to_str {
Some(to_str) => to_str,
None => &quote!(std::string::ToString::to_string),
};
let column_type = match &self.column_type {
Some(column_type) => column_type,
None => &quote!(String(sea_orm::sea_query::StringLen::None)),
};
let impl_not_u8 = if cfg!(feature = "postgres-array") {
quote!(
#[automatically_derived]
impl sea_orm::sea_query::postgres_array::NotU8 for #name {}
)
} else {
quote!()
};
quote!(
#[automatically_derived]
impl std::convert::From<#name> for sea_orm::Value {
fn from(source: #name) -> Self {
#to_str(&source).into()
}
}
#[automatically_derived]
impl sea_orm::TryGetable for #name {
fn try_get_by<I: sea_orm::ColIdx>(res: &sea_orm::QueryResult, idx: I)
-> std::result::Result<Self, sea_orm::TryGetError> {
let string = String::try_get_by(res, idx)?;
#from_str(&string).map_err(|err| {
sea_orm::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr {
from: "String",
into: stringify!(#name),
source: std::sync::Arc::new(err),
})
})
}
}
#[automatically_derived]
impl sea_orm::sea_query::ValueType for #name {
fn try_from(v: sea_orm::Value) -> std::result::Result<Self, sea_orm::sea_query::ValueTypeErr> {
let string = <String as sea_orm::sea_query::ValueType>::try_from(v)?;
#from_str(&string).map_err(|_| sea_orm::sea_query::ValueTypeErr)
}
fn type_name() -> std::string::String {
stringify!(#name).to_owned()
}
fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::String
}
fn column_type() -> sea_orm::sea_query::ColumnType {
sea_orm::sea_query::ColumnType::#column_type
}
}
#[automatically_derived]
impl sea_orm::sea_query::Nullable for #name {
fn null() -> sea_orm::Value {
sea_orm::Value::String(None)
}
}
#[automatically_derived]
impl sea_orm::IntoActiveValue<#name> for #name {
fn into_active_value(self) -> sea_orm::ActiveValue<#name> {
sea_orm::ActiveValue::Set(self)
}
}
#impl_not_u8
)
}
}
pub fn expand_derive_value_type(input: syn::DeriveInput) -> syn::Result<TokenStream> {
DeriveValueType::new(input)?.expand()
}
@@ -0,0 +1,213 @@
use proc_macro2::{Span, TokenStream};
use quote::quote_spanned;
use syn::{GenericArgument, Ident, LitStr, PathArguments, Type, TypePath};
pub fn column_type_expr(
column_type: Option<TokenStream>,
field_type: &str,
field_span: Span,
) -> TokenStream {
match column_type {
Some(column_type) => {
quote_spanned! { field_span => sea_orm::prelude::ColumnType::#column_type }
}
None => {
let ty: Type = LitStr::new(field_type, field_span)
.parse()
.expect("field type error");
quote_spanned! { field_span => <#ty as sea_orm::sea_query::ValueType>::column_type() }
}
}
}
pub fn column_type_wrapper(
column_type: &Option<String>,
field_type: &Type,
field_span: Span,
) -> Option<Ident> {
let (nullable, field_type) = if let Type::Path(type_path) = field_type
&& let Some(inner) = generic_type_arg(type_path, "Option")
{
(true, inner)
} else {
(false, field_type)
};
if let Some(column_type) = column_type {
let column_type = if let Some((prefix, _)) = column_type.split_once('(') {
prefix
} else {
column_type
};
let value_type = match column_type {
"String" | "Text" => {
if nullable {
Some("StringColumnNullable")
} else {
Some("StringColumn")
}
}
"Blob" | "Binary" | "VarBinary" => Some("BytesColumn"),
"TinyInteger" | "SmallInteger" | "Integer" | "BigInteger" | "TinyUnsigned"
| "SmallUnsigned" | "Unsigned" | "BigUnsigned" | "Float" | "Double" | "Decimal"
| "Money" => {
if nullable {
Some("NumericColumnNullable")
} else {
Some("NumericColumn")
}
}
"DateTime" | "Timestamp" | "TimestampWithTimeZone" => Some("DateTimeLikeColumn"),
"Time" => Some("TimeLikeColumn"),
"Date" => Some("DateLikeColumn"),
"Boolean" => Some("BoolColumn"),
"Json" | "JsonBinary" => Some("JsonColumn"),
"Uuid" => Some("UuidColumn"),
"Array" => Some("GenericArrayColumn"),
_ => None,
}
.map(|ty| Ident::new(ty, field_span));
if value_type.is_some() {
return value_type;
}
}
let Type::Path(field_type) = field_type else {
return None;
};
let value_type = if is_type(field_type, "bool") {
Some("BoolColumn")
} else if is_type(field_type, "String") {
if nullable {
Some("StringColumnNullable")
} else {
Some("StringColumn")
}
} else if let Some(inner) = generic_type_arg(field_type, "Vec") {
if let Type::Path(inner) = inner {
if is_type(inner, "u8") {
Some("BytesColumn")
} else if is_numeric_type(inner) {
Some("NumericArrayColumn")
} else {
Some("GenericArrayColumn")
}
} else {
Some("GenericArrayColumn")
}
} else if is_type(field_type, "Uuid") {
Some("UuidColumn")
} else if is_type(field_type, "IpNetwork") {
Some("IpNetworkColumn")
} else if is_type(field_type, "Json") || is_serde_json_value(field_type) {
Some("JsonColumn")
} else if is_type(field_type, "TextUuid") {
Some("TextUuidColumn")
} else if is_numeric_type(field_type) || type_ident_contains(field_type, "UnixTimestamp") {
if nullable {
Some("NumericColumnNullable")
} else {
Some("NumericColumn")
}
} else if type_ident_contains(field_type, "DateTime")
|| type_ident_contains(field_type, "Timestamp")
{
Some("DateTimeLikeColumn")
} else if type_ident_contains(field_type, "Date") {
Some("DateLikeColumn")
} else if type_ident_contains(field_type, "Time") {
Some("TimeLikeColumn")
} else {
None
};
value_type.map(|ty| Ident::new(ty, field_span))
}
fn generic_type_arg<'a>(type_path: &'a TypePath, ident: &str) -> Option<&'a Type> {
let segment = type_path.path.segments.last()?;
if segment.ident != ident {
return None;
}
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return None;
};
let Some(GenericArgument::Type(inner)) = args.args.first() else {
return None;
};
Some(inner)
}
fn is_type(type_path: &TypePath, ident: &str) -> bool {
type_path.path.segments.len() == 1
&& type_path
.path
.segments
.last()
.is_some_and(|segment| segment.ident == ident)
}
fn type_ident_contains(type_path: &TypePath, pattern: &str) -> bool {
type_path
.path
.segments
.last()
.is_some_and(|segment| segment.ident.to_string().contains(pattern))
}
fn is_serde_json_value(type_path: &TypePath) -> bool {
let mut segments = type_path.path.segments.iter();
matches!(
(segments.next(), segments.next(), segments.next()),
(Some(first), Some(second), None)
if first.ident == "serde_json" && second.ident == "Value"
)
}
fn is_numeric_type(type_path: &TypePath) -> bool {
if type_path.path.segments.len() != 1 {
return false;
}
type_path.path.segments.last().is_some_and(|segment| {
segment.ident == "i8"
|| segment.ident == "i16"
|| segment.ident == "i32"
|| segment.ident == "i64"
|| segment.ident == "u8"
|| segment.ident == "u16"
|| segment.ident == "u32"
|| segment.ident == "u64"
|| segment.ident == "f32"
|| segment.ident == "f64"
|| segment.ident == "Decimal"
|| segment.ident == "BigDecimal"
})
}
pub fn array_type_expr(
array_type: Option<TokenStream>,
field_type: &str,
field_span: Span,
) -> TokenStream {
match array_type {
Some(array_type) => {
quote_spanned! { field_span => sea_orm::sea_query::ArrayType::#array_type }
}
None => {
let ty: Type = LitStr::new(field_type, field_span)
.parse()
.expect("field type error");
quote_spanned! { field_span => <#ty as sea_orm::sea_query::ValueType>::array_type() }
}
}
}
pub fn can_try_from_u64(field_type: &str) -> bool {
matches!(
field_type,
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64"
)
}
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
Ident, LitStr, Token,
parse::{Parse, ParseStream},
};
struct CallArgs {
backend: Ident,
_comma: Token![,],
sql_string: LitStr,
}
impl Parse for CallArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(CallArgs {
backend: input.parse()?,
_comma: input.parse()?,
sql_string: input.parse()?,
})
}
}
pub fn expand(input: proc_macro::TokenStream) -> syn::Result<TokenStream> {
let CallArgs {
backend,
sql_string,
..
} = syn::parse(input)?;
let builder = match backend.to_string().as_str() {
"MySql" => quote!(MysqlQueryBuilder),
"Postgres" => quote!(PostgresQueryBuilder),
"Sqlite" => quote!(SqliteQueryBuilder),
backend => panic!("Unsupported backend {backend}"),
};
Ok(quote! {{
use sea_orm::sea_query;
let query = sea_query::raw_query!(#builder, #sql_string);
sea_orm::Statement {
sql: query.sql,
values: Some(query.values),
db_backend: sea_orm::DbBackend::#backend,
}
}})
}
+23
View File
@@ -0,0 +1,23 @@
> The `strum` module is adapted from https://github.com/Peternator7/strum
MIT License
Copyright (c) 2019 Peter Glotfelty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,176 @@
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{Data, DeriveInput, Fields, Ident};
use super::helpers::{HasStrumVariantProperties, HasTypeProperties, non_enum_error};
pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
let name = &ast.ident;
let r#gen = &ast.generics;
let (impl_generics, ty_generics, where_clause) = r#gen.split_for_impl();
let vis = &ast.vis;
let type_properties = ast.get_type_properties()?;
let strum_module_path = type_properties.crate_module_path();
let doc_comment = format!("An iterator over the variants of [{name}]");
if r#gen.lifetimes().count() > 0 {
return Err(syn::Error::new(
Span::call_site(),
"This macro doesn't support enums with lifetimes. \
The resulting enums would be unbounded.",
));
}
let phantom_data = if r#gen.type_params().count() > 0 {
let g = r#gen.type_params().map(|param| &param.ident);
quote! { < ( #(#g),* ) > }
} else {
quote! { < () > }
};
let variants = match &ast.data {
Data::Enum(v) => &v.variants,
_ => return Err(non_enum_error()),
};
let mut arms = Vec::new();
let mut idx = 0usize;
for variant in variants {
if variant.get_variant_properties()?.disabled.is_some() {
continue;
}
let ident = &variant.ident;
let params = match &variant.fields {
Fields::Unit => quote! {},
Fields::Unnamed(fields) => {
let defaults = ::core::iter::repeat_n(
quote!(::core::default::Default::default()),
fields.unnamed.len(),
);
quote! { (#(#defaults),*) }
}
Fields::Named(fields) => {
let fields = fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap());
quote! { {#(#fields: ::core::default::Default::default()),*} }
}
};
arms.push(quote! {#idx => ::core::option::Option::Some(#name::#ident #params)});
idx += 1;
}
let variant_count = arms.len();
arms.push(quote! { _ => ::core::option::Option::None });
let iter_name = syn::parse_str::<Ident>(&format!("{name}Iter")).unwrap();
// Create a string literal "MyEnumIter" to use in the debug impl.
let iter_name_debug_struct =
syn::parse_str::<syn::LitStr>(&format!("\"{iter_name}\"")).unwrap();
Ok(quote! {
#[doc = #doc_comment]
#[allow(
missing_copy_implementations,
)]
#vis struct #iter_name #impl_generics {
idx: usize,
back_idx: usize,
marker: ::core::marker::PhantomData #phantom_data,
}
impl #impl_generics ::core::fmt::Debug for #iter_name #ty_generics #where_clause {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
// We don't know if the variants implement debug themselves so the only thing we
// can really show is how many elements are left.
f.debug_struct(#iter_name_debug_struct)
.field("len", &self.len())
.finish()
}
}
impl #impl_generics #iter_name #ty_generics #where_clause {
fn get(&self, idx: usize) -> ::core::option::Option<#name #ty_generics> {
match idx {
#(#arms),*
}
}
}
impl #impl_generics #strum_module_path::IntoEnumIterator for #name #ty_generics #where_clause {
type Iterator = #iter_name #ty_generics;
fn iter() -> #iter_name #ty_generics {
#iter_name {
idx: 0,
back_idx: 0,
marker: ::core::marker::PhantomData,
}
}
}
impl #impl_generics Iterator for #iter_name #ty_generics #where_clause {
type Item = #name #ty_generics;
fn next(&mut self) -> ::core::option::Option<<Self as Iterator>::Item> {
self.nth(0)
}
fn size_hint(&self) -> (usize, ::core::option::Option<usize>) {
let t = if self.idx + self.back_idx >= #variant_count { 0 } else { #variant_count - self.idx - self.back_idx };
(t, Some(t))
}
fn nth(&mut self, n: usize) -> ::core::option::Option<<Self as Iterator>::Item> {
let idx = self.idx + n + 1;
if idx + self.back_idx > #variant_count {
// We went past the end of the iterator. Freeze idx at #variant_count
// so that it doesn't overflow if the user calls this repeatedly.
// See PR #76 for context.
self.idx = #variant_count;
::core::option::Option::None
} else {
self.idx = idx;
#iter_name::get(self, idx - 1)
}
}
}
impl #impl_generics ExactSizeIterator for #iter_name #ty_generics #where_clause {
fn len(&self) -> usize {
self.size_hint().0
}
}
impl #impl_generics DoubleEndedIterator for #iter_name #ty_generics #where_clause {
fn next_back(&mut self) -> ::core::option::Option<<Self as Iterator>::Item> {
let back_idx = self.back_idx + 1;
if self.idx + back_idx > #variant_count {
// We went past the end of the iterator. Freeze back_idx at #variant_count
// so that it doesn't overflow if the user calls this repeatedly.
// See PR #76 for context.
self.back_idx = #variant_count;
::core::option::Option::None
} else {
self.back_idx = back_idx;
#iter_name::get(self, #variant_count - self.back_idx)
}
}
}
impl #impl_generics ::core::iter::FusedIterator for #iter_name #ty_generics #where_clause { }
impl #impl_generics Clone for #iter_name #ty_generics #where_clause {
fn clone(&self) -> #iter_name #ty_generics {
#iter_name {
idx: self.idx,
back_idx: self.back_idx,
marker: self.marker.clone(),
}
}
}
})
}
@@ -0,0 +1,116 @@
use heck::{
ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase,
};
use std::str::FromStr;
use syn::{
Ident, LitStr,
parse::{Parse, ParseStream},
};
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CaseStyle {
CamelCase,
KebabCase,
MixedCase,
ShoutySnakeCase,
SnakeCase,
TitleCase,
UpperCase,
LowerCase,
ScreamingKebabCase,
PascalCase,
}
const VALID_CASE_STYLES: &[&str] = &[
"camelCase",
"PascalCase",
"kebab-case",
"snake_case",
"SCREAMING_SNAKE_CASE",
"SCREAMING-KEBAB-CASE",
"lowercase",
"UPPERCASE",
"title_case",
"mixed_case",
];
impl Parse for CaseStyle {
fn parse(input: ParseStream) -> syn::Result<Self> {
let text = input.parse::<LitStr>()?;
let val = text.value();
val.as_str().parse().map_err(|_| {
syn::Error::new_spanned(
&text,
format!(
"Unexpected case style for serialize_all: `{val}`. Valid values are: `{VALID_CASE_STYLES:?}`",
),
)
})
}
}
impl FromStr for CaseStyle {
type Err = ();
fn from_str(text: &str) -> Result<Self, ()> {
Ok(match text {
"camel_case" | "PascalCase" => CaseStyle::PascalCase,
"camelCase" => CaseStyle::CamelCase,
"snake_case" | "snek_case" => CaseStyle::SnakeCase,
"kebab_case" | "kebab-case" => CaseStyle::KebabCase,
"SCREAMING-KEBAB-CASE" => CaseStyle::ScreamingKebabCase,
"shouty_snake_case" | "shouty_snek_case" | "SCREAMING_SNAKE_CASE" => {
CaseStyle::ShoutySnakeCase
}
"title_case" => CaseStyle::TitleCase,
"mixed_case" => CaseStyle::MixedCase,
"lowercase" => CaseStyle::LowerCase,
"UPPERCASE" => CaseStyle::UpperCase,
_ => return Err(()),
})
}
}
pub trait CaseStyleHelpers {
fn convert_case(&self, case_style: Option<CaseStyle>) -> String;
}
impl CaseStyleHelpers for Ident {
fn convert_case(&self, case_style: Option<CaseStyle>) -> String {
let ident_string = self.to_string();
if let Some(case_style) = case_style {
match case_style {
CaseStyle::PascalCase => ident_string.to_upper_camel_case(),
CaseStyle::KebabCase => ident_string.to_kebab_case(),
CaseStyle::MixedCase => ident_string.to_lower_camel_case(),
CaseStyle::ShoutySnakeCase => ident_string.to_shouty_snake_case(),
CaseStyle::SnakeCase => ident_string.to_snake_case(),
CaseStyle::TitleCase => ident_string.to_title_case(),
CaseStyle::UpperCase => ident_string.to_uppercase(),
CaseStyle::LowerCase => ident_string.to_lowercase(),
CaseStyle::ScreamingKebabCase => ident_string.to_kebab_case().to_uppercase(),
CaseStyle::CamelCase => {
let camel_case = ident_string.to_upper_camel_case();
let mut pascal = String::with_capacity(camel_case.len());
let mut it = camel_case.chars();
if let Some(ch) = it.next() {
pascal.extend(ch.to_lowercase());
}
pascal.extend(it);
pascal
}
}
} else {
ident_string
}
}
}
#[test]
fn test_convert_case() {
let id = Ident::new("test_me", proc_macro2::Span::call_site());
assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase)));
assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase)));
}
@@ -0,0 +1,275 @@
use proc_macro2::TokenStream;
use syn::{
Attribute, DeriveInput, Expr, ExprLit, Ident, Lit, LitBool, LitStr, Meta, MetaNameValue, Path,
Token, Variant, Visibility, parenthesized,
parse::{Parse, ParseStream},
parse_str, parse2,
punctuated::Punctuated,
};
use super::case_style::CaseStyle;
pub mod kw {
use syn::custom_keyword;
pub use syn::token::Crate;
// enum metadata
custom_keyword!(serialize_all);
custom_keyword!(use_phf);
// enum discriminant metadata
custom_keyword!(derive);
custom_keyword!(name);
custom_keyword!(vis);
// variant metadata
custom_keyword!(message);
custom_keyword!(detailed_message);
custom_keyword!(serialize);
custom_keyword!(to_string);
custom_keyword!(disabled);
custom_keyword!(default);
custom_keyword!(props);
custom_keyword!(ascii_case_insensitive);
}
pub enum EnumMeta {
SerializeAll {
kw: kw::serialize_all,
case_style: CaseStyle,
},
AsciiCaseInsensitive(kw::ascii_case_insensitive),
Crate {
kw: kw::Crate,
crate_module_path: Path,
},
UsePhf(kw::use_phf),
}
impl Parse for EnumMeta {
fn parse(input: ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(kw::serialize_all) {
let kw = input.parse::<kw::serialize_all>()?;
input.parse::<Token![=]>()?;
let case_style = input.parse()?;
Ok(EnumMeta::SerializeAll { kw, case_style })
} else if lookahead.peek(kw::Crate) {
let kw = input.parse::<kw::Crate>()?;
input.parse::<Token![=]>()?;
let path_str: LitStr = input.parse()?;
let path_tokens = parse_str(&path_str.value())?;
let crate_module_path = parse2(path_tokens)?;
Ok(EnumMeta::Crate {
kw,
crate_module_path,
})
} else if lookahead.peek(kw::ascii_case_insensitive) {
Ok(EnumMeta::AsciiCaseInsensitive(input.parse()?))
} else if lookahead.peek(kw::use_phf) {
Ok(EnumMeta::UsePhf(input.parse()?))
} else {
Err(lookahead.error())
}
}
}
pub enum EnumDiscriminantsMeta {
Derive { kw: kw::derive, paths: Vec<Path> },
Name { kw: kw::name, name: Ident },
Vis { kw: kw::vis, vis: Visibility },
Other { path: Path, nested: TokenStream },
}
impl Parse for EnumDiscriminantsMeta {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(kw::derive) {
let kw = input.parse()?;
let content;
parenthesized!(content in input);
let paths = content.parse_terminated(Path::parse, Token![,])?;
Ok(EnumDiscriminantsMeta::Derive {
kw,
paths: paths.into_iter().collect(),
})
} else if input.peek(kw::name) {
let kw = input.parse()?;
let content;
parenthesized!(content in input);
let name = content.parse()?;
Ok(EnumDiscriminantsMeta::Name { kw, name })
} else if input.peek(kw::vis) {
let kw = input.parse()?;
let content;
parenthesized!(content in input);
let vis = content.parse()?;
Ok(EnumDiscriminantsMeta::Vis { kw, vis })
} else {
let path = input.parse()?;
let content;
parenthesized!(content in input);
let nested = content.parse()?;
Ok(EnumDiscriminantsMeta::Other { path, nested })
}
}
}
pub trait DeriveInputExt {
/// Get all the strum metadata associated with an enum.
fn get_metadata(&self) -> syn::Result<Vec<EnumMeta>>;
/// Get all the `strum_discriminants` metadata associated with an enum.
fn get_discriminants_metadata(&self) -> syn::Result<Vec<EnumDiscriminantsMeta>>;
}
impl DeriveInputExt for DeriveInput {
fn get_metadata(&self) -> syn::Result<Vec<EnumMeta>> {
get_metadata_inner("strum", &self.attrs)
}
fn get_discriminants_metadata(&self) -> syn::Result<Vec<EnumDiscriminantsMeta>> {
get_metadata_inner("strum_discriminants", &self.attrs)
}
}
pub enum VariantMeta {
Message {
kw: kw::message,
value: LitStr,
},
DetailedMessage {
kw: kw::detailed_message,
value: LitStr,
},
Serialize {
kw: kw::serialize,
value: LitStr,
},
Documentation {
value: LitStr,
},
ToString {
kw: kw::to_string,
value: LitStr,
},
Disabled(kw::disabled),
Default(kw::default),
AsciiCaseInsensitive {
kw: kw::ascii_case_insensitive,
value: bool,
},
Props {
kw: kw::props,
props: Vec<(LitStr, LitStr)>,
},
}
impl Parse for VariantMeta {
fn parse(input: ParseStream) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(kw::message) {
let kw = input.parse()?;
let _: Token![=] = input.parse()?;
let value = input.parse()?;
Ok(VariantMeta::Message { kw, value })
} else if lookahead.peek(kw::detailed_message) {
let kw = input.parse()?;
let _: Token![=] = input.parse()?;
let value = input.parse()?;
Ok(VariantMeta::DetailedMessage { kw, value })
} else if lookahead.peek(kw::serialize) {
let kw = input.parse()?;
let _: Token![=] = input.parse()?;
let value = input.parse()?;
Ok(VariantMeta::Serialize { kw, value })
} else if lookahead.peek(kw::to_string) {
let kw = input.parse()?;
let _: Token![=] = input.parse()?;
let value = input.parse()?;
Ok(VariantMeta::ToString { kw, value })
} else if lookahead.peek(kw::disabled) {
Ok(VariantMeta::Disabled(input.parse()?))
} else if lookahead.peek(kw::default) {
Ok(VariantMeta::Default(input.parse()?))
} else if lookahead.peek(kw::ascii_case_insensitive) {
let kw = input.parse()?;
let value = if input.peek(Token![=]) {
let _: Token![=] = input.parse()?;
input.parse::<LitBool>()?.value
} else {
true
};
Ok(VariantMeta::AsciiCaseInsensitive { kw, value })
} else if lookahead.peek(kw::props) {
let kw = input.parse()?;
let content;
parenthesized!(content in input);
let props = content.parse_terminated(Prop::parse, Token![,])?;
Ok(VariantMeta::Props {
kw,
props: props
.into_iter()
.map(|Prop(k, v)| (LitStr::new(&k.to_string(), k.span()), v))
.collect(),
})
} else {
Err(lookahead.error())
}
}
}
struct Prop(Ident, LitStr);
impl Parse for Prop {
fn parse(input: ParseStream) -> syn::Result<Self> {
use syn::ext::IdentExt;
let k = Ident::parse_any(input)?;
let _: Token![=] = input.parse()?;
let v = input.parse()?;
Ok(Prop(k, v))
}
}
pub trait VariantExt {
/// Get all the metadata associated with an enum variant.
fn get_metadata(&self) -> syn::Result<Vec<VariantMeta>>;
}
impl VariantExt for Variant {
fn get_metadata(&self) -> syn::Result<Vec<VariantMeta>> {
let result = get_metadata_inner("strum", &self.attrs)?;
self.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.try_fold(result, |mut vec, attr| {
if let Meta::NameValue(MetaNameValue {
value:
Expr::Lit(ExprLit {
lit: Lit::Str(value),
..
}),
..
}) = &attr.meta
{
vec.push(VariantMeta::Documentation {
value: value.clone(),
})
}
Ok(vec)
})
}
}
fn get_metadata_inner<'a, T: Parse>(
ident: &str,
it: impl IntoIterator<Item = &'a Attribute>,
) -> syn::Result<Vec<T>> {
it.into_iter()
.filter(|attr| attr.path().is_ident(ident))
.try_fold(Vec::new(), |mut vec, attr| {
vec.extend(attr.parse_args_with(Punctuated::<T, Token![,]>::parse_terminated)?);
Ok(vec)
})
}
@@ -0,0 +1,21 @@
pub use self::type_props::HasTypeProperties;
pub use self::variant_props::HasStrumVariantProperties;
pub mod case_style;
mod metadata;
pub mod type_props;
pub mod variant_props;
use proc_macro2::Span;
use quote::ToTokens;
pub fn non_enum_error() -> syn::Error {
syn::Error::new(Span::call_site(), "This macro only supports enums.")
}
pub fn occurrence_error<T: ToTokens>(fst: T, snd: T, attr: &str) -> syn::Error {
let mut e =
syn::Error::new_spanned(snd, format!("Found multiple occurrences of strum({attr})"));
e.combine(syn::Error::new_spanned(fst, "first one here"));
e
}
@@ -0,0 +1,116 @@
use proc_macro2::TokenStream;
use quote::quote;
use std::default::Default;
use syn::{DeriveInput, Ident, Path, Visibility, parse_quote};
use super::case_style::CaseStyle;
use super::metadata::{DeriveInputExt, EnumDiscriminantsMeta, EnumMeta};
use super::occurrence_error;
pub trait HasTypeProperties {
fn get_type_properties(&self) -> syn::Result<StrumTypeProperties>;
}
#[derive(Debug, Clone, Default)]
pub struct StrumTypeProperties {
pub case_style: Option<CaseStyle>,
pub ascii_case_insensitive: bool,
pub crate_module_path: Option<Path>,
pub discriminant_derives: Vec<Path>,
pub discriminant_name: Option<Ident>,
pub discriminant_others: Vec<TokenStream>,
pub discriminant_vis: Option<Visibility>,
pub use_phf: bool,
}
impl HasTypeProperties for DeriveInput {
fn get_type_properties(&self) -> syn::Result<StrumTypeProperties> {
let mut output = StrumTypeProperties::default();
let strum_meta = self.get_metadata()?;
let discriminants_meta = self.get_discriminants_metadata()?;
let mut serialize_all_kw = None;
let mut ascii_case_insensitive_kw = None;
let mut use_phf_kw = None;
let mut crate_module_path_kw = None;
for meta in strum_meta {
match meta {
EnumMeta::SerializeAll { case_style, kw } => {
if let Some(fst_kw) = serialize_all_kw {
return Err(occurrence_error(fst_kw, kw, "serialize_all"));
}
serialize_all_kw = Some(kw);
output.case_style = Some(case_style);
}
EnumMeta::AsciiCaseInsensitive(kw) => {
if let Some(fst_kw) = ascii_case_insensitive_kw {
return Err(occurrence_error(fst_kw, kw, "ascii_case_insensitive"));
}
ascii_case_insensitive_kw = Some(kw);
output.ascii_case_insensitive = true;
}
EnumMeta::UsePhf(kw) => {
if let Some(fst_kw) = use_phf_kw {
return Err(occurrence_error(fst_kw, kw, "use_phf"));
}
use_phf_kw = Some(kw);
output.use_phf = true;
}
EnumMeta::Crate {
crate_module_path,
kw,
} => {
if let Some(fst_kw) = crate_module_path_kw {
return Err(occurrence_error(fst_kw, kw, "Crate"));
}
crate_module_path_kw = Some(kw);
output.crate_module_path = Some(crate_module_path);
}
}
}
let mut name_kw = None;
let mut vis_kw = None;
for meta in discriminants_meta {
match meta {
EnumDiscriminantsMeta::Derive { paths, .. } => {
output.discriminant_derives.extend(paths);
}
EnumDiscriminantsMeta::Name { name, kw } => {
if let Some(fst_kw) = name_kw {
return Err(occurrence_error(fst_kw, kw, "name"));
}
name_kw = Some(kw);
output.discriminant_name = Some(name);
}
EnumDiscriminantsMeta::Vis { vis, kw } => {
if let Some(fst_kw) = vis_kw {
return Err(occurrence_error(fst_kw, kw, "vis"));
}
vis_kw = Some(kw);
output.discriminant_vis = Some(vis);
}
EnumDiscriminantsMeta::Other { path, nested } => {
output.discriminant_others.push(quote! { #path(#nested) });
}
}
}
Ok(output)
}
}
impl StrumTypeProperties {
pub fn crate_module_path(&self) -> Path {
self.crate_module_path
.as_ref()
.map_or_else(|| parse_quote!(sea_orm::strum), |path| parse_quote!(#path))
}
}
@@ -0,0 +1,102 @@
use std::default::Default;
use syn::{Ident, LitStr, Variant};
use super::metadata::{VariantExt, VariantMeta, kw};
use super::occurrence_error;
pub trait HasStrumVariantProperties {
fn get_variant_properties(&self) -> syn::Result<StrumVariantProperties>;
}
#[derive(Clone, Eq, PartialEq, Debug, Default)]
pub struct StrumVariantProperties {
pub disabled: Option<kw::disabled>,
pub default: Option<kw::default>,
pub ascii_case_insensitive: Option<bool>,
pub message: Option<LitStr>,
pub detailed_message: Option<LitStr>,
pub documentation: Vec<LitStr>,
pub string_props: Vec<(LitStr, LitStr)>,
serialize: Vec<LitStr>,
to_string: Option<LitStr>,
ident: Option<Ident>,
}
impl HasStrumVariantProperties for Variant {
fn get_variant_properties(&self) -> syn::Result<StrumVariantProperties> {
let mut output = StrumVariantProperties {
ident: Some(self.ident.clone()),
..Default::default()
};
let mut message_kw = None;
let mut detailed_message_kw = None;
let mut to_string_kw = None;
let mut disabled_kw = None;
let mut default_kw = None;
let mut ascii_case_insensitive_kw = None;
for meta in self.get_metadata()? {
match meta {
VariantMeta::Message { value, kw } => {
if let Some(fst_kw) = message_kw {
return Err(occurrence_error(fst_kw, kw, "message"));
}
message_kw = Some(kw);
output.message = Some(value);
}
VariantMeta::DetailedMessage { value, kw } => {
if let Some(fst_kw) = detailed_message_kw {
return Err(occurrence_error(fst_kw, kw, "detailed_message"));
}
detailed_message_kw = Some(kw);
output.detailed_message = Some(value);
}
VariantMeta::Documentation { value } => {
output.documentation.push(value);
}
VariantMeta::Serialize { value, .. } => {
output.serialize.push(value);
}
VariantMeta::ToString { value, kw } => {
if let Some(fst_kw) = to_string_kw {
return Err(occurrence_error(fst_kw, kw, "to_string"));
}
to_string_kw = Some(kw);
output.to_string = Some(value);
}
VariantMeta::Disabled(kw) => {
if let Some(fst_kw) = disabled_kw {
return Err(occurrence_error(fst_kw, kw, "disabled"));
}
disabled_kw = Some(kw);
output.disabled = Some(kw);
}
VariantMeta::Default(kw) => {
if let Some(fst_kw) = default_kw {
return Err(occurrence_error(fst_kw, kw, "default"));
}
default_kw = Some(kw);
output.default = Some(kw);
}
VariantMeta::AsciiCaseInsensitive { kw, value } => {
if let Some(fst_kw) = ascii_case_insensitive_kw {
return Err(occurrence_error(fst_kw, kw, "ascii_case_insensitive"));
}
ascii_case_insensitive_kw = Some(kw);
output.ascii_case_insensitive = Some(value);
}
VariantMeta::Props { props, .. } => {
output.string_props.extend(props);
}
}
}
Ok(output)
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Source code adapted from https://github.com/Peternator7/strum
#![allow(dead_code)]
pub mod enum_iter;
pub mod helpers;
@@ -0,0 +1,231 @@
use sea_orm::sea_query::{ArrayType, Value, ValueType};
use sea_orm::{ActiveEnum, entity::prelude::StringLen};
use sea_orm_macros::{DeriveActiveEnum, EnumIter};
#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)]
#[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "test_enum",
rename_all = "camelCase"
)]
enum TestEnum {
DefaultVariant,
#[sea_orm(rename = "camelCase")]
VariantCamelCase,
#[sea_orm(rename = "kebab-case")]
VariantKebabCase,
#[sea_orm(rename = "mixed_case")]
VariantMixedCase,
#[sea_orm(rename = "SCREAMING_SNAKE_CASE")]
VariantShoutySnakeCase,
#[sea_orm(rename = "snake_case")]
VariantSnakeCase,
#[sea_orm(rename = "title_case")]
VariantTitleCase,
#[sea_orm(rename = "UPPERCASE")]
VariantUpperCase,
#[sea_orm(rename = "lowercase")]
VariantLowerCase,
#[sea_orm(rename = "SCREAMING-KEBAB-CASE")]
VariantScreamingKebabCase,
#[sea_orm(rename = "PascalCase")]
VariantPascalCase,
#[sea_orm(string_value = "CuStOmStRiNgVaLuE")]
CustomStringValue,
}
#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)]
#[sea_orm(db_type = "Enum", enum_name = "test_enum", rename_all = "camelCase")]
enum TestRenameAllWithoutCasesEnum {
HelloWorld,
}
#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)]
#[sea_orm(
rs_type = "Enum",
db_type = "Enum",
enum_name = "test_enum",
rename_all = "camelCase"
)]
enum TestEnumWithEnumValue {
DefaultVariant,
}
#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)]
#[sea_orm(
rs_type = "String",
db_type = "String(StringLen::None)",
rename_all = "snake_case"
)]
pub enum TestEnum2 {
HelloWorld,
#[sea_orm(rename = "camelCase")]
HelloWorldTwo,
}
#[derive(Debug, EnumIter, DeriveActiveEnum, Eq, PartialEq)]
#[sea_orm(
rs_type = "String",
db_type = "String(StringLen::None)",
rename_all = "snake_case"
)]
pub enum TestEnum3 {
HelloWorld,
}
#[test]
fn derive_active_enum_value() {
assert_eq!(TestEnum::DefaultVariant.to_value(), "defaultVariant");
assert_eq!(TestEnum::VariantCamelCase.to_value(), "variantCamelCase");
assert_eq!(TestEnum::VariantKebabCase.to_value(), "variant-kebab-case");
assert_eq!(TestEnum::VariantMixedCase.to_value(), "variantMixedCase");
assert_eq!(
TestEnum::VariantShoutySnakeCase.to_value(),
"VARIANT_SHOUTY_SNAKE_CASE"
);
assert_eq!(TestEnum::VariantSnakeCase.to_value(), "variant_snake_case");
assert_eq!(TestEnum::VariantTitleCase.to_value(), "Variant Title Case");
assert_eq!(TestEnum::VariantUpperCase.to_value(), "VARIANTUPPERCASE");
assert_eq!(TestEnum::VariantLowerCase.to_value(), "variantlowercase");
assert_eq!(
TestEnum::VariantScreamingKebabCase.to_value(),
"VARIANT-SCREAMING-KEBAB-CASE"
);
assert_eq!(TestEnum::VariantPascalCase.to_value(), "VariantPascalCase");
assert_eq!(TestEnum::CustomStringValue.to_value(), "CuStOmStRiNgVaLuE");
}
#[test]
fn derive_active_enum_from_value() {
assert_eq!(
TestEnum::try_from_value(&"defaultVariant".to_string()),
Ok(TestEnum::DefaultVariant)
);
assert_eq!(
TestEnum::try_from_value(&"variantCamelCase".to_string()),
Ok(TestEnum::VariantCamelCase)
);
assert_eq!(
TestEnum::try_from_value(&"variant-kebab-case".to_string()),
Ok(TestEnum::VariantKebabCase)
);
assert_eq!(
TestEnum::try_from_value(&"variantMixedCase".to_string()),
Ok(TestEnum::VariantMixedCase)
);
assert_eq!(
TestEnum::try_from_value(&"VARIANT_SHOUTY_SNAKE_CASE".to_string()),
Ok(TestEnum::VariantShoutySnakeCase),
);
assert_eq!(
TestEnum::try_from_value(&"variant_snake_case".to_string()),
Ok(TestEnum::VariantSnakeCase)
);
assert_eq!(
TestEnum::try_from_value(&"Variant Title Case".to_string()),
Ok(TestEnum::VariantTitleCase)
);
assert_eq!(
TestEnum::try_from_value(&"VARIANTUPPERCASE".to_string()),
Ok(TestEnum::VariantUpperCase)
);
assert_eq!(
TestEnum::try_from_value(&"variantlowercase".to_string()),
Ok(TestEnum::VariantLowerCase)
);
assert_eq!(
TestEnum::try_from_value(&"VARIANT-SCREAMING-KEBAB-CASE".to_string()),
Ok(TestEnum::VariantScreamingKebabCase),
);
assert_eq!(
TestEnum::try_from_value(&"VariantPascalCase".to_string()),
Ok(TestEnum::VariantPascalCase)
);
assert_eq!(
TestEnum::try_from_value(&"CuStOmStRiNgVaLuE".to_string()),
Ok(TestEnum::CustomStringValue)
);
assert_eq!(
<TestEnum as TryFrom<&str>>::try_from("defaultVariant"),
Ok(TestEnum::DefaultVariant)
);
assert!(<TestEnum as TryFrom<&str>>::try_from("other").is_err());
}
#[test]
fn derive_active_enum_value_2() {
assert_eq!(TestEnum2::HelloWorld.to_value(), "hello_world");
assert_eq!(TestEnum2::HelloWorldTwo.to_value(), "helloWorldTwo");
assert_eq!(TestEnum3::HelloWorld.to_value(), "hello_world");
assert_eq!(
TestRenameAllWithoutCasesEnum::HelloWorld
.to_value()
.value
.as_ref(),
"helloWorld"
);
}
#[test]
fn derive_database_enum_value_type() {
assert_eq!(TestEnum::enum_type_name(), Some("test_enum"));
assert_eq!(TestEnum::array_type(), ArrayType::String);
assert_eq!(
Value::from(TestEnum::DefaultVariant),
Value::String(Some(String::from("defaultVariant")))
);
assert_eq!(
<TestEnum as ValueType>::try_from(Value::String(Some(String::from("defaultVariant"))))
.unwrap(),
TestEnum::DefaultVariant
);
}
#[test]
fn derive_database_enum_rs_type_enum() {
let value = TestEnumWithEnumValue::DefaultVariant.to_value();
assert_eq!(value.value.as_ref(), "defaultVariant");
assert_eq!(
<TestEnumWithEnumValue as ActiveEnum>::try_from_value(&value),
Ok(TestEnumWithEnumValue::DefaultVariant)
);
assert_eq!(
<TestEnumWithEnumValue as TryFrom<&str>>::try_from("defaultVariant"),
Ok(TestEnumWithEnumValue::DefaultVariant)
);
assert!(<TestEnumWithEnumValue as TryFrom<&str>>::try_from("other").is_err());
let value: Value = value.into();
assert_eq!(
value,
Value::Enum(sea_orm::sea_query::OptionEnum::Some(Box::new(
sea_orm::sea_query::Enum {
type_name: String::from("test_enum").into(),
value: "defaultVariant".into(),
},
)))
);
}
#[test]
fn derive_database_enum_default_rs_type_enum() {
let value = TestRenameAllWithoutCasesEnum::HelloWorld.to_value();
assert_eq!(value.value.as_ref(), "helloWorld");
let value: Value = value.into();
assert_eq!(
value,
Value::Enum(sea_orm::sea_query::OptionEnum::Some(Box::new(
sea_orm::sea_query::Enum {
type_name: String::from("test_enum").into(),
value: "helloWorld".into(),
},
)))
);
}
#[test]
fn derive_non_database_enum_value_type() {
assert_eq!(TestEnum2::enum_type_name(), None);
assert_eq!(TestEnum2::array_type(), ArrayType::String);
}
@@ -0,0 +1,40 @@
use sea_orm::entity::prelude::*;
use sea_orm_macros::DeriveEntityModel;
mod string_pk {
use super::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "string_pk")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
mod string_pk_set_true {
use super::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "string_pk")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = true)]
pub id: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
#[test]
fn test_auto_increment_default_by_type() {
assert!(!string_pk::PrimaryKey::auto_increment());
assert!(string_pk_set_true::PrimaryKey::auto_increment());
}
@@ -0,0 +1,58 @@
use std::str::FromStr;
use sea_orm::Iden;
use sea_orm::Iterable;
use sea_orm::prelude::*;
use sea_orm_macros::DeriveEntityModel;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user", rename_all = "camelCase")]
pub struct Model {
#[sea_orm(primary_key)]
id: i32,
username: String,
first_name: String,
middle_name: String,
#[sea_orm(column_name = "lAsTnAmE")]
last_name: String,
orders_count: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
#[test]
fn test_column_names() {
let columns: Vec<String> = Column::iter().map(|item| item.to_string()).collect();
assert_eq!(
columns,
vec![
"id",
"username",
"firstName",
"middleName",
"lAsTnAmE",
"ordersCount",
]
);
let col =
Column::from_str("firstName").expect("column from str should recognize column_name attr");
assert!(matches!(col, Column::FirstName));
let col =
Column::from_str("first_name").expect("column from str should recognize column_name attr");
assert!(matches!(col, Column::FirstName));
let col =
Column::from_str("lastName").expect("column from str should recognize column_name attr");
assert!(matches!(col, Column::LastName));
let col =
Column::from_str("last_name").expect("column from str should recognize column_name attr");
assert!(matches!(col, Column::LastName));
let col =
Column::from_str("lAsTnAmE").expect("column from str should recognize column_name attr");
assert!(matches!(col, Column::LastName));
}
@@ -0,0 +1,79 @@
#[test]
fn when_user_import_nothing_macro_still_works_test() {
#[derive(sea_orm::DeriveValueType)]
struct MyString(String);
}
#[test]
fn when_user_alias_result_macro_still_works_test() {
#[allow(dead_code)]
type Result<T> = std::result::Result<T, ()>;
#[derive(sea_orm::DeriveValueType)]
struct MyString(String);
}
#[test]
fn when_stringy_newtype_works_test() {
#[allow(dead_code)]
#[derive(sea_orm::DeriveValueType)]
#[sea_orm(value_type = "String")]
struct Foo {
inner: i32,
}
impl std::fmt::Display for Foo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl std::str::FromStr for Foo {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self { inner: s.parse()? })
}
}
}
#[test]
fn when_explicit_stringy_newtype_works_test() {
#[derive(sea_orm::DeriveValueType)]
#[sea_orm(value_type = "String")]
struct Foo(i32);
impl std::fmt::Display for Foo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::str::FromStr for Foo {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.parse()?))
}
}
}
#[test]
fn when_custom_from_str_works() {
#[derive(sea_orm::DeriveValueType)]
#[sea_orm(
value_type = "String",
from_str = "Foo::from_str",
to_str = "Foo::to_str"
)]
struct Foo(i32);
impl Foo {
fn from_str(_s: &str) -> Result<Self, std::convert::Infallible> {
Ok(Self(42))
}
fn to_str(&self) -> String {
42.to_string()
}
}
}