queryweave_codegen/conventions.rs
1//! How the derives name what they generate, and which names they require of
2//! the structs they are placed on or link to.
3//!
4//! Every naming rule goes through a [`Conventions`] implementation, chosen
5//! by the derive's entry point (`DefaultConventions` for this project's
6//! derives), so the rules can be swapped without touching a derive's steps.
7//!
8//! A derive never rebuilds another type's generated *type* names from these
9//! rules: it links to the path the user wrote (see `queryweave::Filterable`),
10//! so each layer may name its types under different conventions. Generated
11//! *member* names (sort variants, filter fields) are different: a mirror (an
12//! API DTO) names the mirrored model's members by applying its own conventions to
13//! the model's field names, so the layers must agree on
14//! [`Conventions::sort_variant`] and [`Conventions::filter_field`].
15//!
16//! The conventions also say where the runtime crates the generated code
17//! names live ([`Conventions::core_crate`], [`Conventions::api_crate`],
18//! [`Conventions::sql_crate`]), so a crate that renames or re-exports them
19//! can still use the derives; `#[queryweave(crate = path)]` overrides them on
20//! a single struct. Each derive names exactly one runtime crate: the one of its
21//! layer (`queryweave-api` and `queryweave-sql` re-export the core).
22//!
23//! Last, they list the integrations a `Filters` mirror may opt into
24//! ([`Conventions::integrations`], see [`crate::integrations`]).
25
26use quote::{ToTokens, format_ident};
27
28/// The names of the types `#[derive(Filters)]` generates for a struct (a
29/// domain model or a DTO).
30#[derive(Debug, PartialEq, Eq, Clone)]
31pub struct FiltersNames {
32 /// The query model: paging, sorting and the filter options.
33 pub query: syn::Ident,
34 /// The filter options model.
35 pub filters: syn::Ident,
36 /// The sortable-fields enum.
37 pub sort_field: syn::Ident,
38 /// The sort details: a sort field and its direction.
39 pub sort_details: syn::Ident,
40 /// The struct the derive is placed on.
41 pub input: syn::Ident,
42 /// The page of the struct.
43 pub page: syn::Ident,
44 /// The query parameters (API `Filters` only).
45 pub query_params: syn::Ident,
46}
47
48/// The naming rules of the derives.
49///
50/// Its functions report a name that does not fit through
51/// [`crate::errors`], at what the user wrote: the struct's name, or the path
52/// inside the attribute naming another struct.
53pub trait Conventions {
54 /// The names of the types `#[derive(Filters)]` generates for `input`.
55 ///
56 /// # Errors
57 ///
58 /// Returns an error, at `input`, when the struct's name does not fit.
59 fn filters_names(input: &syn::Ident) -> syn::Result<FiltersNames>;
60
61 /// Check the name of a model deriving `MergePatch`.
62 ///
63 /// # Errors
64 ///
65 /// Returns an error, at `name`, when it does not fit.
66 fn check_model_name(name: &syn::Ident) -> syn::Result<()>;
67
68 /// Check the name of a merge patch struct: the struct a derive is placed
69 /// on (`MergePatchNullableFields`, `ValidateNonNullable`), or one an
70 /// attribute links to (`#[merge_patch(patch = path)]`, or
71 /// `#[validate_non_nullable(mirrors = path)]`), calling it
72 /// `what`.
73 ///
74 /// # Errors
75 ///
76 /// Returns an error, at `span`, when the name does not fit.
77 fn check_merge_patch_name<S: ToTokens + ?Sized>(
78 name: &syn::Ident,
79 span: &S,
80 what: &str,
81 ) -> syn::Result<()>;
82
83 /// The variant of the generated sortable-fields enum for the `#[sortable]`
84 /// field `field`, spanned to `span`.
85 ///
86 /// Shared across layers: a mirror names the mirrored model's variants with
87 /// it.
88 fn sort_variant(field: &syn::Ident, span: proc_macro2::Span) -> syn::Ident;
89
90 /// The generated filter field (and, on a DTO, query parameter) filtering
91 /// `field` with the operator written `operator` in `#[filter(...)]` (`lt`,
92 /// `eq`, `gt`, `sw`, `c`, `ew` or `bool`), spanned to `span`.
93 ///
94 /// Shared across layers: a mirror names the mirrored model's filter fields
95 /// with it.
96 fn filter_field(field: &syn::Ident, operator: &str, span: proc_macro2::Span) -> syn::Ident;
97
98 /// Check the name of a wrapper an attribute links to
99 /// (`#[append_to_query(filters = path)]` of `AppendToQuery`), calling it
100 /// `what`.
101 ///
102 /// # Errors
103 ///
104 /// Returns an error, at `span`, when the name does not fit.
105 fn check_wrapper_name<S: ToTokens + ?Sized>(
106 name: &syn::Ident,
107 span: &S,
108 what: &str,
109 ) -> syn::Result<()>;
110
111 /// The path of the core runtime crate, named by the code generated for a
112 /// root model (`Filters`, `MergePatch`, `MergePatchNullableFields`,
113 /// `RawValue`).
114 ///
115 /// Defaults to `::queryweave`: absolute, so a local item of the same name
116 /// cannot shadow it. Override it when the crate is renamed or re-exported.
117 #[must_use]
118 fn core_crate() -> syn::Path {
119 syn::parse_quote!(::queryweave)
120 }
121
122 /// The path of the API runtime crate, named by the code generated for an
123 /// API type (a `Filters` mirror with `serde` or `utoipa`,
124 /// `ValidateNonNullable`). Defaults to `::queryweave_api`.
125 #[must_use]
126 fn api_crate() -> syn::Path {
127 syn::parse_quote!(::queryweave_api)
128 }
129
130 /// The path of the SQL runtime crate, named by the code generated for the
131 /// persistence layer (`Columns`, `AppendToQuery`,
132 /// `AppendToQueryWithSeparator`). Defaults to `::queryweave_sql`.
133 #[must_use]
134 fn sql_crate() -> syn::Path {
135 syn::parse_quote!(::queryweave_sql)
136 }
137
138 /// The integrations a `Filters` mirror may opt into, each with the flag
139 /// of `#[filters(...)]` named after it, in the order their additions are
140 /// generated. Their names must differ from each other and from `mirrors`.
141 ///
142 /// Defaults to the built-in ones ([`crate::integrations::BUILT_IN`]:
143 /// `serde`, `utoipa`, `validator`). Override it to add a crate's own (e.g.
144 /// a GraphQL integration) or to replace some (e.g. serde by another
145 /// serializer).
146 #[must_use]
147 fn integrations() -> Vec<&'static dyn crate::integrations::Integration> {
148 crate::integrations::BUILT_IN.to_vec()
149 }
150}
151
152/// This project's conventions.
153///
154/// `Filters` names its types after the struct's pascal-case segments, the
155/// last one being the layer's suffix (`OrderModel` → `OrdersQueryModel`,
156/// `OrderSortFieldModel`, ...; `OrderDto` → `OrdersQueryParams`, ...), so the
157/// struct's name needs at least two segments, as does a model deriving
158/// `MergePatch`. A merge patch struct is named `MergePatch…`
159/// (`MergePatchOrderModel`), and a wrapper is pascal cased.
160///
161/// The runtime crates keep their default, absolute paths (`::queryweave`,
162/// ...).
163///
164/// A sort variant is its field pascal cased (`delivery_date` →
165/// `DeliveryDate`), and a filter field is its field followed by the operator
166/// (`username_sw`), except `bool`, which keeps the bare field (`is_active`).
167pub struct DefaultConventions;
168
169impl Conventions for DefaultConventions {
170 fn filters_names(input: &syn::Ident) -> syn::Result<FiltersNames> {
171 let parts = crate::names::pascal_case_segments(input, input, "struct", 2)?;
172 // At least two segments, so there is a last one.
173 let (last, init) = parts
174 .split_last()
175 .map_or(("", &[][..]), |(last, init)| (last.as_str(), init));
176 let without_last = init.join("");
177
178 Ok(FiltersNames {
179 query: format_ident!("{without_last}sQuery{last}"),
180 filters: format_ident!("{without_last}sFilterOptions{last}"),
181 sort_field: format_ident!("{without_last}SortField{last}"),
182 sort_details: format_ident!("{without_last}SortDetails{last}"),
183 input: input.clone(),
184 page: format_ident!("{without_last}sPage{last}"),
185 query_params: format_ident!("{without_last}sQueryParams"),
186 })
187 }
188
189 fn check_model_name(name: &syn::Ident) -> syn::Result<()> {
190 crate::names::pascal_case_segments(name, name, "struct", 2).map(drop)
191 }
192
193 fn check_merge_patch_name<S: ToTokens + ?Sized>(
194 name: &syn::Ident,
195 span: &S,
196 what: &str,
197 ) -> syn::Result<()> {
198 crate::names::merge_patch_segments(name, span, what).map(drop)
199 }
200
201 fn sort_variant(field: &syn::Ident, span: proc_macro2::Span) -> syn::Ident {
202 let pascal_cased = field
203 .to_string()
204 .split('_')
205 .map(crate::utils::uppercase_first_letter)
206 .collect::<String>();
207
208 format_ident!("{pascal_cased}", span = span)
209 }
210
211 fn filter_field(field: &syn::Ident, operator: &str, span: proc_macro2::Span) -> syn::Ident {
212 if operator == "bool" {
213 format_ident!("{field}", span = span)
214 } else {
215 format_ident!("{field}_{operator}", span = span)
216 }
217 }
218
219 fn check_wrapper_name<S: ToTokens + ?Sized>(
220 name: &syn::Ident,
221 span: &S,
222 what: &str,
223 ) -> syn::Result<()> {
224 crate::names::pascal_case_segments(name, span, what, 1).map(drop)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::{Conventions, DefaultConventions, FiltersNames};
231 use pretty_assertions::assert_eq;
232 use quote::format_ident;
233
234 #[test]
235 fn the_default_filters_names_follow_the_struct_segments() {
236 assert_eq!(
237 DefaultConventions::filters_names(&format_ident!("OrderLineModel")).unwrap(),
238 FiltersNames {
239 query: format_ident!("OrderLinesQueryModel"),
240 filters: format_ident!("OrderLinesFilterOptionsModel"),
241 sort_field: format_ident!("OrderLineSortFieldModel"),
242 sort_details: format_ident!("OrderLineSortDetailsModel"),
243 input: format_ident!("OrderLineModel"),
244 page: format_ident!("OrderLinesPageModel"),
245 query_params: format_ident!("OrderLinesQueryParams"),
246 }
247 );
248 }
249
250 #[test]
251 fn the_default_filters_names_need_two_segments() {
252 assert!(DefaultConventions::filters_names(&format_ident!("Order")).is_err());
253 assert!(DefaultConventions::filters_names(&format_ident!("order_model")).is_err());
254 }
255
256 #[test]
257 fn the_default_model_name_needs_two_segments() {
258 assert!(DefaultConventions::check_model_name(&format_ident!("OrderModel")).is_ok());
259 assert!(DefaultConventions::check_model_name(&format_ident!("Order")).is_err());
260 }
261
262 #[test]
263 fn the_default_merge_patch_name_starts_with_merge_patch() {
264 let name = format_ident!("MergePatchOrderModel");
265 assert!(DefaultConventions::check_merge_patch_name(&name, &name, "struct").is_ok());
266
267 let name = format_ident!("OrderPatch");
268 assert!(DefaultConventions::check_merge_patch_name(&name, &name, "struct").is_err());
269 }
270
271 #[test]
272 fn the_default_sort_variant_is_the_field_pascal_cased() {
273 let span = proc_macro2::Span::call_site();
274
275 assert_eq!(
276 DefaultConventions::sort_variant(&format_ident!("delivery_date"), span),
277 format_ident!("DeliveryDate")
278 );
279 assert_eq!(
280 DefaultConventions::sort_variant(&format_ident!("value"), span),
281 format_ident!("Value")
282 );
283 }
284
285 #[test]
286 fn the_default_filter_field_is_suffixed_with_the_operator_except_bool() {
287 let span = proc_macro2::Span::call_site();
288 let field = format_ident!("username");
289
290 assert_eq!(
291 DefaultConventions::filter_field(&field, "sw", span),
292 format_ident!("username_sw")
293 );
294 assert_eq!(
295 DefaultConventions::filter_field(&field, "bool", span),
296 format_ident!("username")
297 );
298 }
299
300 #[test]
301 fn the_default_runtime_crates_are_named_by_absolute_path() {
302 let path = |path: syn::Path| quote::ToTokens::to_token_stream(&path).to_string();
303
304 assert_eq!(path(DefaultConventions::core_crate()), ":: queryweave");
305 assert_eq!(path(DefaultConventions::api_crate()), ":: queryweave_api");
306 assert_eq!(path(DefaultConventions::sql_crate()), ":: queryweave_sql");
307 }
308
309 #[test]
310 fn the_default_wrapper_name_is_pascal_cased() {
311 let name = format_ident!("OrdersQueryModelWrapper");
312 assert!(DefaultConventions::check_wrapper_name(&name, &name, "wrapper").is_ok());
313
314 let name = format_ident!("orders_wrapper");
315 assert!(DefaultConventions::check_wrapper_name(&name, &name, "wrapper").is_err());
316 }
317}