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 {
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 {
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 {
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"
)
})
}
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>());
}
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
}
}
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(
|lhs, rhs| lhs + rhs.as_str(),
)
.map_or(
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() {
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");
assert_eq!(camel_case_with_escaped_non_uax31("foo_bar"), "Foo0x5Fbar");
assert_eq!(camel_case_with_escaped_non_uax31("1"), "_0x31");
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");
}
}