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