use quote::ToTokens;
pub fn path_name<'a>(path: &'a syn::Path, what: &str) -> syn::Result<&'a syn::Ident> {
let Some(segment) = path.segments.last() else {
let message = format!("The path names no {what}");
let help = format!("Name the {what} with a path, e.g. `crate::models::OrderModel`");
return crate::errors::abort_spanned(path, &message, &help);
};
Ok(&segment.ident)
}
pub fn pascal_case_segments<S: ToTokens + ?Sized>(
name: &syn::Ident,
span: &S,
what: &str,
min_segments: usize,
) -> syn::Result<Vec<String>> {
let name_str = name.to_string();
let Ok(segments) = crate::utils::split_pascal_case(&name_str) else {
let message = format!("The {what}'s name `{name}` is not pascal cased");
let help = format!("Rename the {what} in PascalCase");
return crate::errors::abort_spanned(span, &message, &help);
};
if segments.len() < min_segments {
let message = format!(
"The {what}'s name `{name}` must have at least {min_segments} pascal-case segments"
);
let help = format!(
"Rename the {what} so its name has at least {min_segments} segments (`OrderModel` has two: `Order` and `Model`)"
);
return crate::errors::abort_spanned(span, &message, &help);
}
Ok(segments.into_iter().map(String::from).collect())
}
pub fn merge_patch_segments<S: ToTokens + ?Sized>(
name: &syn::Ident,
span: &S,
what: &str,
) -> syn::Result<Vec<String>> {
let segments = pascal_case_segments(name, span, what, 1)?;
let starts_with_merge_patch = segments.first().is_some_and(|first| first == "Merge")
&& segments.get(1).is_some_and(|second| second == "Patch");
if !starts_with_merge_patch || segments.len() < 4 {
let message = format!(
"The {what}'s name `{name}` must be `MergePatch` followed by at least two pascal-case segments"
);
let help = format!("Rename the {what}, e.g. `MergePatchOrderModel`");
return crate::errors::abort_spanned(span, &message, &help);
}
Ok(segments)
}
#[cfg(test)]
mod tests {
use super::{merge_patch_segments, pascal_case_segments, path_name};
use pretty_assertions::assert_eq;
use rstest::rstest;
fn ident(name: &str) -> syn::Ident {
syn::parse_str(name).unwrap()
}
#[test]
fn the_path_names_its_last_segment() {
let path: syn::Path = syn::parse_quote!(crate::models::OrderModel);
assert_eq!(path_name(&path, "model").unwrap(), "OrderModel");
}
#[rstest]
#[case("OrderModel", 2, Some(vec!["Order", "Model"]))]
#[case("OrderModel", 3, None)]
#[case("Order", 2, None)]
#[case("order_model", 1, None)]
fn the_segments_are_counted_against_the_minimum(
#[case] name: &str,
#[case] min_segments: usize,
#[case] expected: Option<Vec<&str>>,
) {
let name = ident(name);
assert_eq!(
pascal_case_segments(&name, &name, "struct", min_segments).ok(),
expected.map(|segments| segments.into_iter().map(String::from).collect())
);
}
#[rstest]
#[case("MergePatchOrderModel", true)]
#[case("MergePatchOrderLineModel", true)]
#[case("MergePatchModel", false)]
#[case("PatchMergeOrderModel", false)]
#[case("OrderMergePatchModel", false)]
#[case("MergeOrderLineModel", false)]
#[case("OrderPatchLineModel", false)]
fn a_merge_patch_name_starts_with_merge_patch(#[case] name: &str, #[case] is_valid: bool) {
let name = ident(name);
assert_eq!(
merge_patch_segments(&name, &name, "struct").is_ok(),
is_valid
);
}
#[test]
fn the_error_names_the_struct() {
let name = ident("PatchOrderModel");
let error = merge_patch_segments(&name, &name, "merge patch model").unwrap_err();
assert!(error.to_string().starts_with(
"The merge patch model's name `PatchOrderModel` must be `MergePatch` followed by at least two pascal-case segments"
));
}
}