muon_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`](muon::Observe) trait to enable mutation tracking.
10///
11/// This macro automatically generates an [`Observe`](muon::Observe) implementation, producing a
12/// default [`Observer`](muon::observe::Observer) type that wraps the struct and tracks mutations
13/// to each field according to that field's own [`Observe`](muon::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`](muon::Observe), or you need an alternative
23/// observer implementation, you can customize this via the `#[muon(...)]` field attribute inside
24/// a `#[derive(Observe)]` struct:
25///
26/// - `#[muon(noop)]` — use [`NoopObserver`](muon::observe::NoopObserver) for this field
27/// - `#[muon(shallow)]` — use [`ShallowObserver`](muon::observe::ShallowObserver) for this field
28/// - `#[muon(snapshot)]` — use [`SnapshotObserver`](muon::observe::SnapshotObserver) for this field
29///
30/// These attributes allow you to override the default [`Observer`](muon::observe::Observer) type
31/// that would otherwise come from the field's [`Observe`](muon::Observe) implementation.
32///
33/// ## Example
34///
35/// ```
36/// use serde::Serialize;
37/// use muon::Observe;
38///
39/// #[derive(Serialize, Observe)]
40/// struct User {
41/// name: String, // StringObserver
42/// age: i32, // SnapshotObserver<i32>
43///
44/// #[muon(noop)]
45/// cache: String, // Not tracked
46///
47/// #[muon(shallow)]
48/// metadata: Metadata, // ShallowObserver<Metadata>
49/// }
50///
51/// #[derive(Serialize)]
52/// struct Metadata {
53/// created_at: String,
54/// updated_at: String,
55/// }
56/// ```
57#[proc_macro_derive(Observe, attributes(muon))]
58pub fn derive_observe(input: TokenStream) -> TokenStream {
59 let input: syn::DeriveInput = syn::parse_macro_input!(input);
60 derive::derive_observe(input).into()
61}
62
63/// Observe and collect mutations within a closure.
64///
65/// This macro wraps a closure's operations to track all mutations that occur within it. The closure
66/// receives a mutable reference to the value, and any mutations made are automatically collected
67/// and returned.
68///
69/// ## Syntax
70///
71/// ```
72/// # use muon::adapter::Json;
73/// # use muon::observe;
74/// # let mut binding = String::new();
75/// # let Json(mutation) =
76/// observe!(binding => { /* mutations */ }).unwrap();
77/// # let f: &dyn FnOnce(&mut String) -> Result<Json, serde_json::Error> = &
78/// observe!(|binding: &mut String| { /* mutations */ });
79/// ```
80///
81/// ## Example
82///
83/// ```
84/// use serde::Serialize;
85/// use muon::adapter::Json;
86/// use muon::{Observe, observe};
87///
88/// #[derive(Serialize, Observe)]
89/// struct Point {
90/// x: f64,
91/// y: f64,
92/// }
93///
94/// let mut point = Point { x: 1.0, y: 2.0 };
95///
96/// let Json(mutation) = observe!(point => {
97/// point.x += 1.0;
98/// point.y *= 2.0;
99/// }).unwrap();
100///
101/// assert_eq!(point.x, 2.0);
102/// assert_eq!(point.y, 4.0);
103/// ```
104#[proc_macro]
105pub fn observe(input: TokenStream) -> TokenStream {
106 let input: observe::ObserveInput = syn::parse_macro_input!(input);
107 observe::observe(input).into()
108}
109
110#[cfg(test)]
111mod test {
112 use std::env::var;
113 use std::fs::{create_dir_all, read_to_string, write};
114 use std::path::{Path, PathBuf};
115
116 use macro_expand::Context;
117 use pretty_assertions::StrComparison;
118 use prettyplease::unparse;
119 use walkdir::WalkDir;
120
121 struct TestDiff {
122 path: PathBuf,
123 expect: String,
124 actual: String,
125 }
126
127 #[test]
128 fn fixtures() {
129 let input_dir = "fixtures/input";
130 let output_dir = "fixtures/output";
131 let mut diffs = vec![];
132 let will_emit = var("EMIT").is_ok_and(|v| !v.is_empty());
133 for entry in WalkDir::new(input_dir).into_iter().filter_map(Result::ok) {
134 let input_path = entry.path();
135 if !input_path.is_file() || input_path.extension() != Some("rs".as_ref()) {
136 continue;
137 }
138 let path = input_path.strip_prefix(input_dir).unwrap();
139 let output_path = Path::new(output_dir).join(path);
140 let input = read_to_string(input_path).unwrap().parse().unwrap();
141 let mut ctx = Context::new();
142 ctx.module("muon")
143 .proc_macro("observe", crate::observe::observe)
144 .proc_macro_derive("Observe", crate::derive::derive_observe, vec!["muon".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}