Skip to main content

yog_data_derive/
lib.rs

1//! `#[derive(Schema)]` — see `yog_data::Schema`.
2//!
3//! - A struct with named fields is an object. Field attributes:
4//!   `#[data(rename = "name")]`, `#[data(default)]` /
5//!   `#[data(default = "path::to_fn")]`, `#[data(skip)]` (never read or
6//!   written; `Default`). `Option` fields may be left out.
7//! - A struct with one unnamed field is that field's format.
8//! - An enum of unit variants is a string: the variant's name in
9//!   snake_case, or `#[data(rename = "...")]`.
10//! - `#[data(tag = "type")]` on an enum: an object whose `type` names the
11//!   variant; a variant is a unit, has named fields, or wraps one struct
12//!   whose fields sit next to the tag.
13//!
14//! Doc comments become the shape's docs (Yog-IDLE's hover text and the
15//! docs' field tables).
16
17use proc_macro::TokenStream;
18use proc_macro2::TokenStream as Tokens;
19use quote::quote;
20use syn::{Data, DeriveInput, Fields, Ident, LitStr, Type, parse_macro_input};
21
22#[proc_macro_derive(Schema, attributes(data))]
23pub fn derive_schema(input: TokenStream) -> TokenStream {
24    let input = parse_macro_input!(input as DeriveInput);
25    match expand(&input) {
26        Ok(t) => t.into(),
27        Err(e) => e.to_compile_error().into(),
28    }
29}
30
31#[derive(Default)]
32struct Attrs {
33    rename: Option<String>,
34    default: Option<Option<syn::Path>>,
35    skip: bool,
36    tag: Option<String>,
37    with: Option<Type>,
38    range: Option<(f64, f64)>,
39    /// A tagged enum's variant for every tag the others don't take.
40    other: bool,
41}
42
43fn attrs(list: &[syn::Attribute]) -> syn::Result<Attrs> {
44    let mut a = Attrs::default();
45    for attr in list.iter().filter(|a| a.path().is_ident("data")) {
46        attr.parse_nested_meta(|meta| {
47            if meta.path.is_ident("rename") {
48                a.rename = Some(meta.value()?.parse::<LitStr>()?.value());
49            } else if meta.path.is_ident("tag") {
50                a.tag = Some(meta.value()?.parse::<LitStr>()?.value());
51            } else if meta.path.is_ident("default") {
52                a.default = Some(if meta.input.peek(syn::Token![=]) {
53                    Some(meta.value()?.parse::<LitStr>()?.parse()?)
54                } else {
55                    None
56                });
57            } else if meta.path.is_ident("range") {
58                let lit = meta.value()?.parse::<LitStr>()?;
59                let text = lit.value();
60                let parsed = text.split_once("..=").and_then(|(lo, hi)| {
61                    Some((
62                        lo.trim().parse::<f64>().ok()?,
63                        hi.trim().parse::<f64>().ok()?,
64                    ))
65                });
66                a.range = Some(parsed.ok_or_else(|| {
67                    syn::Error::new_spanned(&lit, "range = \"lo..=hi\" (numbers)")
68                })?);
69            } else if meta.path.is_ident("with") {
70                a.with = Some(meta.value()?.parse::<LitStr>()?.parse()?);
71            } else if meta.path.is_ident("skip") {
72                a.skip = true;
73            } else if meta.path.is_ident("other") {
74                a.other = true;
75            } else {
76                return Err(meta
77                    .error("unknown `data` attribute (rename, default, skip, tag, with, range, other)"));
78            }
79            Ok(())
80        })?;
81    }
82    Ok(a)
83}
84
85fn docs(list: &[syn::Attribute]) -> String {
86    let mut lines = Vec::new();
87    for attr in list.iter().filter(|a| a.path().is_ident("doc")) {
88        if let syn::Meta::NameValue(nv) = &attr.meta
89            && let syn::Expr::Lit(syn::ExprLit {
90                lit: syn::Lit::Str(s),
91                ..
92            }) = &nv.value
93        {
94            lines.push(s.value().trim().to_string());
95        }
96    }
97    // Paragraphs keep their breaks; lines of one paragraph join with spaces.
98    let mut out = String::new();
99    for line in lines {
100        if line.is_empty() {
101            if !out.ends_with("\n\n") && !out.is_empty() {
102                out.push_str("\n\n");
103            }
104        } else {
105            if !out.is_empty() && !out.ends_with('\n') {
106                out.push(' ');
107            }
108            out.push_str(&line);
109        }
110    }
111    out.trim_end().to_string()
112}
113
114fn snake(ident: &Ident) -> String {
115    let mut out = String::new();
116    for (i, c) in ident.to_string().chars().enumerate() {
117        if c.is_ascii_uppercase() {
118            if i > 0 {
119                out.push('_');
120            }
121            out.push(c.to_ascii_lowercase());
122        } else {
123            out.push(c);
124        }
125    }
126    out
127}
128
129struct NamedField {
130    ident: Ident,
131    ty: Type,
132    key: String,
133    doc: String,
134    default: Option<Option<syn::Path>>,
135    skip: bool,
136    /// The codec: `with = "…"`, else `Plain` (the type's own schema).
137    with: Type,
138    range: Option<(f64, f64)>,
139}
140
141fn named_fields(fields: &syn::FieldsNamed) -> syn::Result<Vec<NamedField>> {
142    fields
143        .named
144        .iter()
145        .map(|f| {
146            let a = attrs(&f.attrs)?;
147            let ident = f.ident.clone().unwrap();
148            let key = a
149                .rename
150                .unwrap_or_else(|| ident.to_string().trim_start_matches("r#").to_string());
151            Ok(NamedField {
152                ident,
153                ty: f.ty.clone(),
154                key,
155                doc: docs(&f.attrs),
156                default: a.default,
157                skip: a.skip,
158                with: a
159                    .with
160                    .unwrap_or_else(|| syn::parse_quote!(::yog_data::with::Plain)),
161                range: a.range,
162            })
163        })
164        .collect()
165}
166
167/// `Vec<Field>` describing `fields`.
168fn field_shapes(fields: &[NamedField]) -> Tokens {
169    let items = fields.iter().filter(|f| !f.skip).map(|f| {
170        let (key, doc, ty, w) = (&f.key, &f.doc, &f.ty, &f.with);
171        let has_default = f.default.is_some();
172        let range = match f.range {
173            Some((lo, hi)) => quote! { ::std::option::Option::Some((#lo, #hi)) },
174            None => quote! { ::std::option::Option::None },
175        };
176        quote! {
177            ::yog_data::schema::Field {
178                name: #key,
179                doc: #doc,
180                shape: <#w as ::yog_data::with::With<#ty>>::shape,
181                required: !#has_default && <#w as ::yog_data::with::With<#ty>>::absent().is_none(),
182                range: #range,
183            }
184        }
185    });
186    quote! { ::std::vec![#(#items),*] }
187}
188
189/// Reads `fields` out of `map` (in scope) into `ctor { ... }`.
190fn read_fields(fields: &[NamedField], ctor: Tokens) -> Tokens {
191    let keys = fields.iter().filter(|f| !f.skip).map(|f| &f.key);
192    let lets = fields.iter().map(|f| {
193        let (ident, ty, key, w) = (&f.ident, &f.ty, &f.key, &f.with);
194        if f.skip {
195            return quote! { let #ident: ::std::option::Option<#ty> = ::std::option::Option::Some(::std::default::Default::default()); };
196        }
197        match &f.default {
198            None => quote! { let #ident = cx.field_with::<#w, #ty>(map, #key); },
199            Some(None) => quote! { let #ident = cx.field_or_with::<#w, #ty>(map, #key, ::std::default::Default::default); },
200            Some(Some(path)) => quote! { let #ident = cx.field_or_with::<#w, #ty>(map, #key, #path); },
201        }
202    });
203    let ranges = fields.iter().filter_map(|f| {
204        let (ident, key) = (&f.ident, &f.key);
205        let (lo, hi) = f.range?;
206        Some(quote! { let #ident = cx.in_range(#key, #ident, #lo, #hi); })
207    });
208    let inits = fields.iter().map(|f| {
209        let ident = &f.ident;
210        quote! { #ident: #ident? }
211    });
212    quote! {
213        cx.unknown_keys(map, &[#(#keys),*]);
214        cx.own_keys(|cx| {
215            #(#lets)*
216            #(#ranges)*
217            ::std::option::Option::Some(#ctor { #(#inits),* })
218        })
219    }
220}
221
222/// Inserts `fields` (bound by name) into `m` (in scope).
223fn write_fields(fields: &[NamedField]) -> Tokens {
224    let puts = fields.iter().filter(|f| !f.skip).map(|f| {
225        let (ident, key, ty, w) = (&f.ident, &f.key, &f.ty, &f.with);
226        quote! {
227            if !<#w as ::yog_data::with::With<#ty>>::skip(#ident) {
228                m.insert(#key, <#w as ::yog_data::with::With<#ty>>::write(#ident));
229            }
230        }
231    });
232    quote! { #(#puts)* }
233}
234
235fn expand(input: &DeriveInput) -> syn::Result<Tokens> {
236    let name = &input.ident;
237    let (impl_g, ty_g, where_g) = input.generics.split_for_impl();
238    let top = attrs(&input.attrs)?;
239    let shape_name = top.rename.clone().unwrap_or_else(|| name.to_string());
240    let doc = docs(&input.attrs);
241    let body = match &input.data {
242        Data::Struct(s) => match &s.fields {
243            Fields::Named(named) => {
244                let fields = named_fields(named)?;
245                let shapes = field_shapes(&fields);
246                let read = read_fields(&fields, quote! { Self });
247                let idents: Vec<_> = fields.iter().map(|f| &f.ident).collect();
248                let write = write_fields(&fields);
249                quote! {
250                    fn shape() -> ::yog_data::Shape {
251                        ::yog_data::Shape::Struct(::yog_data::schema::StructShape { name: #shape_name, doc: #doc, fields: #shapes })
252                    }
253                    fn read(v: &::yog_data::ValueRef<'_>, cx: &mut ::yog_data::Reader) -> ::std::option::Option<Self> {
254                        let map = cx.map(v)?;
255                        #read
256                    }
257                    #[allow(unused_variables)]
258                    fn write(&self) -> ::yog_data::Value {
259                        let Self { #(#idents),* } = self;
260                        let mut m = ::yog_data::Map::new();
261                        #write
262                        ::yog_data::Value::Map(m)
263                    }
264                }
265            }
266            Fields::Unnamed(u) if u.unnamed.len() == 1 => {
267                let ty = &u.unnamed[0].ty;
268                quote! {
269                    fn shape() -> ::yog_data::Shape { <#ty as ::yog_data::Schema>::shape() }
270                    fn read(v: &::yog_data::ValueRef<'_>, cx: &mut ::yog_data::Reader) -> ::std::option::Option<Self> {
271                        <#ty as ::yog_data::Schema>::read(v, cx).map(Self)
272                    }
273                    fn write(&self) -> ::yog_data::Value { ::yog_data::Schema::write(&self.0) }
274                    fn absent() -> ::std::option::Option<Self> { <#ty as ::yog_data::Schema>::absent().map(Self) }
275                    fn skip(&self) -> bool { ::yog_data::Schema::skip(&self.0) }
276                }
277            }
278            _ => {
279                return Err(syn::Error::new_spanned(
280                    name,
281                    "Schema: a struct needs named fields or exactly one unnamed field",
282                ));
283            }
284        },
285        Data::Enum(e) => expand_enum(e, &top, &shape_name, &doc)?,
286        Data::Union(_) => {
287            return Err(syn::Error::new_spanned(
288                name,
289                "Schema: unions aren't supported",
290            ));
291        }
292    };
293    Ok(quote! {
294        impl #impl_g ::yog_data::Schema for #name #ty_g #where_g {
295            #body
296        }
297    })
298}
299
300fn expand_enum(e: &syn::DataEnum, top: &Attrs, shape_name: &str, doc: &str) -> syn::Result<Tokens> {
301    struct V {
302        ident: Ident,
303        key: String,
304        doc: String,
305        kind: VKind,
306        other: bool,
307    }
308    enum VKind {
309        Unit,
310        Named(Vec<NamedField>),
311        Wrap(Box<Type>),
312    }
313    let mut vs = Vec::new();
314    for v in &e.variants {
315        let a = attrs(&v.attrs)?;
316        let kind = match &v.fields {
317            Fields::Unit => VKind::Unit,
318            Fields::Named(n) => VKind::Named(named_fields(n)?),
319            Fields::Unnamed(u) if u.unnamed.len() == 1 => {
320                VKind::Wrap(Box::new(u.unnamed[0].ty.clone()))
321            }
322            Fields::Unnamed(_) => {
323                return Err(syn::Error::new_spanned(
324                    &v.ident,
325                    "Schema: a variant wraps one struct at most",
326                ));
327            }
328        };
329        if top.tag.is_none() && !matches!(kind, VKind::Unit) {
330            return Err(syn::Error::new_spanned(
331                &v.ident,
332                "Schema: variants with fields need `#[data(tag = \"type\")]` on the enum",
333            ));
334        }
335        if a.other && (top.tag.is_none() || !matches!(kind, VKind::Wrap(_))) {
336            return Err(syn::Error::new_spanned(
337                &v.ident,
338                "Schema: `other` is a tagged enum's variant wrapping a type that reads the whole object, tag included",
339            ));
340        }
341        vs.push(V {
342            key: a.rename.unwrap_or_else(|| snake(&v.ident)),
343            ident: v.ident.clone(),
344            doc: docs(&v.attrs),
345            kind,
346            other: a.other,
347        });
348    }
349    // The catch-all variant, if any: not a name of its own.
350    let other = vs.iter().position(|v| v.other).map(|i| vs.remove(i));
351    let keys: Vec<&String> = vs.iter().map(|v| &v.key).collect();
352    let variant_shapes = vs.iter().map(|v| {
353        let (key, doc) = (&v.key, &v.doc);
354        let fields = match &v.kind {
355            VKind::Unit => quote! { ::std::vec::Vec::new() },
356            VKind::Named(f) => field_shapes(f),
357            VKind::Wrap(ty) => quote! {
358                match <#ty as ::yog_data::Schema>::shape() {
359                    ::yog_data::Shape::Struct(s) => s.fields,
360                    _ => ::std::vec::Vec::new(),
361                }
362            },
363        };
364        quote! { ::yog_data::schema::Variant { name: #key, doc: #doc, fields: #fields } }
365    });
366    let tag_tokens = match &top.tag {
367        Some(t) => quote! { ::std::option::Option::Some(#t) },
368        None => quote! { ::std::option::Option::None },
369    };
370    let shape = quote! {
371        fn shape() -> ::yog_data::Shape {
372            ::yog_data::Shape::Enum(::yog_data::schema::EnumShape {
373                name: #shape_name,
374                doc: #doc,
375                tag: #tag_tokens,
376                variants: ::std::vec![#(#variant_shapes),*],
377            })
378        }
379    };
380
381    let Some(tag) = &top.tag else {
382        let reads = vs.iter().map(|v| {
383            let (ident, key) = (&v.ident, &v.key);
384            quote! { #key => ::std::option::Option::Some(Self::#ident), }
385        });
386        let writes = vs.iter().map(|v| {
387            let (ident, key) = (&v.ident, &v.key);
388            quote! { Self::#ident => #key, }
389        });
390        return Ok(quote! {
391            #shape
392            fn read(v: &::yog_data::ValueRef<'_>, cx: &mut ::yog_data::Reader) -> ::std::option::Option<Self> {
393                let ::yog_data::ValueRef::Str(s) = v else {
394                    cx.expected("a name", v);
395                    return ::std::option::Option::None;
396                };
397                match *s {
398                    #(#reads)*
399                    other => {
400                        cx.unknown_name(other, &[#(#keys),*]);
401                        ::std::option::Option::None
402                    }
403                }
404            }
405            fn write(&self) -> ::yog_data::Value {
406                ::yog_data::Value::Str(::std::string::String::from(match self { #(#writes)* }))
407            }
408        });
409    };
410
411    let reads = vs.iter().map(|v| {
412        let (ident, key) = (&v.ident, &v.key);
413        let body = match &v.kind {
414            VKind::Unit => quote! {
415                cx.with_allowed(#tag, |cx| cx.unknown_keys(map, &[]));
416                ::std::option::Option::Some(Self::#ident)
417            },
418            VKind::Named(fields) => {
419                let read = read_fields(fields, quote! { Self::#ident });
420                quote! { cx.with_allowed(#tag, |cx| { #read }) }
421            }
422            VKind::Wrap(ty) => quote! {
423                cx.with_allowed(#tag, |cx| <#ty as ::yog_data::Schema>::read(v, cx)).map(Self::#ident)
424            },
425        };
426        quote! { #key => { #body } }
427    });
428    let writes = vs.iter().map(|v| {
429        let (ident, key) = (&v.ident, &v.key);
430        match &v.kind {
431            VKind::Unit => quote! {
432                Self::#ident => {
433                    m.insert(#tag, ::yog_data::Value::from(#key));
434                }
435            },
436            VKind::Named(fields) => {
437                let idents: Vec<_> = fields.iter().map(|f| &f.ident).collect();
438                let puts = write_fields(fields);
439                quote! {
440                    #[allow(unused_variables)]
441                    Self::#ident { #(#idents),* } => {
442                        m.insert(#tag, ::yog_data::Value::from(#key));
443                        #puts
444                    }
445                }
446            }
447            VKind::Wrap(_) => quote! {
448                Self::#ident(inner) => {
449                    m.insert(#tag, ::yog_data::Value::from(#key));
450                    if let ::yog_data::Value::Map(fields) = ::yog_data::Schema::write(inner) {
451                        for (k, v) in fields {
452                            m.insert(k, v);
453                        }
454                    }
455                }
456            },
457        }
458    });
459    let (fallback, other_write) = match &other {
460        Some(V { ident, kind: VKind::Wrap(ty), .. }) => (
461            quote! { _ => <#ty as ::yog_data::Schema>::read(v, cx).map(Self::#ident), },
462            quote! { Self::#ident(inner) => { m = match ::yog_data::Schema::write(inner) { ::yog_data::Value::Map(fields) => fields, _ => m }; } },
463        ),
464        _ => (
465            quote! {
466                other => {
467                    cx.at(::yog_data::Seg::Key(::std::string::String::from(#tag)), |cx| cx.unknown_name(other, &[#(#keys),*]));
468                    ::std::option::Option::None
469                }
470            },
471            quote! {},
472        ),
473    };
474    Ok(quote! {
475        #shape
476        fn read(v: &::yog_data::ValueRef<'_>, cx: &mut ::yog_data::Reader) -> ::std::option::Option<Self> {
477            let map = cx.map(v)?;
478            let ::std::option::Option::Some(kind) = map.get(#tag) else {
479                cx.problem(::std::format!("missing `{}` ({})", #tag, [#(#keys),*].map(|k| ::std::format!("`{k}`")).join(", ")));
480                return ::std::option::Option::None;
481            };
482            let ::yog_data::ValueRef::Str(kind) = kind else {
483                cx.at(::yog_data::Seg::Key(::std::string::String::from(#tag)), |cx| cx.expected("a name", kind));
484                return ::std::option::Option::None;
485            };
486            match *kind {
487                #(#reads)*
488                #fallback
489            }
490        }
491        fn write(&self) -> ::yog_data::Value {
492            let mut m = ::yog_data::Map::new();
493            match self { #(#writes)* #other_write }
494            ::yog_data::Value::Map(m)
495        }
496    })
497}