model_views_derive/
lib.rs

1//! Procedural macro for deriving view types from models.
2//!
3//! This crate provides the `#[derive(Views)]` macro that automatically generates
4//! specialized view types for different access modes (Get, Create, Patch) from a
5//! base model struct.
6//!
7//! # Overview
8//!
9//! The `Views` derive macro generates up to three view types for a model:
10//!
11//! - **`{Model}Get`**: A read-only view for retrieving data
12//! - **`{Model}Create`**: A view for creating new instances
13//! - **`{Model}Patch`**: A view for partial updates using the `Patch<T>` wrapper
14//!
15//! Each generated type only includes fields relevant to its access mode, based on
16//! field-level attributes that specify visibility policies.
17//!
18//! # Field Policies
19//!
20//! Control field visibility in each view using these attributes:
21//!
22//! - `#[views(get = "policy")]`: Controls field visibility in the Get view
23//!   - `"required"` (default): Field is always present
24//!   - `"optional"`: Field is wrapped in `Option<T>`
25//!   - `"forbidden"`: Field is excluded from this view
26//!
27//! - `#[views(create = "policy")]`: Controls field visibility in the Create view
28//!   - `"required"` (default): Field must be provided
29//!   - `"optional"`: Field is wrapped in `Option<T>`
30//!   - `"forbidden"`: Field is excluded from this view
31//!
32//! - `#[views(patch = "policy")]`: Controls field visibility in the Patch view
33//!   - `"patch"` (default): Field is wrapped in `Patch<T>`
34//!   - `"optional"`: Field is wrapped in `Patch<Option<T>>`
35//!   - `"forbidden"`: Field is excluded from this view
36//!
37//! # Container Attributes
38//!
39//! - `#[views(crate = "path")]`: Override the path to the `model_views` crate
40//! - `#[views(serde)]`: Automatically derive `Serialize`/`Deserialize` for generated types
41//!
42//! # Example
43//!
44//! ```rust,ignore
45//! #[derive(Views)]
46//! #[views(serde)]
47//! struct User {
48//!     #[views(get = "required", create = "forbidden", patch = "forbidden")]
49//!     id: i64,
50//!     
51//!     #[views(get = "required", create = "required", patch = "patch")]
52//!     name: String,
53//!     
54//!     #[views(get = "optional", create = "optional", patch = "optional")]
55//!     email: String,
56//! }
57//! ```
58//!
59//! This generates:
60//! - `UserGet` with `id: i64`, `name: String`, `email: Option<Option<String>>`
61//! - `UserCreate` with `name: String`, `email: Option<Option<String>>`
62//! - `UserPatch` with `name: Patch<String>`, `email: Patch<Option<String>>`
63
64#![allow(clippy::option_if_let_else)]
65
66use darling::{FromDeriveInput, FromField, util::Ignored};
67use proc_macro::TokenStream;
68use quote::{format_ident, quote};
69use syn::{DeriveInput, Type, parse_macro_input};
70
71const BASE_CRATE: &str = "model_views";
72
73#[derive(FromDeriveInput)]
74#[darling(attributes(views))]
75struct ViewsInput {
76    ident: syn::Ident,
77    vis: syn::Visibility,
78    generics: syn::Generics,
79    data: darling::ast::Data<Ignored, ViewsField>,
80    /// Path (string) to base crate, e.g. "`model_views`"
81    #[darling(default)]
82    crate_: Option<String>,
83    /// Whether to derive serde traits for the generated types
84    #[darling(default)]
85    serde: Option<bool>,
86}
87
88#[derive(FromField, Clone)]
89#[darling(attributes(views))]
90struct ViewsField {
91    ident: Option<syn::Ident>,
92    ty: Type,
93    #[darling(default)]
94    get: Option<String>,
95    #[darling(default)]
96    create: Option<String>,
97    #[darling(default)]
98    patch: Option<String>,
99}
100
101/// Derives view types for different access modes from a model struct.
102///
103/// This procedural macro generates up to three specialized view types based on the
104/// annotated model:
105///
106/// - `{Model}Get`: For read/retrieval operations
107/// - `{Model}Create`: For creation operations
108/// - `{Model}Patch`: For update/modification operations
109///
110/// # Generated Types
111///
112/// For a struct named `User`, the macro generates:
113/// - `UserGet` with appropriate `Serialize` derives (if serde enabled)
114/// - `UserCreate` with appropriate `Deserialize` derives (if serde enabled)
115/// - `UserPatch` with `Default` and `Deserialize` derives (if serde enabled)
116///
117/// Each generated type implements `View<ViewMode{Get,Create,Patch}>` for the original type,
118/// allowing generic code to work with different view modes.
119///
120/// # Container Attributes
121///
122/// The `#[views(...)]` attribute on the struct itself accepts:
123///
124/// - `crate = "path"`: Override the path to the `model_views` crate. Useful when
125///   re-exporting or when the crate is available under a different name.
126///   
127///   ```rust,ignore
128///   #[derive(Views)]
129///   #[views(crate = "my_models::views")]
130///   struct User { /* ... */ }
131///   ```
132///
133/// - `serde` or `serde = true`: Automatically derive `Serialize` for Get views and
134///   `Deserialize` for Create and Patch views. Also adds `deny_unknown_fields` and
135///   appropriate field-level serde attributes.
136///   
137///   ```rust,ignore
138///   #[derive(Views)]
139///   #[views(serde)]
140///   struct User { /* ... */ }
141///   ```
142///
143/// # Field Attributes
144///
145/// Each field can be independently configured for each view mode using `#[views(...)]`:
146///
147/// ## Get Mode (`get = "policy"`)
148///
149/// Controls how the field appears in the `{Model}Get` type:
150/// - `"required"` (default): Field is always present with its view type
151/// - `"optional"`: Field is wrapped in `Option<T>`
152/// - `"forbidden"`: Field is excluded from the Get view
153///
154/// ## Create Mode (`create = "policy"`)
155///
156/// Controls how the field appears in the `{Model}Create` type:
157/// - `"required"` (default): Field must be provided during creation
158/// - `"optional"`: Field is wrapped in `Option<T>`, with serde's `default` and
159///   `skip_serializing_if` attributes when serde is enabled
160/// - `"forbidden"`: Field is excluded from the Create view
161///
162/// ## Patch Mode (`patch = "policy"`)
163///
164/// Controls how the field appears in the `{Model}Patch` type:
165/// - `"patch"` (default): Field is wrapped in `Patch<T>`, allowing explicit ignore/update
166/// - `"optional"`: Field is wrapped in `Patch<Option<T>>`
167/// - `"forbidden"`: Field is excluded from the Patch view
168///
169/// # Examples
170///
171/// ## Basic Usage
172///
173/// ```rust,ignore
174/// use model_views::Views;
175///
176/// #[derive(Views)]
177/// struct Article {
178///     // ID only appears in Get view (auto-generated, can't be set)
179///     #[views(get = "required", create = "forbidden", patch = "forbidden")]
180///     id: u64,
181///     
182///     // Title is required everywhere
183///     #[views(get = "required", create = "required", patch = "required")]
184///     title: String,
185///     
186///     // Published status can be patched
187///     #[views(get = "required", create = "optional", patch = "required")]
188///     published: bool,
189/// }
190/// ```
191///
192/// This generates:
193/// - `ArticleGet { id: u64, title: String, published: bool }`
194/// - `ArticleCreate { title: String, published: Option<bool> }`
195/// - `ArticlePatch { title: Patch<String>, published: Patch<bool> }`
196///
197/// ## With Serde Support
198///
199/// ```rust,ignore
200/// #[derive(Views)]
201/// #[views(serde)]
202/// struct User {
203///     #[views(get = "required", create = "forbidden", patch = "forbidden")]
204///     id: u64,
205///     #[views(get = "required", create = "required", patch = "required")]
206///     name: String,
207/// }
208/// ```
209///
210/// The generated types will have appropriate `Serialize`/`Deserialize` derives.
211///
212/// ## Generic Types
213///
214/// The macro supports generic parameters:
215///
216/// ```rust,ignore
217/// #[derive(Views)]
218/// struct Container<T> {
219///     #[views(get = "required", create = "required", patch = "required")]
220///     value: T,
221/// }
222/// ```
223///
224/// # Panics
225///
226/// The macro will panic at compile time if:
227/// - Applied to an enum or union (only structs with named fields are supported)
228/// - An unknown policy value is used (e.g., `get = "invalid"`)
229/// - The `crate` attribute contains an invalid path
230///
231/// # Implementation Details
232///
233/// - View types only include fields that have at least one non-forbidden policy
234/// - If all fields are forbidden for a view mode, that view type is still generated
235///   (as an empty struct)
236/// - Generated types preserve the original struct's visibility and generic parameters
237/// - Non-`#[views(...)]` attributes from the original struct are copied to generated types
238/// - When serde is enabled, optional create fields get `#[serde(default, skip_serializing_if = "Option::is_none")]`
239#[proc_macro_derive(Views, attributes(views, view))]
240#[allow(clippy::missing_panics_doc,clippy::cognitive_complexity,clippy::too_many_lines)]
241pub fn derive_views(input: TokenStream) -> TokenStream {
242    let input = parse_macro_input!(input as DeriveInput);
243    let meta = ViewsInput::from_derive_input(&input).expect("parse #[derive(Views)]");
244
245    let crate_path: syn::Path = if let Some(s) = &meta.crate_ {
246        syn::parse_str(s).expect("valid path in #[views(crate = \"...\")]")
247    } else {
248        syn::parse_str(BASE_CRATE).unwrap()
249    };
250
251    let with_serde = meta.serde.unwrap_or(false);
252
253    let name = &meta.ident;
254    let (impl_generics, ty_generics, where_clause) = meta.generics.split_for_impl();
255
256    let create_ident = format_ident!("{name}Create");
257    let read_ident = format_ident!("{name}Get");
258    let patch_ident = format_ident!("{name}Patch");
259
260    let mut create_fields = Vec::new();
261    let mut read_fields = Vec::new();
262    let mut patch_fields = Vec::new();
263
264    // Track whether a given mode actually has any fields
265    let mut has_get = false;
266    let mut has_create = false;
267    let mut has_patch = false;
268
269    let mv_view = quote!(#crate_path::View);
270    let mv_get = quote!(#crate_path::ViewModeGet);
271    let mv_create = quote!(#crate_path::ViewModeCreate);
272    let mv_patch = quote!(#crate_path::ViewModePatch);
273    let mv_patch_t = quote!(#crate_path::Patch);
274
275    if let darling::ast::Data::Struct(ds) = &meta.data {
276        for f in &ds.fields {
277            let ident = f.ident.clone().expect("named fields only");
278            let fty = &f.ty;
279
280            // policies with defaults
281            let get_p = f.get.as_deref().unwrap_or("required");
282            let crt_p = f.create.as_deref().unwrap_or("required");
283            let patch_p = f.patch.as_deref().unwrap_or("required");
284
285            // ---- GET / READ ----
286            match get_p {
287                "required" => {
288                    has_get = true;
289                    read_fields.push(quote! { pub #ident: <#fty as #mv_view<#mv_get>>::Type, });
290                }
291                "optional" => {
292                    has_get = true;
293                    read_fields.push(quote! {
294                        pub #ident: ::core::option::Option<<#fty as #mv_view<#mv_get>>::Type>,
295                    });
296                }
297                "forbidden" => {}
298                other => panic!("unknown get policy: {other}"),
299            }
300
301            // ---- CREATE ----
302            match crt_p {
303                "required" => {
304                    has_create = true;
305                    create_fields.push(quote! {
306                        pub #ident: <#fty as #mv_view<#mv_create>>::Type,
307                    });
308                }
309                "optional" => {
310                    has_create = true;
311                    if with_serde {
312                        create_fields.push(quote! {
313                            #[serde(default, skip_serializing_if = "Option::is_none")]
314                        });
315                    }
316                    create_fields.push(quote! {
317                        pub #ident: ::core::option::Option<<#fty as #mv_view<#mv_create>>::Type>,
318                    });
319                }
320                "forbidden" => {}
321                other => panic!("unknown create policy: {other}"),
322            }
323
324            // ---- PATCH ----
325            match patch_p {
326                "required" => {
327                    has_patch = true;
328                    patch_fields.push(quote! {
329                        pub #ident: #mv_patch_t<<#fty as #mv_view<#mv_patch>>::Type>,
330                    });
331                }
332                "optional" => {
333                    has_patch = true;
334                    patch_fields.push(quote! {
335                        pub #ident: #mv_patch_t<::core::option::Option<<#fty as #mv_view<#mv_patch>>::Type>>,
336                    });
337                }
338                "forbidden" => {}
339                other => panic!("unknown patch policy: {other}"),
340            }
341        }
342    } else {
343        panic!("#[derive(Views)] supports struct with named fields only");
344    }
345
346    // pull locals for quote!
347    let vis = &meta.vis;
348    let struct_attrs: Vec<_> = input
349        .attrs
350        .iter()
351        .filter(|attr| !attr.path().is_ident("views"))
352        .collect();
353    let create_ident = &create_ident;
354    let read_ident = &read_ident;
355    let patch_ident = &patch_ident;
356
357    let create_fields_ts = &create_fields;
358    let read_fields_ts = &read_fields;
359    let patch_fields_ts = &patch_fields;
360
361    // Build items conditionally
362    let mut items = Vec::<proc_macro2::TokenStream>::new();
363
364    let serialize_attrs = if with_serde {
365        quote! {
366            #[derive(::serde::Serialize)]
367            #[serde(deny_unknown_fields)]
368        }
369    } else {
370        quote! {}
371    };
372
373    let deserialize_attrs = if with_serde {
374        quote! {
375            #[derive(::serde::Deserialize)]
376            #[serde(deny_unknown_fields)]
377        }
378    } else {
379        quote! {}
380    };
381
382    if has_create {
383        items.push(quote! {
384            #deserialize_attrs
385            #(#struct_attrs)*
386            #vis struct #create_ident #ty_generics
387            #where_clause
388            {
389                #(#create_fields_ts)*
390            }
391
392            impl #impl_generics #mv_view<#mv_create> for #name #ty_generics #where_clause {
393                type Type = #create_ident #ty_generics;
394            }
395        });
396    }
397
398    if has_get {
399        items.push(quote! {
400            #serialize_attrs
401            #(#struct_attrs)*
402            #vis struct #read_ident #ty_generics
403            #where_clause
404            {
405                #(#read_fields_ts)*
406            }
407
408            impl #impl_generics #mv_view<#mv_get> for #name #ty_generics #where_clause {
409                type Type = #read_ident #ty_generics;
410            }
411        });
412    }
413
414    if has_patch {
415        items.push(quote! {
416            #[derive(::core::default::Default)]
417            #deserialize_attrs
418            #(#struct_attrs)*
419            #vis struct #patch_ident #ty_generics
420            #where_clause
421            {
422                #(#patch_fields_ts)*
423            }
424
425            impl #impl_generics #mv_view<#mv_patch> for #name #ty_generics #where_clause {
426                type Type = #patch_ident #ty_generics;
427            }
428        });
429    }
430
431    let out = quote! { #(#items)* };
432    out.into()
433}