pub fn extract_doc_comment(attrs: &[syn::Attribute]) -> Option<String> {
let mut doc_lines = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc")
&& let syn::Meta::NameValue(meta_nv) = &attr.meta
&& let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &meta_nv.value
{
let line = lit_str.value();
let trimmed = line
.strip_prefix(" / ")
.or_else(|| line.strip_prefix("/ "))
.unwrap_or(&line)
.trim();
doc_lines.push(trimmed.to_string());
}
}
if doc_lines.is_empty() {
None
} else {
Some(doc_lines.join("\n"))
}
}
#[allow(clippy::option_if_let_else)] pub fn strip_raw_prefix_owned(ident: String) -> String {
if let Some(stripped) = ident.strip_prefix("r#") {
stripped.to_string()
} else {
ident
}
}
pub use crate::schema_macro::type_utils::capitalize_first;
pub fn extract_schema_name_from_entity(ty: &syn::Type) -> Option<String> {
match ty {
syn::Type::Path(type_path) => {
let segments: Vec<_> = type_path.path.segments.iter().collect();
if segments.len() < 2 {
return None;
}
let last = segments.last()?;
if last.ident != "Entity" {
return None;
}
let module_segment = segments.get(segments.len() - 2)?;
let module_name = module_segment.ident.to_string();
let schema_name = capitalize_first(&module_name);
Some(schema_name)
}
_ => None,
}
}
pub fn extract_transparent(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
if !attr.path().is_ident("serde") {
return false;
}
let mut is_transparent = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("transparent") {
is_transparent = true;
}
Ok(())
});
is_transparent
})
}
pub fn extract_schema_ref_override(attrs: &[syn::Attribute]) -> Option<(String, bool)> {
attrs.iter().find_map(|attr| {
if !attr.path().is_ident("schema") {
return None;
}
let mut ref_name = None;
let mut nullable = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("ref") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
ref_name = Some(lit.value());
} else if meta.path.is_ident("nullable") {
nullable = true;
}
Ok(())
});
ref_name.map(|name| (name, nullable))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_doc_comment_single_line() {
let attrs: Vec<syn::Attribute> = syn::parse_quote! {
#[doc = " This is a doc comment"]
};
let result = extract_doc_comment(&attrs);
assert_eq!(result, Some("This is a doc comment".to_string()));
}
#[test]
fn test_extract_doc_comment_multi_line() {
let attrs: Vec<syn::Attribute> = syn::parse_quote! {
#[doc = " First line"]
#[doc = " Second line"]
#[doc = " Third line"]
};
let result = extract_doc_comment(&attrs);
assert_eq!(
result,
Some("First line\nSecond line\nThird line".to_string())
);
}
#[test]
fn test_extract_doc_comment_no_leading_space() {
let attrs: Vec<syn::Attribute> = syn::parse_quote! {
#[doc = "No leading space"]
};
let result = extract_doc_comment(&attrs);
assert_eq!(result, Some("No leading space".to_string()));
}
#[test]
fn test_extract_doc_comment_empty() {
let attrs: Vec<syn::Attribute> = vec![];
let result = extract_doc_comment(&attrs);
assert_eq!(result, None);
}
#[test]
fn test_extract_doc_comment_with_non_doc_attrs() {
let attrs: Vec<syn::Attribute> = syn::parse_quote! {
#[derive(Debug)]
#[doc = " The doc comment"]
#[serde(rename = "test")]
};
let result = extract_doc_comment(&attrs);
assert_eq!(result, Some("The doc comment".to_string()));
}
#[test]
fn test_extract_schema_name_from_entity_super_path() {
let ty: syn::Type = syn::parse_str("super::user::Entity").unwrap();
let result = extract_schema_name_from_entity(&ty);
assert_eq!(result, Some("User".to_string()));
}
#[test]
fn test_extract_schema_name_from_entity_crate_path() {
let ty: syn::Type = syn::parse_str("crate::models::memo::Entity").unwrap();
let result = extract_schema_name_from_entity(&ty);
assert_eq!(result, Some("Memo".to_string()));
}
#[test]
fn test_extract_schema_name_from_entity_not_entity() {
let ty: syn::Type = syn::parse_str("crate::models::user::Model").unwrap();
let result = extract_schema_name_from_entity(&ty);
assert_eq!(result, None);
}
#[test]
fn test_extract_schema_name_from_entity_single_segment() {
let ty: syn::Type = syn::parse_str("Entity").unwrap();
let result = extract_schema_name_from_entity(&ty);
assert_eq!(result, None);
}
#[test]
fn test_extract_schema_name_from_entity_non_path_type() {
let ty: syn::Type = syn::parse_str("&str").unwrap();
let result = extract_schema_name_from_entity(&ty);
assert_eq!(result, None);
}
#[test]
fn test_extract_schema_name_from_entity_empty_module_name() {
let ty: syn::Type = syn::parse_str("super::some_module::Entity").unwrap();
let result = extract_schema_name_from_entity(&ty);
assert_eq!(result, Some("Some_module".to_string()));
}
#[test]
fn test_strip_raw_prefix_owned() {
assert_eq!(strip_raw_prefix_owned("r#type".to_string()), "type");
assert_eq!(strip_raw_prefix_owned("r#match".to_string()), "match");
assert_eq!(strip_raw_prefix_owned("normal".to_string()), "normal");
assert_eq!(strip_raw_prefix_owned("r#".to_string()), "");
}
#[rstest::rstest]
#[case(" / leaked", "leaked")]
#[case("/ leaked", "leaked")]
fn doc_marker_prefixes_are_removed(#[case] input: &str, #[case] expected: &str) {
let item: syn::ItemStruct =
syn::parse_str(&format!(r#"#[doc = "{input}"] struct Value;"#)).unwrap();
assert_eq!(extract_doc_comment(&item.attrs), Some(expected.to_string()));
}
}