Skip to content

Commit 3cdf3b2

Browse files
committed
New schema format
1 parent 6d4aad3 commit 3cdf3b2

File tree

13 files changed

+1947
-0
lines changed

13 files changed

+1947
-0
lines changed

packages/cw-schema-derive/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "cw-schema-derive"
3+
version.workspace = true
4+
edition = "2021"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
heck = "0.5.0"
11+
itertools = { version = "0.13.0", default-features = false }
12+
proc-macro2 = "1.0.86"
13+
quote = "1.0.36"
14+
syn = { version = "2.0.72", features = ["full"] }
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
use std::borrow::Cow;
2+
3+
use crate::bail;
4+
use proc_macro2::TokenStream;
5+
use quote::quote;
6+
use syn::{punctuated::Punctuated, DataEnum, DataStruct, DataUnion, DeriveInput, Lit};
7+
8+
struct SerdeContainerOptions {
9+
rename_all: Option<syn::LitStr>,
10+
untagged: bool,
11+
}
12+
13+
impl SerdeContainerOptions {
14+
fn parse(attributes: &[syn::Attribute]) -> syn::Result<Self> {
15+
let mut options = SerdeContainerOptions {
16+
rename_all: None,
17+
untagged: false,
18+
};
19+
20+
for attribute in attributes
21+
.iter()
22+
.filter(|attr| attr.path().is_ident("serde"))
23+
{
24+
attribute.parse_nested_meta(|meta| {
25+
if meta.path.is_ident("rename_all") {
26+
options.rename_all = Some(meta.value()?.parse()?);
27+
} else if meta.path.is_ident("untagged") {
28+
options.untagged = true;
29+
} else {
30+
// TODO: support other serde attributes
31+
//
32+
// For now we simply clear the buffer to avoid errors
33+
let _ = meta
34+
.value()
35+
.map(|val| val.parse::<TokenStream>().unwrap())
36+
.unwrap_or_else(|_| meta.input.cursor().token_stream());
37+
}
38+
39+
Ok(())
40+
})?;
41+
}
42+
43+
Ok(options)
44+
}
45+
}
46+
47+
struct ContainerOptions {
48+
crate_path: syn::Path,
49+
}
50+
51+
impl ContainerOptions {
52+
fn parse(attributes: &[syn::Attribute]) -> syn::Result<Self> {
53+
let mut options = ContainerOptions {
54+
crate_path: syn::parse_str("::cw_schema")?,
55+
};
56+
57+
for attribute in attributes
58+
.iter()
59+
.filter(|attr| attr.path().is_ident("schemaifier"))
60+
{
61+
attribute.parse_nested_meta(|meta| {
62+
if meta.path.is_ident("crate") {
63+
let stringified: syn::LitStr = meta.value()?.parse()?;
64+
options.crate_path = stringified.parse()?;
65+
} else {
66+
bail!(meta.path, "unknown attribute");
67+
}
68+
69+
Ok(())
70+
})?;
71+
}
72+
73+
Ok(options)
74+
}
75+
}
76+
77+
#[inline]
78+
fn normalize_option<T: quote::ToTokens>(value: Option<T>) -> TokenStream {
79+
match value {
80+
Some(value) => quote! { Some(#value.into()) },
81+
None => quote! { None },
82+
}
83+
}
84+
85+
fn extract_documentation(attributes: &[syn::Attribute]) -> syn::Result<Option<String>> {
86+
let docs_iter = attributes
87+
.iter()
88+
.filter(|attribute| attribute.path().is_ident("doc"))
89+
.map(|doc_attribute| {
90+
let name_value = doc_attribute.meta.require_name_value()?;
91+
92+
let syn::Expr::Lit(syn::ExprLit {
93+
lit: Lit::Str(ref text),
94+
..
95+
}) = name_value.value
96+
else {
97+
bail!(name_value, "expected string literal");
98+
};
99+
100+
Ok(Cow::Owned(text.value().trim().to_string()))
101+
});
102+
103+
let docs = itertools::intersperse(docs_iter, Ok(Cow::Borrowed("\n")))
104+
.collect::<syn::Result<String>>()?;
105+
106+
if docs.is_empty() {
107+
return Ok(None);
108+
}
109+
110+
Ok(Some(docs))
111+
}
112+
113+
fn patch_type_params<'a>(
114+
options: &ContainerOptions,
115+
type_params: impl Iterator<Item = &'a mut syn::TypeParam>,
116+
) {
117+
let crate_path = &options.crate_path;
118+
119+
for param in type_params {
120+
param.bounds.push(syn::TypeParamBound::Verbatim(
121+
quote! { #crate_path::Schemaifier },
122+
));
123+
}
124+
}
125+
126+
pub struct ContainerMeta {
127+
name: syn::Ident,
128+
description: Option<String>,
129+
generics: syn::Generics,
130+
options: ContainerOptions,
131+
serde_options: SerdeContainerOptions,
132+
}
133+
134+
fn expand_enum(mut meta: ContainerMeta, input: DataEnum) -> syn::Result<TokenStream> {
135+
let crate_path = &meta.options.crate_path;
136+
137+
let mut cases = Vec::new();
138+
for variant in input.variants.iter() {
139+
let value = match variant.fields {
140+
syn::Fields::Named(ref fields) => {
141+
let items = fields.named.iter().map(|field| {
142+
let name = field.ident.as_ref().unwrap();
143+
let description = normalize_option(extract_documentation(&field.attrs)?);
144+
let field_ty = &field.ty;
145+
146+
let expanded = quote! {
147+
(
148+
stringify!(#name).into(),
149+
#crate_path::StructProperty {
150+
description: #description,
151+
value: <#field_ty as #crate_path::Schemaifier>::visit_schema(visitor),
152+
}
153+
)
154+
};
155+
156+
Ok(expanded)
157+
}).collect::<syn::Result<Vec<_>>>()?;
158+
159+
quote! {
160+
#crate_path::EnumValue::Named {
161+
properties: #crate_path::reexport::BTreeMap::from([
162+
#( #items, )*
163+
])
164+
}
165+
}
166+
}
167+
syn::Fields::Unnamed(ref fields) => {
168+
let types = fields.unnamed.iter().map(|field| &field.ty);
169+
170+
quote! {
171+
#crate_path::EnumValue::Tuple {
172+
items: vec![
173+
#( <#types as #crate_path::Schemaifier>::visit_schema(visitor), )*
174+
]
175+
}
176+
}
177+
}
178+
syn::Fields::Unit => quote! { #crate_path::EnumValue::Unit },
179+
};
180+
181+
let variant_name = &variant.ident;
182+
let description = normalize_option(extract_documentation(&variant.attrs)?);
183+
184+
let expanded = quote! {
185+
#crate_path::EnumCase {
186+
description: #description,
187+
value: #value,
188+
}
189+
};
190+
191+
cases.push(quote! {
192+
(
193+
stringify!(#variant_name).into(),
194+
#expanded,
195+
)
196+
});
197+
}
198+
199+
let name = &meta.name;
200+
let description = normalize_option(meta.description.as_ref());
201+
let crate_path = &meta.options.crate_path;
202+
203+
patch_type_params(&meta.options, meta.generics.type_params_mut());
204+
let (impl_generics, ty_generics, where_clause) = meta.generics.split_for_impl();
205+
206+
Ok(quote! {
207+
impl #impl_generics #crate_path::Schemaifier for #name #ty_generics #where_clause {
208+
fn visit_schema(visitor: &mut #crate_path::SchemaVisitor) -> #crate_path::DefinitionReference {
209+
let node = #crate_path::Node {
210+
name: std::any::type_name::<Self>().into(),
211+
description: #description,
212+
value: #crate_path::NodeType::Enum {
213+
discriminator: None,
214+
cases: #crate_path::reexport::BTreeMap::from([
215+
#( #cases, )*
216+
]),
217+
},
218+
};
219+
220+
visitor.insert(Self::id(), node)
221+
}
222+
}
223+
})
224+
}
225+
226+
fn expand_struct(mut meta: ContainerMeta, input: DataStruct) -> syn::Result<TokenStream> {
227+
let name = &meta.name;
228+
let description = normalize_option(meta.description.as_ref());
229+
let crate_path = &meta.options.crate_path;
230+
231+
let node_ty = match input.fields {
232+
syn::Fields::Named(named) => {
233+
let items = named.named.iter().map(|field| {
234+
let name = field.ident.as_ref().unwrap();
235+
let description = normalize_option(extract_documentation(&field.attrs)?);
236+
let field_ty = &field.ty;
237+
238+
let expanded = quote! {
239+
(
240+
stringify!(#name).into(),
241+
#crate_path::StructProperty {
242+
description: #description,
243+
value: <#field_ty as #crate_path::Schemaifier>::visit_schema(visitor),
244+
}
245+
)
246+
};
247+
248+
Ok(expanded)
249+
}).collect::<syn::Result<Vec<_>>>()?;
250+
251+
quote! {
252+
#crate_path::StructType::Named {
253+
properties: #crate_path::reexport::BTreeMap::from([
254+
#( #items, )*
255+
])
256+
}
257+
}
258+
}
259+
syn::Fields::Unnamed(fields) => {
260+
let type_names = fields.unnamed.iter().map(|field| &field.ty);
261+
262+
quote! {
263+
#crate_path::StructType::Tuple {
264+
items: vec![
265+
#(
266+
<#type_names as #crate_path::Schemaifier>::visit_schema(visitor),
267+
)*
268+
],
269+
}
270+
}
271+
}
272+
syn::Fields::Unit => quote! { #crate_path::StructType::Unit },
273+
};
274+
275+
let node = quote! {
276+
#crate_path::Node {
277+
name: std::any::type_name::<Self>().into(),
278+
description: #description,
279+
value: #crate_path::NodeType::Struct(#node_ty),
280+
}
281+
};
282+
283+
patch_type_params(&meta.options, meta.generics.type_params_mut());
284+
let (impl_generics, ty_generics, where_clause) = meta.generics.split_for_impl();
285+
286+
Ok(quote! {
287+
impl #impl_generics #crate_path::Schemaifier for #name #ty_generics #where_clause {
288+
fn visit_schema(visitor: &mut #crate_path::SchemaVisitor) -> #crate_path::DefinitionReference {
289+
let node = {
290+
#node
291+
};
292+
293+
visitor.insert(Self::id(), node)
294+
}
295+
}
296+
})
297+
}
298+
299+
fn expand_union(_meta: ContainerMeta, input: DataUnion) -> syn::Result<TokenStream> {
300+
Err(syn::Error::new_spanned(
301+
input.union_token,
302+
"Unions are not supported (yet)",
303+
))
304+
}
305+
306+
pub fn expand(input: DeriveInput) -> syn::Result<TokenStream> {
307+
let options = ContainerOptions::parse(&input.attrs)?;
308+
let serde_options = SerdeContainerOptions::parse(&input.attrs)?;
309+
310+
let description = extract_documentation(&input.attrs)?;
311+
312+
let meta = ContainerMeta {
313+
name: input.ident,
314+
description,
315+
generics: input.generics,
316+
options,
317+
serde_options,
318+
};
319+
320+
match input.data {
321+
syn::Data::Enum(input) => expand_enum(meta, input),
322+
syn::Data::Struct(input) => expand_struct(meta, input),
323+
syn::Data::Union(input) => expand_union(meta, input),
324+
}
325+
}

packages/cw-schema-derive/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
mod expand;
2+
3+
macro_rules! bail {
4+
($span_src:expr, $msg:literal) => {{
5+
return Err($crate::error_message!($span_src, $msg));
6+
}};
7+
}
8+
9+
macro_rules! error_message {
10+
($span_src:expr, $msg:literal) => {{
11+
::syn::Error::new(::syn::spanned::Spanned::span(&{ $span_src }), $msg)
12+
}};
13+
}
14+
// Needed so we can import macros. Rust, why?
15+
use {bail, error_message};
16+
17+
#[proc_macro_derive(Schemaifier, attributes(schemaifier, serde))]
18+
pub fn schemaifier(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
19+
let input = syn::parse_macro_input!(input as syn::DeriveInput);
20+
21+
match expand::expand(input) {
22+
Ok(output) => output.into(),
23+
Err(err) => err.to_compile_error().into(),
24+
}
25+
}

packages/cw-schema/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

0 commit comments

Comments
 (0)