Skip to main content

queryweave_codegen/
names.rs

1//! Checks on the names of the structs a derive is placed on or links to.
2//!
3//! The building blocks of [`crate::conventions::DefaultConventions`], which
4//! names the generated types after these structs' pascal-case segments
5//! (`OrderModel` → `Orders…Model`), so the names must split into enough of
6//! them. Derives call them only through their conventions. Errors are
7//! reported at the span given: the struct's name, or the path of the
8//! attribute naming another struct.
9
10use quote::ToTokens;
11
12/// The struct named by `path` (e.g. of `#[merge_patch(patch = path)]`): its
13/// last segment.
14///
15/// # Errors
16///
17/// Returns an error at the path when it has no segment, naming it `what`.
18pub fn path_name<'a>(path: &'a syn::Path, what: &str) -> syn::Result<&'a syn::Ident> {
19    let Some(segment) = path.segments.last() else {
20        let message = format!("The path names no {what}");
21        let help = format!("Name the {what} with a path, e.g. `crate::models::OrderModel`");
22        return crate::errors::abort_spanned(path, &message, &help);
23    };
24
25    Ok(&segment.ident)
26}
27
28/// The pascal-case segments of the struct name `name`, which must have at
29/// least `min_segments`; errors are reported at `span`, calling the struct
30/// `what` (e.g. "struct", "merge patch model").
31///
32/// # Errors
33///
34/// Returns an error when `name` is not pascal cased, or has too few segments.
35///
36/// # Examples
37///
38/// ```rust
39/// # use queryweave_codegen::names::pascal_case_segments;
40/// let name: syn::Ident = syn::parse_quote!(OrderModel);
41///
42/// assert_eq!(
43///     pascal_case_segments(&name, &name, "struct", 2).unwrap(),
44///     vec!["Order", "Model"]
45/// );
46/// assert!(pascal_case_segments(&name, &name, "struct", 3).is_err());
47/// ```
48pub fn pascal_case_segments<S: ToTokens + ?Sized>(
49    name: &syn::Ident,
50    span: &S,
51    what: &str,
52    min_segments: usize,
53) -> syn::Result<Vec<String>> {
54    let name_str = name.to_string();
55    let Ok(segments) = crate::utils::split_pascal_case(&name_str) else {
56        let message = format!("The {what}'s name `{name}` is not pascal cased");
57        let help = format!("Rename the {what} in PascalCase");
58        return crate::errors::abort_spanned(span, &message, &help);
59    };
60
61    if segments.len() < min_segments {
62        let message = format!(
63            "The {what}'s name `{name}` must have at least {min_segments} pascal-case segments"
64        );
65        let help = format!(
66            "Rename the {what} so its name has at least {min_segments} segments (`OrderModel` has two: `Order` and `Model`)"
67        );
68        return crate::errors::abort_spanned(span, &message, &help);
69    }
70
71    Ok(segments.into_iter().map(String::from).collect())
72}
73
74/// The pascal-case segments of the name of a merge patch struct: `MergePatch`
75/// followed by at least two more (e.g. `MergePatchOrderModel`); errors are
76/// reported at `span`, calling the struct `what`.
77///
78/// # Errors
79///
80/// Returns an error when `name` is not such a name.
81///
82/// # Examples
83///
84/// ```rust
85/// # use queryweave_codegen::names::merge_patch_segments;
86/// let name: syn::Ident = syn::parse_quote!(MergePatchOrderModel);
87/// let misnamed: syn::Ident = syn::parse_quote!(PatchOrderModel);
88///
89/// assert_eq!(merge_patch_segments(&name, &name, "struct").unwrap().len(), 4);
90/// assert!(merge_patch_segments(&misnamed, &misnamed, "struct").is_err());
91/// ```
92pub fn merge_patch_segments<S: ToTokens + ?Sized>(
93    name: &syn::Ident,
94    span: &S,
95    what: &str,
96) -> syn::Result<Vec<String>> {
97    let segments = pascal_case_segments(name, span, what, 1)?;
98
99    let starts_with_merge_patch = segments.first().is_some_and(|first| first == "Merge")
100        && segments.get(1).is_some_and(|second| second == "Patch");
101    if !starts_with_merge_patch || segments.len() < 4 {
102        let message = format!(
103            "The {what}'s name `{name}` must be `MergePatch` followed by at least two pascal-case segments"
104        );
105        let help = format!("Rename the {what}, e.g. `MergePatchOrderModel`");
106        return crate::errors::abort_spanned(span, &message, &help);
107    }
108
109    Ok(segments)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::{merge_patch_segments, pascal_case_segments, path_name};
115    use pretty_assertions::assert_eq;
116    use rstest::rstest;
117
118    fn ident(name: &str) -> syn::Ident {
119        syn::parse_str(name).unwrap()
120    }
121
122    #[test]
123    fn the_path_names_its_last_segment() {
124        let path: syn::Path = syn::parse_quote!(crate::models::OrderModel);
125
126        assert_eq!(path_name(&path, "model").unwrap(), "OrderModel");
127    }
128
129    #[rstest]
130    #[case("OrderModel", 2, Some(vec!["Order", "Model"]))]
131    #[case("OrderModel", 3, None)]
132    #[case("Order", 2, None)]
133    #[case("order_model", 1, None)]
134    fn the_segments_are_counted_against_the_minimum(
135        #[case] name: &str,
136        #[case] min_segments: usize,
137        #[case] expected: Option<Vec<&str>>,
138    ) {
139        let name = ident(name);
140
141        assert_eq!(
142            pascal_case_segments(&name, &name, "struct", min_segments).ok(),
143            expected.map(|segments| segments.into_iter().map(String::from).collect())
144        );
145    }
146
147    // Exactly four segments is enough: `MergePatch` + two.
148    #[rstest]
149    #[case("MergePatchOrderModel", true)]
150    #[case("MergePatchOrderLineModel", true)]
151    #[case("MergePatchModel", false)]
152    #[case("PatchMergeOrderModel", false)]
153    #[case("OrderMergePatchModel", false)]
154    // Both leading segments are needed: `Merge` alone, or `Patch` alone.
155    #[case("MergeOrderLineModel", false)]
156    #[case("OrderPatchLineModel", false)]
157    fn a_merge_patch_name_starts_with_merge_patch(#[case] name: &str, #[case] is_valid: bool) {
158        let name = ident(name);
159
160        assert_eq!(
161            merge_patch_segments(&name, &name, "struct").is_ok(),
162            is_valid
163        );
164    }
165
166    // The error names the struct and the offending name.
167    #[test]
168    fn the_error_names_the_struct() {
169        let name = ident("PatchOrderModel");
170        let error = merge_patch_segments(&name, &name, "merge patch model").unwrap_err();
171
172        assert!(error.to_string().starts_with(
173            "The merge patch model's name `PatchOrderModel` must be `MergePatch` followed by at least two pascal-case segments"
174        ));
175    }
176}