1use 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 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 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 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
167fn 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
189fn 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
222fn 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 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}