queryweave_codegen/integrations.rs
1//! What a `Filters` mirror's generated code integrates with (serialization,
2//! API documentation, validation, ...), each opted into per struct with a
3//! flag of `#[filters(...)]` named after it.
4//!
5//! An integration only adds to the mirror's generated types: derives and
6//! attributes, field attributes, extra items, and the sort direction and page
7//! types it needs. The mirror's code generation names no integration, and no
8//! integration relies on another's attributes: each states the wire format
9//! itself (e.g. snake case sort variants).
10//!
11//! The integrations a mirror may opt into are the conventions'
12//! ([`crate::conventions::Conventions::integrations`]): the built-in ones
13//! ([`BUILT_IN`]: `serde`, `utoipa`, `validator`, `schemars`, `garde`) by
14//! default, to which a
15//! proc-macro crate adds its own, or which it replaces (e.g. by another
16//! serializer).
17//!
18//! # Examples
19//!
20//! An integration deriving a GraphQL input type, with its own sort direction
21//! (named by path from the using crate, e.g. a type deriving the GraphQL
22//! enum and converting both ways with `queryweave::SortDirectionModel`):
23//!
24//! ```rust
25//! use queryweave_codegen::integrations::{Context, Integration, TypeAdditions};
26//! use quote::quote;
27//!
28//! struct Graphql;
29//!
30//! impl Integration for Graphql {
31//! fn name(&self) -> &'static str {
32//! "graphql"
33//! }
34//!
35//! fn sort_direction(&self, _context: &Context<'_>) -> Option<proc_macro2::TokenStream> {
36//! Some(quote!(crate::graphql::SortDirection))
37//! }
38//!
39//! fn sort_field(&self, _context: &Context<'_>) -> TypeAdditions {
40//! TypeAdditions {
41//! derives: vec![quote!(async_graphql::Enum)],
42//! attributes: quote!(#[graphql(rename_items = "snake_case")]),
43//! }
44//! }
45//! }
46//!
47//! // Registered next to the built-in ones, in a crate's conventions:
48//! let integrations: Vec<&'static dyn Integration> = queryweave_codegen::integrations::BUILT_IN
49//! .into_iter()
50//! .chain([&Graphql as &'static dyn Integration])
51//! .collect();
52//! assert_eq!(integrations.len(), queryweave_codegen::integrations::BUILT_IN.len() + 1);
53//! ```
54
55mod garde;
56mod schemars;
57mod serde;
58mod utoipa;
59mod validator;
60
61/// What an integration knows of the mirror it adds to.
62pub struct Context<'a> {
63 /// The names of the mirror's generated types.
64 pub names: &'a crate::conventions::FiltersNames,
65 /// The runtime crate the generated code names.
66 pub runtime: &'a syn::Path,
67 /// The integration's flag, as written in `#[filters(...)]`: where to
68 /// report what concerns the integration (e.g. a disabled feature).
69 pub flag: &'a syn::Ident,
70}
71
72/// What an integration knows of the merge patch DTO deriving
73/// `ValidateNonNullable` it adds to.
74pub struct NonNullableContext<'a> {
75 /// The DTO.
76 pub ident: &'a syn::Ident,
77 /// The DTO's fields, by the names clients send.
78 pub fields: &'a [syn::Ident],
79 /// The runtime crate the generated code names.
80 pub runtime: &'a syn::Path,
81 /// The integration's flag, as written in `#[validate_non_nullable(...)]`.
82 pub flag: &'a syn::Ident,
83}
84
85/// What an integration adds to one generated type.
86#[derive(Default)]
87pub struct TypeAdditions {
88 /// The derives, each a path (e.g. `serde::Deserialize`).
89 pub derives: Vec<proc_macro2::TokenStream>,
90 /// The attributes, written after the derives.
91 pub attributes: proc_macro2::TokenStream,
92}
93
94/// An integration of a mirror's generated code with another crate.
95///
96/// Every addition defaults to nothing, so an integration implements only what
97/// it adds. Its additions name the crates they need by absolute path or by a
98/// path from the using crate, never assuming the call site's imports.
99pub trait Integration: Sync {
100 /// Its flag in `#[filters(...)]`, e.g. `serde`.
101 fn name(&self) -> &'static str;
102
103 /// The integrations it cannot do without, each with the reason, reported
104 /// at its flag when one is missing.
105 fn requires(&self) -> &'static [(&'static str, &'static str)] {
106 &[]
107 }
108
109 /// Whether its additions name the API runtime crate (e.g.
110 /// `SortDirectionDto`), which a mirror then names by default.
111 fn names_api_runtime(&self) -> bool {
112 false
113 }
114
115 /// The sort direction type it needs in the sort details, if not the core
116 /// `SortDirectionModel`. It converts both ways with `SortDirectionModel`,
117 /// for the mirror to convert into the struct it mirrors and back. The
118 /// integrations of a mirror providing one must agree on it.
119 fn sort_direction(&self, _context: &Context<'_>) -> Option<proc_macro2::TokenStream> {
120 None
121 }
122
123 /// The type of a page of the mirror's input, for the page alias. The
124 /// integrations of a mirror providing one must agree on it.
125 fn page(&self, _context: &Context<'_>) -> Option<proc_macro2::TokenStream> {
126 None
127 }
128
129 /// Its additions to the sort field enum.
130 fn sort_field(&self, _context: &Context<'_>) -> TypeAdditions {
131 TypeAdditions::default()
132 }
133
134 /// Its additions to the sort details struct.
135 fn sort_details(&self, _context: &Context<'_>) -> TypeAdditions {
136 TypeAdditions::default()
137 }
138
139 /// Its additions to the query parameters struct.
140 fn query_params(&self, _context: &Context<'_>) -> TypeAdditions {
141 TypeAdditions::default()
142 }
143
144 /// Its attributes on the `page_size` query parameter.
145 fn page_size(&self, _context: &Context<'_>) -> proc_macro2::TokenStream {
146 proc_macro2::TokenStream::new()
147 }
148
149 /// Its attributes on the `sort` query parameter (of a sortable mirror).
150 fn sort(&self, _context: &Context<'_>) -> proc_macro2::TokenStream {
151 proc_macro2::TokenStream::new()
152 }
153
154 /// Its items of any mirror (e.g. trait impls on the generated types).
155 fn items(&self, _context: &Context<'_>) -> proc_macro2::TokenStream {
156 proc_macro2::TokenStream::new()
157 }
158
159 /// Its items of a sortable mirror (e.g. a validator of the sort details).
160 fn sortable_items(&self, _context: &Context<'_>) -> proc_macro2::TokenStream {
161 proc_macro2::TokenStream::new()
162 }
163
164 /// Whether it applies to `ValidateNonNullable` (a validation library),
165 /// defining the DTO's `validate_non_nullable` with
166 /// [`Integration::validate_non_nullable`].
167 fn supports_validate_non_nullable(&self) -> bool {
168 false
169 }
170
171 /// The DTO's `validate_non_nullable`: its library's validation of the
172 /// struct, then an error for each field of `rejected_nulls()` (the fields
173 /// set to `null` that may not be), in its library's error type.
174 fn validate_non_nullable(&self, _context: &NonNullableContext<'_>) -> proc_macro2::TokenStream {
175 proc_macro2::TokenStream::new()
176 }
177}
178
179/// The built-in integrations, the default of
180/// [`crate::conventions::Conventions::integrations`].
181///
182/// In the order their additions are generated: `serde` (deserializable query
183/// parameters, (de)serializable sort types, the page alias), `utoipa` (API
184/// documentation schemas), `validator` (validation, needing `serde`),
185/// `schemars` (JSON schemas) and `garde` (validation needing no serializer).
186pub const BUILT_IN: [&dyn Integration; 5] = [
187 &serde::Serde,
188 &utoipa::Utoipa,
189 &validator::Validator,
190 &schemars::Schemars,
191 &garde::Garde,
192];
193
194/// An integration a mirror opts into, with the flag written for it.
195#[derive(Clone)]
196pub(crate) struct Active {
197 /// The integration.
198 pub integration: &'static dyn Integration,
199 /// Its flag, as written (so an error about it can point at it).
200 pub flag: syn::Ident,
201}
202
203impl std::fmt::Debug for Active {
204 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 formatter.debug_tuple("Active").field(&self.flag).finish()
206 }
207}
208
209impl Active {
210 /// What the integration knows of the mirror whose generated types are
211 /// named `names`, naming `runtime`.
212 pub(crate) const fn context<'a>(
213 &'a self,
214 names: &'a crate::conventions::FiltersNames,
215 runtime: &'a syn::Path,
216 ) -> Context<'a> {
217 Context {
218 names,
219 runtime,
220 flag: &self.flag,
221 }
222 }
223}
224
225/// A check that the API runtime is built with its feature `feature` (e.g.
226/// `Serde`), which the integration naming its types needs: reported at the
227/// integration's flag, naming the feature to enable (see the runtime's
228/// `features` module), rather than only as traits missing on its types.
229fn api_feature_check(context: &Context<'_>, feature: &str) -> proc_macro2::TokenStream {
230 let span = context.flag.span();
231 let runtime = crate::utils::respanned(context.runtime, span);
232 let feature = syn::Ident::new(feature, span);
233
234 quote::quote_spanned! {span=>
235 const _: () = {
236 const fn requires_feature<T: #runtime::features::#feature>() {}
237 requires_feature::<#runtime::SortDirectionDto>();
238 };
239 }
240}
241
242/// Integrations are told apart by name.
243impl PartialEq for Active {
244 fn eq(&self, other: &Self) -> bool {
245 self.integration.name() == other.integration.name() && self.flag == other.flag
246 }
247}
248
249impl Eq for Active {}
250
251/// Check that `registry`'s integrations can each be told apart by its flag:
252/// distinct names, none of them another option of `#[filters(...)]`.
253///
254/// # Errors
255///
256/// Returns an error at the derive (the conventions are not the user's code)
257/// naming the clashing integration.
258pub(crate) fn check_registry(registry: &[&'static dyn Integration]) -> syn::Result<()> {
259 for (index, integration) in registry.iter().enumerate() {
260 let name = integration.name();
261 let clashes = name == "mirrors"
262 || registry
263 .iter()
264 .take(index)
265 .any(|earlier| earlier.name() == name);
266 if clashes {
267 return crate::errors::abort_call_site(
268 &format!("The conventions register a second `{name}` option of #[filters(...)]"),
269 "Give each integration of `Conventions::integrations` its own name, other than `mirrors`",
270 );
271 }
272 }
273 Ok(())
274}
275
276#[cfg(test)]
277mod tests {
278
279 /// An integration named `name`, adding nothing.
280 struct Named(&'static str);
281
282 impl super::Integration for Named {
283 fn name(&self) -> &'static str {
284 self.0
285 }
286 }
287
288 static OTHER_SERDE: Named = Named("serde");
289 static MIRRORS: Named = Named("mirrors");
290 static GRAPHQL: Named = Named("graphql");
291
292 // The built-in integrations, and others added to them, are told apart.
293 #[test]
294 fn test_check_registry_accepts_distinct_names() {
295 assert!(super::check_registry(&super::BUILT_IN).is_ok());
296 assert!(super::check_registry(&[super::BUILT_IN[0], &GRAPHQL]).is_ok());
297 }
298
299 // A flag must name one integration, and not be another option.
300 #[test]
301 fn test_check_registry_rejects_clashing_names() {
302 assert!(super::check_registry(&[super::BUILT_IN[0], &OTHER_SERDE]).is_err());
303 assert!(super::check_registry(&[&MIRRORS]).is_err());
304 }
305
306 // What an integration requires is another built-in one.
307 #[test]
308 fn test_requirements_name_built_in_integrations() {
309 for integration in super::BUILT_IN {
310 for (required, _) in integration.requires() {
311 assert!(
312 super::BUILT_IN
313 .iter()
314 .any(|other| other.name() == *required),
315 "{required}"
316 );
317 }
318 }
319 }
320}