Skip to main content

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}