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}
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 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 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
163fn 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
185fn 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
218fn 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::ValueRef<'_>, 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::ValueRef<'_>, 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::ValueRef<'_>, cx: &mut ::yog_data::Reader) -> ::std::option::Option<Self> {
379 let ::yog_data::ValueRef::Str(s) = v else {
380 cx.expected("a name", v);
381 return ::std::option::Option::None;
382 };
383 match *s {
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::ValueRef<'_>, 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::ValueRef::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 {
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}