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}
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 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
139fn 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
156fn 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
183fn 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}