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