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}