queryweave_codegen/
names.rs1use quote::ToTokens;
11
12pub 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
28pub 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
74pub 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 #[rstest]
149 #[case("MergePatchOrderModel", true)]
150 #[case("MergePatchOrderLineModel", true)]
151 #[case("MergePatchModel", false)]
152 #[case("PatchMergeOrderModel", false)]
153 #[case("OrderMergePatchModel", false)]
154 #[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 #[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}