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