Skip to main content

kui_derive/
lib.rs

1//! `#[derive(Message)]`: a Rust enum or struct to and from the plain-data
2//! payload a kui message is.
3//!
4//! kui carries every message as a `Value` map shaped `{kind, ...fields}`,
5//! so one view model serves Rust, Lua, C and Node alike. This derive lets a
6//! Rust app keep a typed enum instead of building and picking apart those
7//! maps by string: `widgets::button(ui, "Save", Msg::Save)` sends it and
8//! `ev.message::<Msg>()` reads it back. Most apps get the derive through
9//! kui-native (`use kui_native::Message`; cargo feature `derive`, on by
10//! default) and never name this crate.
11//!
12//! # Example
13//!
14//! The generated code reaches kui through `kui_native`, which this crate
15//! does not depend on, so the example is not compiled here.
16//!
17//! ```rust,ignore
18//! use kui_native::widgets;
19//! use kui_native::{App, Message, NodeSpec, Ui, UiEvent};
20//!
21//! #[derive(Message, Clone, Debug, PartialEq)]
22//! enum Msg {
23//!     Inc,                          // {kind: "inc"}
24//!     Pick { id: u64 },             // {kind: "pick", id: 3}
25//!     #[message(kind = "add10")]
26//!     AddTen,                       // {kind: "add10"}
27//! }
28//!
29//! #[derive(Default)]
30//! struct Counter {
31//!     count: i64,
32//! }
33//!
34//! impl App for Counter {
35//!     fn view(&mut self, ui: &mut Ui<'_>) {
36//!         ui.with(NodeSpec::row().gap(8.0), |ui| {
37//!             widgets::button(ui, "+1", Msg::Inc);
38//!             widgets::button(ui, "+10", Msg::AddTen);
39//!         });
40//!     }
41//!
42//!     fn on_event(&mut self, ev: UiEvent) {
43//!         match ev.message::<Msg>() {
44//!             Some(Msg::Inc) => self.count += 1,
45//!             Some(Msg::AddTen) => self.count += 10,
46//!             Some(Msg::Pick { id }) => println!("picked {id}"),
47//!             None => {} // not a `Msg`: a resize, a focus change
48//!         }
49//!     }
50//! }
51//! ```
52//!
53//! # What it generates
54//!
55//! For a type `Msg`:
56//!
57//! - `From<Msg> for Value`: a map whose `kind` is the variant's name in
58//!   snake_case (`TabNew` is `"tab_new"`; a struct uses its own name) and
59//!   whose other keys are the fields, a tuple variant's by position
60//!   (`"0"`, `"1"`, ...).
61//! - `TryFrom<&Value>` and `TryFrom<Value> for Msg`, with `MessageError`
62//!   saying what did not fit.
63//! - `MessageField for Msg`, so a message can be a field of another.
64//!
65//! A field is anything that implements `MessageField`: `bool`, the
66//! numbers, `String`, `Option<T>`, `Vec<T>`, `Box<T>`, `Value` and other
67//! messages.
68//!
69//! # Attributes
70//!
71//! All under `#[message(...)]`:
72//!
73//! - on a variant or a struct, `kind = "..."` names its `kind` instead of
74//!   the snake_case name;
75//! - on an enum of unit variants only, `string` makes it a bare string
76//!   (`"h"`) rather than a map (`{kind: "h"}`), which reads best for a
77//!   field such as a split's direction;
78//! - on the type, `crate = "..."` names the path the generated code
79//!   reaches kui through: `::kui_native` unless said, `kui_core` for a
80//!   crate that depends on kui-core alone.
81//!
82//! ```rust,ignore
83//! #[derive(Message, Clone, Copy, Debug, PartialEq)]
84//! #[message(string)]
85//! enum Dir { H, V }                 // "h" / "v"
86//!
87//! #[derive(Message, Clone, Debug, PartialEq)]
88//! #[message(crate = "kui_core")]
89//! struct Resize { w: f64, h: f64 }  // {kind: "resize", w, h}
90//! ```
91//!
92//! The book: <https://kui-book.qxuken.dev>. Repository:
93//! <https://github.com/qxuken/kui>.
94
95use proc_macro::TokenStream;
96use proc_macro2::{Span, TokenStream as Tokens};
97use quote::{format_ident, quote};
98use syn::{Data, DeriveInput, Fields, LitStr, parse_macro_input, spanned::Spanned};
99
100/// Derives `From<Self> for Value`, `TryFrom<&Value>`, `TryFrom<Value>` and
101/// `MessageField` for an enum or a struct; see the [crate docs](crate).
102#[proc_macro_derive(Message, attributes(message))]
103pub fn derive_message(input: TokenStream) -> TokenStream {
104    let input = parse_macro_input!(input as DeriveInput);
105    match expand(&input) {
106        Ok(t) => t.into(),
107        Err(e) => e.to_compile_error().into(),
108    }
109}
110
111/// What `#[message(…)]` said on one item.
112#[derive(Default)]
113struct Attrs {
114    kind: Option<String>,
115    krate: Option<syn::Path>,
116    string: bool,
117}
118
119fn attrs(list: &[syn::Attribute]) -> syn::Result<Attrs> {
120    let mut out = Attrs::default();
121    for a in list.iter().filter(|a| a.path().is_ident("message")) {
122        a.parse_nested_meta(|m| {
123            if m.path.is_ident("kind") {
124                out.kind = Some(m.value()?.parse::<LitStr>()?.value());
125            } else if m.path.is_ident("crate") {
126                out.krate = Some(m.value()?.parse::<LitStr>()?.parse()?);
127            } else if m.path.is_ident("string") {
128                out.string = true;
129            } else {
130                return Err(m.error("expected `kind = \"…\"`, `crate = \"…\"` or `string`"));
131            }
132            Ok(())
133        })?;
134    }
135    Ok(out)
136}
137
138/// `TabNew` → `tab_new`, `HTTPGet` → `http_get`.
139fn snake(name: &str) -> String {
140    let chars: Vec<char> = name.chars().collect();
141    let mut out = String::new();
142    for (i, &c) in chars.iter().enumerate() {
143        if c.is_uppercase() {
144            let prev_lower = i > 0 && !chars[i - 1].is_uppercase() && chars[i - 1] != '_';
145            let next_lower = chars.get(i + 1).is_some_and(|n| n.is_lowercase());
146            let prev_upper = i > 0 && chars[i - 1].is_uppercase();
147            if i > 0 && (prev_lower || (prev_upper && next_lower)) {
148                out.push('_');
149            }
150            out.extend(c.to_lowercase());
151        } else {
152            out.push(c);
153        }
154    }
155    out
156}
157
158/// One shape a message takes: its kind, the constructor path, and its
159/// fields as (key, binding, type).
160struct Shape {
161    kind: String,
162    path: Tokens,
163    fields: Vec<(String, syn::Ident, syn::Type)>,
164    named: Option<bool>,
165}
166
167fn shape(kind: String, path: Tokens, fields: &Fields) -> Shape {
168    let (named, fields) = match fields {
169        Fields::Unit => (None, Vec::new()),
170        Fields::Named(f) => (
171            Some(true),
172            f.named
173                .iter()
174                .map(|f| {
175                    let id = f.ident.clone().expect("named");
176                    (id.to_string(), id, f.ty.clone())
177                })
178                .collect(),
179        ),
180        Fields::Unnamed(f) => (
181            Some(false),
182            f.unnamed
183                .iter()
184                .enumerate()
185                .map(|(i, f)| (i.to_string(), format_ident!("f{i}"), f.ty.clone()))
186                .collect(),
187        ),
188    };
189    Shape {
190        kind,
191        path,
192        fields,
193        named,
194    }
195}
196
197impl Shape {
198    /// The pattern binding every field, for the encoding side.
199    fn pattern(&self) -> Tokens {
200        let path = &self.path;
201        let binds = self.fields.iter().map(|(_, b, _)| b);
202        match self.named {
203            None => quote!(#path),
204            Some(true) => quote!(#path { #(#binds),* }),
205            Some(false) => quote!(#path ( #(#binds),* )),
206        }
207    }
208
209    /// The map this shape encodes to.
210    fn encode(&self, k: &Tokens) -> Tokens {
211        let kind = &self.kind;
212        let entries = self
213            .fields
214            .iter()
215            .map(|(key, b, _)| quote!((#key, #k::MessageField::to_value(#b))));
216        quote!(#k::Value::map([("kind", #k::Value::str(#kind)), #(#entries),*]))
217    }
218
219    /// The constructor reading every field from the map `v`.
220    fn decode(&self, k: &Tokens) -> Tokens {
221        let path = &self.path;
222        let kind = &self.kind;
223        let reads = self
224            .fields
225            .iter()
226            .map(|(key, _, ty)| quote!(#k::message::field::<#ty>(v, #kind, #key)?));
227        match self.named {
228            None => quote!(#path),
229            Some(true) => {
230                let names = self.fields.iter().map(|(_, b, _)| b);
231                quote!(#path { #(#names: #reads),* })
232            }
233            Some(false) => quote!(#path ( #(#reads),* )),
234        }
235    }
236}
237
238fn expand(input: &DeriveInput) -> syn::Result<Tokens> {
239    let top = attrs(&input.attrs)?;
240    let k: Tokens = match &top.krate {
241        Some(p) => quote!(#p),
242        None => quote!(::kui_native),
243    };
244    let name = &input.ident;
245    let (imp, ty, wh) = input.generics.split_for_impl();
246
247    let shapes: Vec<Shape> = match &input.data {
248        Data::Enum(e) => {
249            let mut out = Vec::new();
250            for v in &e.variants {
251                let a = attrs(&v.attrs)?;
252                if a.krate.is_some() || a.string {
253                    return Err(syn::Error::new(
254                        v.span(),
255                        "`crate` and `string` go on the type, not on a variant",
256                    ));
257                }
258                let id = &v.ident;
259                out.push(shape(
260                    a.kind.unwrap_or_else(|| snake(&id.to_string())),
261                    quote!(#name::#id),
262                    &v.fields,
263                ));
264            }
265            out
266        }
267        Data::Struct(s) => vec![shape(
268            top.kind.clone().unwrap_or_else(|| snake(&name.to_string())),
269            quote!(#name),
270            &s.fields,
271        )],
272        Data::Union(u) => {
273            return Err(syn::Error::new(
274                u.union_token.span,
275                "a message is an enum or a struct",
276            ));
277        }
278    };
279    if top.kind.is_some() && matches!(input.data, Data::Enum(_)) {
280        return Err(syn::Error::new(
281            Span::call_site(),
282            "`kind` on an enum goes on each variant",
283        ));
284    }
285    {
286        let mut seen = std::collections::HashSet::new();
287        for s in &shapes {
288            if !seen.insert(s.kind.as_str()) {
289                return Err(syn::Error::new(
290                    Span::call_site(),
291                    format!("two shapes of this message are both of kind {:?}", s.kind),
292                ));
293            }
294        }
295    }
296
297    let (encode, decode) = if top.string {
298        if shapes.iter().any(|s| s.named.is_some()) || !matches!(input.data, Data::Enum(_)) {
299            return Err(syn::Error::new(
300                Span::call_site(),
301                "`string` is for an enum whose variants are all units",
302            ));
303        }
304        let pats = shapes.iter().map(Shape::pattern);
305        let kinds: Vec<&String> = shapes.iter().map(|s| &s.kind).collect();
306        let paths = shapes.iter().map(|s| &s.path);
307        (
308            quote!(match m { #(#pats => #k::Value::str(#kinds)),* }),
309            quote!(match v.as_str().ok_or(#k::MessageError::NoKind)? {
310                #(#kinds => Ok(#paths),)*
311                other => Err(#k::MessageError::UnknownKind(other.to_string())),
312            }),
313        )
314    } else {
315        let pats = shapes.iter().map(Shape::pattern);
316        let encodes = shapes.iter().map(|s| s.encode(&k));
317        let kinds = shapes.iter().map(|s| &s.kind);
318        let decodes = shapes.iter().map(|s| s.decode(&k));
319        (
320            quote!(match m { #(#pats => #encodes),* }),
321            quote!(match #k::message::kind_of(v)? {
322                #(#kinds => Ok(#decodes),)*
323                other => Err(#k::MessageError::UnknownKind(other.to_string())),
324            }),
325        )
326    };
327
328    Ok(quote! {
329        impl #imp ::core::convert::From<#name #ty> for #k::Value #wh {
330            fn from(m: #name #ty) -> #k::Value {
331                #encode
332            }
333        }
334
335        impl #imp ::core::convert::TryFrom<&#k::Value> for #name #ty #wh {
336            type Error = #k::MessageError;
337            fn try_from(v: &#k::Value) -> ::core::result::Result<Self, #k::MessageError> {
338                #decode
339            }
340        }
341
342        impl #imp ::core::convert::TryFrom<#k::Value> for #name #ty #wh {
343            type Error = #k::MessageError;
344            fn try_from(v: #k::Value) -> ::core::result::Result<Self, #k::MessageError> {
345                <Self as ::core::convert::TryFrom<&#k::Value>>::try_from(&v)
346            }
347        }
348
349        impl #imp #k::MessageField for #name #ty #wh {
350            fn to_value(self) -> #k::Value {
351                self.into()
352            }
353            fn from_value(v: &#k::Value) -> ::core::option::Option<Self> {
354                <Self as ::core::convert::TryFrom<&#k::Value>>::try_from(v).ok()
355            }
356        }
357    })
358}
359
360#[cfg(test)]
361mod tests {
362    use super::snake;
363
364    #[test]
365    fn variant_names_become_snake_case_kinds() {
366        assert_eq!(snake("Inc"), "inc");
367        assert_eq!(snake("TabNew"), "tab_new");
368        assert_eq!(snake("PaneDrag"), "pane_drag");
369        assert_eq!(snake("HTTPGet"), "http_get");
370        assert_eq!(snake("Add10"), "add10");
371        assert_eq!(snake("H"), "h");
372    }
373}