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}
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 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 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
149fn 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
166fn 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
193fn 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}