Skip to main content

kui_derive/
lib.rs

1//! `#[derive(Message)]` (backlog C50): an enum or a struct to and from the
2//! `{kind, …fields}` map a kui payload is, so a Rust app writes
3//! `on_click(Msg::Save)` and matches `ev.message::<Msg>()` where it would
4//! otherwise build and pick apart `Value`s by string.
5//!
6//! What it generates, for a type `Msg`:
7//!
8//! - `From<Msg> for Value`: a map whose `kind` is the variant's name in
9//!   snake_case (`TabNew` is `"tab_new"`, a struct's own name for a
10//!   struct) and whose other keys are the fields — a tuple variant's by
11//!   position, `"0"`, `"1"`, ….
12//! - `TryFrom<&Value>` and `TryFrom<Value> for Msg`, with
13//!   `kui_native::MessageError` saying what did not fit.
14//! - `kui_native::MessageField for Msg`, so a message can be a field of another.
15//!
16//! Attributes, all under `#[message(…)]`:
17//!
18//! - on a variant or a struct, `kind = "…"` names its `kind` instead;
19//! - on an enum of unit variants only, `string` makes it a bare string
20//!   (`"h"`) rather than a map (`{kind: "h"}`) — the shape a field such as
21//!   a split's direction reads best as;
22//! - on the type, `crate = "…"` names the path the generated code reaches
23//!   kui through: `::kui` unless said, `kui_core` for a crate that depends
24//!   on kui-core alone.
25
26use proc_macro::TokenStream;
27use proc_macro2::{Span, TokenStream as Tokens};
28use quote::{format_ident, quote};
29use syn::{Data, DeriveInput, Fields, LitStr, parse_macro_input, spanned::Spanned};
30
31#[proc_macro_derive(Message, attributes(message))]
32pub fn derive_message(input: TokenStream) -> TokenStream {
33    let input = parse_macro_input!(input as DeriveInput);
34    match expand(&input) {
35        Ok(t) => t.into(),
36        Err(e) => e.to_compile_error().into(),
37    }
38}
39
40/// What `#[message(…)]` said on one item.
41#[derive(Default)]
42struct Attrs {
43    kind: Option<String>,
44    krate: Option<syn::Path>,
45    string: bool,
46}
47
48fn attrs(list: &[syn::Attribute]) -> syn::Result<Attrs> {
49    let mut out = Attrs::default();
50    for a in list.iter().filter(|a| a.path().is_ident("message")) {
51        a.parse_nested_meta(|m| {
52            if m.path.is_ident("kind") {
53                out.kind = Some(m.value()?.parse::<LitStr>()?.value());
54            } else if m.path.is_ident("crate") {
55                out.krate = Some(m.value()?.parse::<LitStr>()?.parse()?);
56            } else if m.path.is_ident("string") {
57                out.string = true;
58            } else {
59                return Err(m.error("expected `kind = \"…\"`, `crate = \"…\"` or `string`"));
60            }
61            Ok(())
62        })?;
63    }
64    Ok(out)
65}
66
67/// `TabNew` → `tab_new`, `HTTPGet` → `http_get`.
68fn snake(name: &str) -> String {
69    let chars: Vec<char> = name.chars().collect();
70    let mut out = String::new();
71    for (i, &c) in chars.iter().enumerate() {
72        if c.is_uppercase() {
73            let prev_lower = i > 0 && !chars[i - 1].is_uppercase() && chars[i - 1] != '_';
74            let next_lower = chars.get(i + 1).is_some_and(|n| n.is_lowercase());
75            let prev_upper = i > 0 && chars[i - 1].is_uppercase();
76            if i > 0 && (prev_lower || (prev_upper && next_lower)) {
77                out.push('_');
78            }
79            out.extend(c.to_lowercase());
80        } else {
81            out.push(c);
82        }
83    }
84    out
85}
86
87/// One shape a message takes: its kind, the constructor path, and its
88/// fields as (key, binding, type).
89struct Shape {
90    kind: String,
91    path: Tokens,
92    fields: Vec<(String, syn::Ident, syn::Type)>,
93    named: Option<bool>,
94}
95
96fn shape(kind: String, path: Tokens, fields: &Fields) -> Shape {
97    let (named, fields) = match fields {
98        Fields::Unit => (None, Vec::new()),
99        Fields::Named(f) => (
100            Some(true),
101            f.named
102                .iter()
103                .map(|f| {
104                    let id = f.ident.clone().expect("named");
105                    (id.to_string(), id, f.ty.clone())
106                })
107                .collect(),
108        ),
109        Fields::Unnamed(f) => (
110            Some(false),
111            f.unnamed
112                .iter()
113                .enumerate()
114                .map(|(i, f)| (i.to_string(), format_ident!("f{i}"), f.ty.clone()))
115                .collect(),
116        ),
117    };
118    Shape {
119        kind,
120        path,
121        fields,
122        named,
123    }
124}
125
126impl Shape {
127    /// The pattern binding every field, for the encoding side.
128    fn pattern(&self) -> Tokens {
129        let path = &self.path;
130        let binds = self.fields.iter().map(|(_, b, _)| b);
131        match self.named {
132            None => quote!(#path),
133            Some(true) => quote!(#path { #(#binds),* }),
134            Some(false) => quote!(#path ( #(#binds),* )),
135        }
136    }
137
138    /// The map this shape encodes to.
139    fn encode(&self, k: &Tokens) -> Tokens {
140        let kind = &self.kind;
141        let entries = self
142            .fields
143            .iter()
144            .map(|(key, b, _)| quote!((#key, #k::MessageField::to_value(#b))));
145        quote!(#k::Value::map([("kind", #k::Value::str(#kind)), #(#entries),*]))
146    }
147
148    /// The constructor reading every field from the map `v`.
149    fn decode(&self, k: &Tokens) -> Tokens {
150        let path = &self.path;
151        let kind = &self.kind;
152        let reads = self
153            .fields
154            .iter()
155            .map(|(key, _, ty)| quote!(#k::message::field::<#ty>(v, #kind, #key)?));
156        match self.named {
157            None => quote!(#path),
158            Some(true) => {
159                let names = self.fields.iter().map(|(_, b, _)| b);
160                quote!(#path { #(#names: #reads),* })
161            }
162            Some(false) => quote!(#path ( #(#reads),* )),
163        }
164    }
165}
166
167fn expand(input: &DeriveInput) -> syn::Result<Tokens> {
168    let top = attrs(&input.attrs)?;
169    let k: Tokens = match &top.krate {
170        Some(p) => quote!(#p),
171        None => quote!(::kui_native),
172    };
173    let name = &input.ident;
174    let (imp, ty, wh) = input.generics.split_for_impl();
175
176    let shapes: Vec<Shape> = match &input.data {
177        Data::Enum(e) => {
178            let mut out = Vec::new();
179            for v in &e.variants {
180                let a = attrs(&v.attrs)?;
181                if a.krate.is_some() || a.string {
182                    return Err(syn::Error::new(
183                        v.span(),
184                        "`crate` and `string` go on the type, not on a variant",
185                    ));
186                }
187                let id = &v.ident;
188                out.push(shape(
189                    a.kind.unwrap_or_else(|| snake(&id.to_string())),
190                    quote!(#name::#id),
191                    &v.fields,
192                ));
193            }
194            out
195        }
196        Data::Struct(s) => vec![shape(
197            top.kind.clone().unwrap_or_else(|| snake(&name.to_string())),
198            quote!(#name),
199            &s.fields,
200        )],
201        Data::Union(u) => {
202            return Err(syn::Error::new(
203                u.union_token.span,
204                "a message is an enum or a struct",
205            ));
206        }
207    };
208    if top.kind.is_some() && matches!(input.data, Data::Enum(_)) {
209        return Err(syn::Error::new(
210            Span::call_site(),
211            "`kind` on an enum goes on each variant",
212        ));
213    }
214    {
215        let mut seen = std::collections::HashSet::new();
216        for s in &shapes {
217            if !seen.insert(s.kind.as_str()) {
218                return Err(syn::Error::new(
219                    Span::call_site(),
220                    format!("two shapes of this message are both of kind {:?}", s.kind),
221                ));
222            }
223        }
224    }
225
226    let (encode, decode) = if top.string {
227        if shapes.iter().any(|s| s.named.is_some()) || !matches!(input.data, Data::Enum(_)) {
228            return Err(syn::Error::new(
229                Span::call_site(),
230                "`string` is for an enum whose variants are all units",
231            ));
232        }
233        let pats = shapes.iter().map(Shape::pattern);
234        let kinds: Vec<&String> = shapes.iter().map(|s| &s.kind).collect();
235        let paths = shapes.iter().map(|s| &s.path);
236        (
237            quote!(match m { #(#pats => #k::Value::str(#kinds)),* }),
238            quote!(match v.as_str().ok_or(#k::MessageError::NoKind)? {
239                #(#kinds => Ok(#paths),)*
240                other => Err(#k::MessageError::UnknownKind(other.to_string())),
241            }),
242        )
243    } else {
244        let pats = shapes.iter().map(Shape::pattern);
245        let encodes = shapes.iter().map(|s| s.encode(&k));
246        let kinds = shapes.iter().map(|s| &s.kind);
247        let decodes = shapes.iter().map(|s| s.decode(&k));
248        (
249            quote!(match m { #(#pats => #encodes),* }),
250            quote!(match #k::message::kind_of(v)? {
251                #(#kinds => Ok(#decodes),)*
252                other => Err(#k::MessageError::UnknownKind(other.to_string())),
253            }),
254        )
255    };
256
257    Ok(quote! {
258        impl #imp ::core::convert::From<#name #ty> for #k::Value #wh {
259            fn from(m: #name #ty) -> #k::Value {
260                #encode
261            }
262        }
263
264        impl #imp ::core::convert::TryFrom<&#k::Value> for #name #ty #wh {
265            type Error = #k::MessageError;
266            fn try_from(v: &#k::Value) -> ::core::result::Result<Self, #k::MessageError> {
267                #decode
268            }
269        }
270
271        impl #imp ::core::convert::TryFrom<#k::Value> for #name #ty #wh {
272            type Error = #k::MessageError;
273            fn try_from(v: #k::Value) -> ::core::result::Result<Self, #k::MessageError> {
274                <Self as ::core::convert::TryFrom<&#k::Value>>::try_from(&v)
275            }
276        }
277
278        impl #imp #k::MessageField for #name #ty #wh {
279            fn to_value(self) -> #k::Value {
280                self.into()
281            }
282            fn from_value(v: &#k::Value) -> ::core::option::Option<Self> {
283                <Self as ::core::convert::TryFrom<&#k::Value>>::try_from(v).ok()
284            }
285        }
286    })
287}
288
289#[cfg(test)]
290mod tests {
291    use super::snake;
292
293    #[test]
294    fn variant_names_become_snake_case_kinds() {
295        assert_eq!(snake("Inc"), "inc");
296        assert_eq!(snake("TabNew"), "tab_new");
297        assert_eq!(snake("PaneDrag"), "pane_drag");
298        assert_eq!(snake("HTTPGet"), "http_get");
299        assert_eq!(snake("Add10"), "add10");
300        assert_eq!(snake("H"), "h");
301    }
302}