use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
Data, DeriveInput, Fields, FieldsNamed, LitStr, Variant, parse_macro_input, spanned::Spanned,
};
#[proc_macro_derive(Schema, attributes(schema, serde))]
pub fn derive_schema(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand(input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
fn expand(input: DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let container = parse_serde_attrs(&input.attrs)?;
let container_schema = parse_field_schema_attrs(&input.attrs)?;
let description = container_schema
.description
.clone()
.or_else(|| extract_doc(&input.attrs));
let body = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(named) => struct_object_schema(
named,
container.rename_all.as_deref(),
description.as_deref(),
)?,
_ => {
return Err(syn::Error::new(
input.span(),
"enum2schema only supports structs with named fields",
));
}
},
Data::Enum(data) => {
let description_insert = match &description {
Some(text) => quote! {
__map.insert(
"description".to_string(),
enum2schema::serde_json::Value::String(#text.to_string()),
);
},
None => quote! {},
};
if container_schema.string_enum {
let mut tags = Vec::new();
for variant in &data.variants {
if parse_field_schema_attrs(&variant.attrs)?.skip {
continue;
}
if !matches!(variant.fields, Fields::Unit) {
return Err(syn::Error::new(
variant.span(),
"schema(string_enum) requires every variant to be a unit variant",
));
}
let serde_attrs = parse_serde_attrs(&variant.attrs)?;
let tag = apply_name(
&variant.ident.to_string(),
serde_attrs.rename.as_deref(),
container.rename_all.as_deref(),
);
tags.push(LitStr::new(&tag, variant.ident.span()));
}
quote! {
{
let mut __map = enum2schema::serde_json::Map::new();
#description_insert
__map.insert(
"type".to_string(),
enum2schema::serde_json::Value::String("string".to_string()),
);
__map.insert(
"enum".to_string(),
enum2schema::serde_json::Value::Array(vec![
#(enum2schema::serde_json::Value::String(#tags.to_string())),*
]),
);
enum2schema::serde_json::Value::Object(__map)
}
}
} else {
let mut variants = Vec::new();
for variant in &data.variants {
if parse_field_schema_attrs(&variant.attrs)?.skip {
continue;
}
variants.push(variant_schema_expr(
variant,
container.rename_all.as_deref(),
)?);
}
quote! {
{
let mut __map = enum2schema::serde_json::Map::new();
#description_insert
__map.insert(
"oneOf".to_string(),
enum2schema::serde_json::Value::Array(vec![ #(#variants),* ]),
);
enum2schema::serde_json::Value::Object(__map)
}
}
}
}
Data::Union(_) => {
return Err(syn::Error::new(
input.span(),
"enum2schema does not support unions",
));
}
};
let generics = add_trait_bounds(input.generics.clone());
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
impl #impl_generics enum2schema::Schema for #name #ty_generics #where_clause {
fn schema() -> enum2schema::serde_json::Value {
#body
}
}
})
}
fn struct_object_schema(
fields: &FieldsNamed,
container_rename_all: Option<&str>,
description: Option<&str>,
) -> syn::Result<TokenStream2> {
let mut inserts = Vec::new();
let mut required = Vec::new();
for field in &fields.named {
let schema_attrs = parse_field_schema_attrs(&field.attrs)?;
if schema_attrs.skip {
continue;
}
let serde_attrs = parse_serde_attrs(&field.attrs)?;
let ident = field.ident.as_ref().unwrap();
let property = apply_name(
&ident.to_string(),
serde_attrs.rename.as_deref(),
container_rename_all,
);
let is_required = !is_option(&field.ty) && !serde_attrs.default;
let field_description = schema_attrs
.description
.clone()
.or_else(|| extract_doc(&field.attrs));
let schema_expr = field_schema_expr(&field.ty, &schema_attrs);
let schema_expr = match &field_description {
Some(text) => quote! { enum2schema::set_description(#schema_expr, #text) },
None => schema_expr,
};
inserts.push(quote! {
__properties.insert(#property.to_string(), #schema_expr);
});
if is_required {
required.push(quote! {
enum2schema::serde_json::Value::String(#property.to_string())
});
}
}
let description_insert = match description {
Some(text) => quote! {
__map.insert(
"description".to_string(),
enum2schema::serde_json::Value::String(#text.to_string()),
);
},
None => quote! {},
};
let required_insert = if required.is_empty() {
quote! {}
} else {
quote! {
__map.insert(
"required".to_string(),
enum2schema::serde_json::Value::Array(vec![ #(#required),* ]),
);
}
};
Ok(quote! {
{
let mut __map = enum2schema::serde_json::Map::new();
__map.insert(
"type".to_string(),
enum2schema::serde_json::Value::String("object".to_string()),
);
#description_insert
let mut __properties = enum2schema::serde_json::Map::new();
#(#inserts)*
__map.insert(
"properties".to_string(),
enum2schema::serde_json::Value::Object(__properties),
);
#required_insert
enum2schema::serde_json::Value::Object(__map)
}
})
}
fn variant_schema_expr(
variant: &Variant,
container_rename_all: Option<&str>,
) -> syn::Result<TokenStream2> {
let serde_attrs = parse_serde_attrs(&variant.attrs)?;
let schema_attrs = parse_field_schema_attrs(&variant.attrs)?;
let tag = apply_name(
&variant.ident.to_string(),
serde_attrs.rename.as_deref(),
container_rename_all,
);
let tag_lit = LitStr::new(&tag, variant.ident.span());
let description = schema_attrs
.description
.clone()
.or_else(|| extract_doc(&variant.attrs));
let base = match &variant.fields {
Fields::Unit => quote! {
{
let mut __map = enum2schema::serde_json::Map::new();
__map.insert(
"type".to_string(),
enum2schema::serde_json::Value::String("string".to_string()),
);
__map.insert(
"const".to_string(),
enum2schema::serde_json::Value::String(#tag_lit.to_string()),
);
enum2schema::serde_json::Value::Object(__map)
}
},
Fields::Named(named) => {
let inner = struct_object_schema(named, None, None)?;
wrap_externally_tagged(&tag_lit, inner)
}
Fields::Unnamed(unnamed) => {
let unnamed_fields: Vec<_> = unnamed.unnamed.iter().collect();
if unnamed_fields.len() == 1 {
let field = unnamed_fields[0];
let field_attrs = parse_field_schema_attrs(&field.attrs)?;
let inner = field_schema_expr(&field.ty, &field_attrs);
wrap_externally_tagged(&tag_lit, inner)
} else {
let count = unnamed_fields.len() as u64;
let item_schemas = unnamed_fields.iter().map(|field| {
let field_type = &field.ty;
quote! { <#field_type as enum2schema::Schema>::schema() }
});
let inner = quote! {
{
let mut __array = enum2schema::serde_json::Map::new();
__array.insert(
"type".to_string(),
enum2schema::serde_json::Value::String("array".to_string()),
);
__array.insert(
"items".to_string(),
enum2schema::serde_json::Value::Array(vec![ #(#item_schemas),* ]),
);
__array.insert(
"minItems".to_string(),
enum2schema::serde_json::Value::from(#count),
);
__array.insert(
"maxItems".to_string(),
enum2schema::serde_json::Value::from(#count),
);
enum2schema::serde_json::Value::Object(__array)
}
};
wrap_externally_tagged(&tag_lit, inner)
}
}
};
Ok(match description {
Some(text) => quote! { enum2schema::set_description(#base, #text) },
None => base,
})
}
fn wrap_externally_tagged(tag_lit: &LitStr, inner: TokenStream2) -> TokenStream2 {
quote! {
{
let mut __outer = enum2schema::serde_json::Map::new();
__outer.insert(
"type".to_string(),
enum2schema::serde_json::Value::String("object".to_string()),
);
let mut __outer_properties = enum2schema::serde_json::Map::new();
__outer_properties.insert(#tag_lit.to_string(), #inner);
__outer.insert(
"properties".to_string(),
enum2schema::serde_json::Value::Object(__outer_properties),
);
__outer.insert(
"required".to_string(),
enum2schema::serde_json::Value::Array(vec![
enum2schema::serde_json::Value::String(#tag_lit.to_string())
]),
);
enum2schema::serde_json::Value::Object(__outer)
}
}
}
fn field_schema_expr(field_type: &syn::Type, attrs: &FieldSchemaAttrs) -> TokenStream2 {
if let Some(path) = &attrs.with {
return quote! { #path() };
}
if let Some(type_override) = &attrs.type_override {
let mut inserts = Vec::new();
inserts.push(quote! {
__override.insert(
"type".to_string(),
enum2schema::serde_json::Value::String(#type_override.to_string()),
);
});
if let Some(items) = &attrs.items_override {
inserts.push(quote! {
let mut __items = enum2schema::serde_json::Map::new();
__items.insert(
"type".to_string(),
enum2schema::serde_json::Value::String(#items.to_string()),
);
__override.insert(
"items".to_string(),
enum2schema::serde_json::Value::Object(__items),
);
});
}
if let Some(length) = attrs.len_override {
let length = length as u64;
inserts.push(quote! {
__override.insert(
"minItems".to_string(),
enum2schema::serde_json::Value::from(#length),
);
__override.insert(
"maxItems".to_string(),
enum2schema::serde_json::Value::from(#length),
);
});
}
return quote! {
{
let mut __override = enum2schema::serde_json::Map::new();
#(#inserts)*
enum2schema::serde_json::Value::Object(__override)
}
};
}
quote! { <#field_type as enum2schema::Schema>::schema() }
}
#[derive(Default)]
struct FieldSchemaAttrs {
skip: bool,
string_enum: bool,
with: Option<syn::Path>,
description: Option<String>,
type_override: Option<String>,
items_override: Option<String>,
len_override: Option<usize>,
}
fn parse_field_schema_attrs(attrs: &[syn::Attribute]) -> syn::Result<FieldSchemaAttrs> {
let mut result = FieldSchemaAttrs::default();
for attr in attrs {
if !attr.path().is_ident("schema") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
result.skip = true;
return Ok(());
}
if meta.path.is_ident("string_enum") {
result.string_enum = true;
return Ok(());
}
if meta.path.is_ident("with") {
result.with = Some(meta.value()?.parse()?);
return Ok(());
}
if meta.path.is_ident("description") {
let literal: LitStr = meta.value()?.parse()?;
result.description = Some(literal.value());
return Ok(());
}
if meta.path.is_ident("type") {
let literal: LitStr = meta.value()?.parse()?;
result.type_override = Some(literal.value());
return Ok(());
}
if meta.path.is_ident("items") {
let literal: LitStr = meta.value()?.parse()?;
result.items_override = Some(literal.value());
return Ok(());
}
if meta.path.is_ident("len") {
let literal: syn::LitInt = meta.value()?.parse()?;
result.len_override = Some(literal.base10_parse()?);
return Ok(());
}
Err(meta.error("unknown enum2schema attribute"))
})?;
}
Ok(result)
}
#[derive(Default)]
struct SerdeAttrs {
rename: Option<String>,
rename_all: Option<String>,
default: bool,
}
fn parse_serde_attrs(attrs: &[syn::Attribute]) -> syn::Result<SerdeAttrs> {
let mut result = SerdeAttrs::default();
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
attr.parse_nested_meta(|meta| {
let is_rename = meta.path.is_ident("rename");
let is_rename_all = meta.path.is_ident("rename_all");
if meta.path.is_ident("default") {
result.default = true;
}
if meta.input.peek(syn::Token![=]) {
let value: syn::Lit = meta.value()?.parse()?;
if let syn::Lit::Str(literal) = value {
if is_rename {
result.rename = Some(literal.value());
} else if is_rename_all {
result.rename_all = Some(literal.value());
}
}
} else if meta.input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
let _: TokenStream2 = content.parse()?;
}
Ok(())
})?;
}
Ok(result)
}
fn extract_doc(attrs: &[syn::Attribute]) -> Option<String> {
let mut lines = Vec::new();
for attr in attrs {
if !attr.path().is_ident("doc") {
continue;
}
if let syn::Meta::NameValue(name_value) = &attr.meta
&& let syn::Expr::Lit(expr_lit) = &name_value.value
&& let syn::Lit::Str(literal) = &expr_lit.lit
{
lines.push(literal.value().trim().to_string());
}
}
if lines.is_empty() {
None
} else {
Some(lines.join("\n"))
}
}
fn is_option(field_type: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = field_type
&& let Some(segment) = type_path.path.segments.last()
{
return segment.ident == "Option";
}
false
}
fn add_trait_bounds(mut generics: syn::Generics) -> syn::Generics {
for param in &mut generics.params {
if let syn::GenericParam::Type(type_param) = param {
type_param
.bounds
.push(syn::parse_quote!(enum2schema::Schema));
}
}
generics
}
fn apply_name(original: &str, rename: Option<&str>, rename_all: Option<&str>) -> String {
if let Some(rename) = rename {
return rename.to_string();
}
if let Some(rule) = rename_all {
return apply_rename_all(original, rule);
}
original.to_string()
}
fn apply_rename_all(name: &str, rule: &str) -> String {
match rule {
"lowercase" => name.to_lowercase(),
"UPPERCASE" => name.to_uppercase(),
"PascalCase" => to_pascal_case(name),
"camelCase" => to_camel_case(name),
"snake_case" => to_snake_case(name),
"SCREAMING_SNAKE_CASE" => to_snake_case(name).to_uppercase(),
"kebab-case" => to_snake_case(name).replace('_', "-"),
"SCREAMING-KEBAB-CASE" => to_snake_case(name).to_uppercase().replace('_', "-"),
_ => name.to_string(),
}
}
fn to_snake_case(name: &str) -> String {
let mut result = String::new();
for (index, character) in name.chars().enumerate() {
if character.is_uppercase() {
if index != 0 {
result.push('_');
}
result.extend(character.to_lowercase());
} else {
result.push(character);
}
}
result
}
fn to_pascal_case(name: &str) -> String {
name.split('_')
.map(|part| {
let mut characters = part.chars();
match characters.next() {
Some(first) => first.to_uppercase().collect::<String>() + characters.as_str(),
None => String::new(),
}
})
.collect()
}
fn to_camel_case(name: &str) -> String {
let pascal = to_pascal_case(name);
let mut characters = pascal.chars();
match characters.next() {
Some(first) => first.to_lowercase().collect::<String>() + characters.as_str(),
None => String::new(),
}
}