morphix_derive/lib.rs
1#![allow(rustdoc::broken_intra_doc_links)]
2#![doc = include_str!("../README.md")]
3
4use proc_macro::TokenStream;
5
6mod derive;
7mod observe;
8
9/// Derive the [`Observe`](morphix::Observe) trait to enable mutation tracking.
10///
11/// This macro automatically generates an [`Observe`](morphix::Observe) implementation, producing a
12/// default [`Observer`](morphix::observe::Observer) type that wraps the struct and tracks mutations
13/// to each field according to that field's own [`Observe`](morphix::Observe) implementation.
14///
15/// ## Requirements
16///
17/// - The struct must also derive or implement [`Serialize`](serde::Serialize)
18/// - Only named structs are supported (not tuple structs or enums)
19///
20/// ## Customizing Behavior
21///
22/// If a field type `T` does not implement [`Observe`](morphix::Observe), or you need an alternative
23/// observer implementation, you can customize this via the `#[morphix(...)]` field attribute inside
24/// a `#[derive(Observe)]` struct:
25///
26/// - `#[morphix(noop)]` — use [`NoopObserver`](morphix::observe::NoopObserver) for this field
27/// - `#[morphix(shallow)]` — use [`ShallowObserver`](morphix::observe::ShallowObserver) for this
28/// field
29/// - `#[morphix(snapshot)]` — use [`SnapshotObserver`](morphix::observe::SnapshotObserver) for this
30/// field
31///
32/// These attributes allow you to override the default [`Observer`](morphix::observe::Observer) type
33/// that would otherwise come from the field's [`Observe`](morphix::Observe) implementation.
34///
35/// ## Example
36///
37/// ```
38/// use serde::Serialize;
39/// use morphix::Observe;
40///
41/// #[derive(Serialize, Observe)]
42/// struct User {
43/// name: String, // StringObserver
44/// age: i32, // SnapshotObserver<i32>
45///
46/// #[morphix(noop)]
47/// cache: String, // Not tracked
48///
49/// #[morphix(shallow)]
50/// metadata: Metadata, // ShallowObserver<Metadata>
51/// }
52///
53/// #[derive(Serialize)]
54/// struct Metadata {
55/// created_at: String,
56/// updated_at: String,
57/// }
58/// ```
59#[proc_macro_derive(Observe, attributes(morphix))]
60pub fn derive_observe(input: TokenStream) -> TokenStream {
61 let input: syn::DeriveInput = syn::parse_macro_input!(input);
62 derive::derive_observe(input).into()
63}
64
65/// Observe and collect mutations within a closure.
66///
67/// This macro wraps a closure's operations to track all mutations that occur within it. The closure
68/// receives a mutable reference to the value, and any mutations made are automatically collected
69/// and returned.
70///
71/// ## Syntax
72///
73/// ```
74/// # use morphix::adapter::Json;
75/// # use morphix::observe;
76/// # let mut binding = String::new();
77/// # let Json(mutation) =
78/// observe!(binding => { /* mutations */ }).unwrap();
79/// # let f: &dyn FnOnce(&mut String) -> Result<Json, serde_json::Error> = &
80/// observe!(|binding: &mut String| { /* mutations */ });
81/// ```
82///
83/// ## Example
84///
85/// ```
86/// use serde::Serialize;
87/// use morphix::adapter::Json;
88/// use morphix::{Observe, observe};
89///
90/// #[derive(Serialize, Observe)]
91/// struct Point {
92/// x: f64,
93/// y: f64,
94/// }
95///
96/// let mut point = Point { x: 1.0, y: 2.0 };
97///
98/// let Json(mutation) = observe!(point => {
99/// point.x += 1.0;
100/// point.y *= 2.0;
101/// }).unwrap();
102///
103/// assert_eq!(point.x, 2.0);
104/// assert_eq!(point.y, 4.0);
105/// ```
106#[proc_macro]
107pub fn observe(input: TokenStream) -> TokenStream {
108 let input: observe::ObserveInput = syn::parse_macro_input!(input);
109 observe::observe(input).into()
110}
111
112#[cfg(test)]
113mod test {
114 use std::env::var;
115 use std::fs::{create_dir_all, read_to_string, write};
116 use std::path::{Path, PathBuf};
117
118 use macro_expand::Context;
119 use pretty_assertions::StrComparison;
120 use prettyplease::unparse;
121 use walkdir::WalkDir;
122
123 struct TestDiff {
124 path: PathBuf,
125 expect: String,
126 actual: String,
127 }
128
129 #[test]
130 fn fixtures() {
131 let input_dir = "fixtures/input";
132 let output_dir = "fixtures/output";
133 let mut diffs = vec![];
134 let will_emit = var("EMIT").is_ok_and(|v| !v.is_empty());
135 for entry in WalkDir::new(input_dir).into_iter().filter_map(Result::ok) {
136 let input_path = entry.path();
137 if !input_path.is_file() || input_path.extension() != Some("rs".as_ref()) {
138 continue;
139 }
140 let path = input_path.strip_prefix(input_dir).unwrap();
141 let output_path = Path::new(output_dir).join(path);
142 let input = read_to_string(input_path).unwrap().parse().unwrap();
143 let mut ctx = Context::new();
144 ctx.register_proc_macro_derive("Observe".into(), crate::derive::derive_observe, vec!["morphix".into()]);
145 let actual = unparse(&syn::parse2(ctx.transform(input)).unwrap());
146 let expect_result = read_to_string(&output_path);
147 if let Ok(expect) = &expect_result
148 && expect == &actual
149 {
150 continue;
151 }
152 if will_emit {
153 create_dir_all(output_path.parent().unwrap()).unwrap();
154 write(output_path, &actual).unwrap();
155 }
156 if let Ok(expect) = expect_result {
157 diffs.push(TestDiff {
158 path: path.to_path_buf(),
159 expect,
160 actual,
161 });
162 }
163 }
164 let len = diffs.len();
165 for diff in diffs {
166 eprintln!("diff {}", diff.path.display());
167 eprintln!("{}", StrComparison::new(&diff.expect, &diff.actual));
168 }
169 if len > 0 && !will_emit {
170 panic!("Some tests failed");
171 }
172 }
173}