use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, Meta};
#[proc_macro_derive(JsonSchemaOneOf)]
pub fn derive_json_schema_one_of(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let enum_name_str = name.to_string();
let data = match &input.data {
Data::Enum(data) => data,
_ => {
return syn::Error::new_spanned(
&input.ident,
"JsonSchemaOneOf can only be derived for enums",
)
.to_compile_error()
.into();
}
};
for variant in &data.variants {
match &variant.fields {
Fields::Unit => {}
_ => {
return syn::Error::new_spanned(
&variant.ident,
"JsonSchemaOneOf only supports unit variants (no fields)",
)
.to_compile_error()
.into();
}
}
}
let rename_case = extract_serde_rename_all(&input.attrs); let const_variants = data.variants.iter().map(|variant| {
let variant_name = variant.ident.to_string();
let json_name = apply_rename_case(&variant_name, &rename_case);
quote! {
schemars::schema::Schema::Object(schemars::schema::SchemaObject {
const_value: Some(serde_json::Value::String(#json_name.to_string())),
metadata: Some(Box::new(schemars::schema::Metadata {
title: Some(#variant_name.to_string()),
..Default::default()
})),
..Default::default()
})
}
});
let expanded = quote! {
impl schemars::JsonSchema for #name {
fn schema_name() -> String {
#enum_name_str.to_owned()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::*;
let mut schema = SchemaObject::default();
schema.subschemas().one_of = Some(vec![
#(#const_variants,)*
]);
schemars::schema::Schema::Object(schema)
}
}
};
TokenStream::from(expanded)
}
fn extract_serde_rename_all(attrs: &[Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("serde") {
if let Meta::List(meta_list) = &attr.meta {
let tokens_str = meta_list.tokens.to_string();
if tokens_str.contains("rename_all") {
if let Some(start) = tokens_str.find('"') {
if let Some(end) = tokens_str.rfind('"') {
if start < end {
return Some(tokens_str[start + 1..end].to_string());
}
}
}
}
}
}
}
None
}
fn apply_rename_case(name: &str, rename_case: &Option<String>) -> String {
match rename_case.as_deref() {
Some("camelCase") => to_camel_case(name),
Some("snake_case") => to_snake_case(name),
Some("kebab-case") => to_kebab_case(name),
Some("SCREAMING_SNAKE_CASE") => to_screaming_snake_case(name),
Some("PascalCase") => to_pascal_case(name),
Some("SCREAMING-KEBAB-CASE") => to_screaming_kebab_case(name),
_ => name.to_string(),
}
}
fn to_camel_case(s: &str) -> String {
if s.is_empty() {
return s.to_string();
}
let mut result = String::new();
let mut chars = s.chars();
if let Some(first) = chars.next() {
result.push(first.to_lowercase().next().unwrap_or(first));
}
result.extend(chars);
result
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = s.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c.is_uppercase() {
if i > 0 {
let prev_was_lower = chars[i - 1].is_lowercase();
let next_is_lower = i + 1 < chars.len() && chars[i + 1].is_lowercase();
if prev_was_lower || next_is_lower {
result.push('_');
}
}
result.push(c.to_lowercase().next().unwrap_or(c));
} else {
result.push(c);
}
}
result
}
fn to_kebab_case(s: &str) -> String {
to_snake_case(s).replace('_', "-")
}
fn to_screaming_snake_case(s: &str) -> String {
to_snake_case(s).to_uppercase()
}
fn to_pascal_case(s: &str) -> String {
s.to_string()
}
fn to_screaming_kebab_case(s: &str) -> String {
to_kebab_case(s).to_uppercase()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_camel_case() {
assert_eq!(to_camel_case("MessageOne"), "messageOne");
assert_eq!(to_camel_case("MessageTwo"), "messageTwo");
assert_eq!(to_camel_case("A"), "a");
assert_eq!(to_camel_case(""), "");
}
#[test]
fn test_snake_case() {
assert_eq!(to_snake_case("MessageOne"), "message_one");
assert_eq!(to_snake_case("MessageTwo"), "message_two");
assert_eq!(to_snake_case("XMLHttpRequest"), "xml_http_request");
}
#[test]
fn test_kebab_case() {
assert_eq!(to_kebab_case("MessageOne"), "message-one");
assert_eq!(to_kebab_case("MessageTwo"), "message-two");
}
#[test]
fn test_screaming_snake_case() {
assert_eq!(to_screaming_snake_case("MessageOne"), "MESSAGE_ONE");
assert_eq!(to_screaming_snake_case("MessageTwo"), "MESSAGE_TWO");
}
}