Skip to main content

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}