use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, Data, DeriveInput, Fields, Ident, ItemFn, Lit, LitInt, LitStr, Token,
};
mod handler;
fn annotate(method: &str, path: LitStr, func: ItemFn) -> TokenStream {
handler::expand(method, path, func).into()
}
struct RouteArgs {
method: Ident,
path: LitStr,
}
impl Parse for RouteArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let method: Ident = input.parse()?;
input.parse::<Token![,]>()?;
let path: LitStr = input.parse()?;
Ok(RouteArgs { method, path })
}
}
#[proc_macro_attribute]
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
let RouteArgs { method, path } = parse_macro_input!(args as RouteArgs);
let func = parse_macro_input!(input as ItemFn);
annotate(&method.to_string(), path, func)
}
#[proc_macro_attribute]
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
let path = parse_macro_input!(args as LitStr);
let func = parse_macro_input!(input as ItemFn);
annotate("GET", path, func)
}
#[proc_macro_attribute]
pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
let path = parse_macro_input!(args as LitStr);
let func = parse_macro_input!(input as ItemFn);
annotate("POST", path, func)
}
#[proc_macro_attribute]
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
let path = parse_macro_input!(args as LitStr);
let func = parse_macro_input!(input as ItemFn);
annotate("PUT", path, func)
}
#[proc_macro_attribute]
pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
let path = parse_macro_input!(args as LitStr);
let func = parse_macro_input!(input as ItemFn);
annotate("PATCH", path, func)
}
#[proc_macro_attribute]
pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
let path = parse_macro_input!(args as LitStr);
let func = parse_macro_input!(input as ItemFn);
annotate("DELETE", path, func)
}
#[proc_macro_derive(FromPath)]
pub fn derive_from_path(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
handler::derive_from_path(ast)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
#[proc_macro_derive(FromRequest)]
pub fn derive_from_request(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_from_request(ast)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
#[proc_macro_derive(Validate, attributes(validate))]
pub fn derive_validate(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_validate(ast)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
fn impl_validate(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let span = input.ident.span();
let name = &input.ident;
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(f) => &f.named,
_ => {
return Err(syn::Error::new(
span,
"#[derive(Validate)] only supports structs with named fields",
))
}
},
_ => {
return Err(syn::Error::new(
span,
"#[derive(Validate)] can only be derived on structs",
))
}
};
let mut all_checks: Vec<proc_macro2::TokenStream> = Vec::new();
for field in fields {
let field_ident = field.ident.as_ref().unwrap();
let field_name = field_ident.to_string();
for attr in &field.attrs {
if attr.path().is_ident("validate") {
let checks = generate_field_checks(attr, field_ident, &field_name)?;
all_checks.extend(checks);
}
}
}
Ok(quote! {
const _: () = {
use ::rust_web_server as _rws;
impl _rws::validate::Validate for #name {
fn validate(&self) -> ::core::result::Result<(), _rws::validate::ValidationErrors> {
let mut __errors = _rws::validate::ValidationErrors::new();
#(#all_checks)*
if __errors.is_empty() {
::core::result::Result::Ok(())
} else {
::core::result::Result::Err(__errors)
}
}
}
};
})
}
fn generate_field_checks(
attr: &syn::Attribute,
field_ident: &syn::Ident,
field_name: &str,
) -> syn::Result<Vec<proc_macro2::TokenStream>> {
let mut checks: Vec<proc_macro2::TokenStream> = Vec::new();
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("email") {
let msg = format!("{field_name} must be a valid email address");
checks.push(quote! {
if !_rws::validate::is_email(&self.#field_ident) {
__errors.add(#field_name, #msg);
}
});
} else if meta.path.is_ident("required") {
let msg = format!("{field_name} must not be empty");
checks.push(quote! {
if self.#field_ident.is_empty() {
__errors.add(#field_name, #msg);
}
});
} else if meta.path.is_ident("url") {
let msg = format!("{field_name} must be a valid URL (http:// or https://)");
checks.push(quote! {
if !_rws::validate::is_url(&self.#field_ident) {
__errors.add(#field_name, #msg);
}
});
} else if meta.path.is_ident("length") {
let mut min: Option<u64> = None;
let mut max: Option<u64> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("min") {
let lit: LitInt = inner.value()?.parse()?;
min = Some(lit.base10_parse()?);
} else if inner.path.is_ident("max") {
let lit: LitInt = inner.value()?.parse()?;
max = Some(lit.base10_parse()?);
} else {
return Err(inner.error("expected `min` or `max`"));
}
Ok(())
})?;
let mut len_checks: Vec<proc_macro2::TokenStream> = Vec::new();
if let Some(n) = min {
let msg = format!("{field_name} must be at least {n} character(s) long");
let n_lit = proc_macro2::Literal::usize_suffixed(n as usize);
len_checks.push(quote! {
if __len < #n_lit { __errors.add(#field_name, #msg); }
});
}
if let Some(n) = max {
let msg = format!("{field_name} must be at most {n} character(s) long");
let n_lit = proc_macro2::Literal::usize_suffixed(n as usize);
len_checks.push(quote! {
if __len > #n_lit { __errors.add(#field_name, #msg); }
});
}
checks.push(quote! {
{
let __len = self.#field_ident.chars().count();
#(#len_checks)*
}
});
} else if meta.path.is_ident("range") {
let mut min: Option<f64> = None;
let mut max: Option<f64> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("min") {
min = Some(lit_to_f64(&inner.value()?.parse::<Lit>()?)?);
} else if inner.path.is_ident("max") {
max = Some(lit_to_f64(&inner.value()?.parse::<Lit>()?)?);
} else {
return Err(inner.error("expected `min` or `max`"));
}
Ok(())
})?;
let mut rng_checks: Vec<proc_macro2::TokenStream> = Vec::new();
if let Some(n) = min {
let msg = format!("{field_name} must be at least {n}");
let n_lit = proc_macro2::Literal::f64_suffixed(n);
rng_checks.push(quote! {
if __val < #n_lit { __errors.add(#field_name, #msg); }
});
}
if let Some(n) = max {
let msg = format!("{field_name} must be at most {n}");
let n_lit = proc_macro2::Literal::f64_suffixed(n);
rng_checks.push(quote! {
if __val > #n_lit { __errors.add(#field_name, #msg); }
});
}
checks.push(quote! {
{
let __val = self.#field_ident as f64;
#(#rng_checks)*
}
});
} else {
return Err(meta.error(
"unknown validator; expected: email, required, url, length, range",
));
}
Ok(())
})?;
Ok(checks)
}
fn lit_to_f64(lit: &Lit) -> syn::Result<f64> {
match lit {
Lit::Float(f) => Ok(f.base10_parse()?),
Lit::Int(i) => Ok(i.base10_parse::<i64>()? as f64),
_ => Err(syn::Error::new_spanned(lit, "expected a numeric literal")),
}
}
fn impl_from_request(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let span = input.ident.span();
let name = &input.ident;
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(f) => &f.named,
_ => {
return Err(syn::Error::new(
span,
"#[derive(FromRequest)] only supports structs with named fields",
))
}
},
_ => {
return Err(syn::Error::new(
span,
"#[derive(FromRequest)] can only be derived on structs",
))
}
};
let extractions = fields.iter().map(|f| {
let ident = f.ident.as_ref().unwrap();
let ty = &f.ty;
quote! {
let #ident = <#ty as _rws::extract::FromRequest>::from_request(__req)?;
}
});
let field_names = fields.iter().map(|f| f.ident.as_ref().unwrap());
Ok(quote! {
const _: () = {
use ::rust_web_server as _rws;
impl _rws::extract::FromRequest for #name {
fn from_request(__req: &_rws::request::Request) -> ::core::result::Result<Self, _rws::response::Response> {
#(#extractions)*
::core::result::Result::Ok(#name { #(#field_names),* })
}
}
};
})
}
#[proc_macro_derive(Config, attributes(config))]
pub fn derive_config(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_config(ast)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
fn impl_config(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let span = input.ident.span();
let name = &input.ident;
let prefix = parse_config_prefix(&input.attrs)?;
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(f) => &f.named,
_ => {
return Err(syn::Error::new(
span,
"#[derive(Config)] only supports structs with named fields",
))
}
},
_ => {
return Err(syn::Error::new(
span,
"#[derive(Config)] can only be derived on structs",
))
}
};
let mut field_loads: Vec<proc_macro2::TokenStream> = Vec::new();
let mut field_names: Vec<&syn::Ident> = Vec::new();
for field in fields {
let ident = field.ident.as_ref().unwrap();
field_names.push(ident);
let (env_key, default) = parse_field_config(&field.attrs, ident, &prefix)?;
let is_option = is_option_type(&field.ty);
let load_expr = if is_option {
quote! {
_rws::config_binding::load_optional(&#env_key)?
}
} else if let Some(default_str) = default {
quote! {
_rws::config_binding::load_with_default(&#env_key, #default_str)?
}
} else {
quote! {
_rws::config_binding::load_required(&#env_key)?
}
};
field_loads.push(quote! {
let #ident = #load_expr;
});
}
Ok(quote! {
impl #name {
pub fn load() -> ::core::result::Result<Self, ::std::string::String> {
use ::rust_web_server as _rws;
#(#field_loads)*
::core::result::Result::Ok(#name {
#(#field_names),*
})
}
}
})
}
fn parse_field_config(
attrs: &[syn::Attribute],
ident: &syn::Ident,
prefix: &str,
) -> syn::Result<(proc_macro2::TokenStream, Option<LitStr>)> {
let mut env_name: Option<String> = None;
let mut default: Option<LitStr> = None;
for attr in attrs {
if !attr.path().is_ident("config") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("env") {
let lit: LitStr = meta.value()?.parse()?;
env_name = Some(lit.value());
} else if meta.path.is_ident("default") {
default = Some(meta.value()?.parse()?);
} else {
return Err(meta.error("unknown config key; expected `env` or `default`"));
}
Ok(())
})?;
}
let key = format!(
"{}{}",
prefix,
env_name.unwrap_or_else(|| ident.to_string().to_uppercase())
);
Ok((quote! { #key }, default))
}
fn parse_config_prefix(attrs: &[syn::Attribute]) -> syn::Result<String> {
let mut prefix = String::new();
for attr in attrs {
if !attr.path().is_ident("config") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("prefix") {
let lit: LitStr = meta.value()?.parse()?;
prefix = lit.value();
} else {
return Err(meta.error("unknown struct config key; expected `prefix`"));
}
Ok(())
})?;
}
Ok(prefix)
}
fn is_option_type(ty: &syn::Type) -> bool {
if let syn::Type::Path(tp) = ty {
if let Some(seg) = tp.path.segments.last() {
return seg.ident == "Option";
}
}
false
}
#[proc_macro_derive(Model, attributes(table, column, primary_key, timestamps, soft_delete, version))]
pub fn derive_model(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_model(ast)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
fn impl_model(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let span = input.ident.span();
let struct_name = &input.ident;
let table_name = parse_table_name(&input.attrs, struct_name)?;
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(f) => &f.named,
_ => {
return Err(syn::Error::new(
span,
"#[derive(Model)] only supports structs with named fields",
))
}
},
_ => {
return Err(syn::Error::new(
span,
"#[derive(Model)] can only be derived on structs",
))
}
};
struct FieldMeta {
field_ident: syn::Ident,
col_name: String,
#[allow(dead_code)]
is_pk: bool,
is_auto_increment: bool,
is_ignored: bool,
is_version: bool,
ty: syn::Type,
}
let mut pk_field: Option<FieldMeta> = None;
let mut regular_fields: Vec<FieldMeta> = Vec::new();
for field in fields {
let ident = field.ident.as_ref().unwrap().clone();
let mut col_name = ident.to_string();
let mut is_pk = false;
let mut is_auto_increment = false;
let mut is_ignored = false;
let mut is_version = false;
for attr in &field.attrs {
if attr.path().is_ident("ignore") {
is_ignored = true;
} else if attr.path().is_ident("version") {
is_version = true;
} else if attr.path().is_ident("primary_key") {
is_pk = true;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("auto_increment") {
is_auto_increment = true;
}
Ok(())
});
} else if attr.path().is_ident("column") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let lit: LitStr = meta.value()?.parse()?;
col_name = lit.value();
}
Ok(())
});
}
}
let meta = FieldMeta {
field_ident: ident,
col_name,
is_pk,
is_auto_increment,
is_ignored,
is_version,
ty: field.ty.clone(),
};
if is_version && (is_pk || is_ignored) {
return Err(syn::Error::new_spanned(field, "#[version] goes on a mapped integer field, not the #[primary_key] or an #[ignore]d one"));
}
if is_pk {
pk_field = Some(meta);
} else {
regular_fields.push(meta);
}
}
let pk = pk_field.ok_or_else(|| {
syn::Error::new(span, "#[derive(Model)] requires exactly one #[primary_key] field")
})?;
let has_struct_attr = |name: &str| input.attrs.iter().any(|a| a.path().is_ident(name));
let column_of = |field: &str, attr: &str, must_be_option: bool| -> syn::Result<String> {
let found = regular_fields.iter().find(|f| f.field_ident == field && !f.is_ignored).ok_or_else(|| {
syn::Error::new(span, format!("#[{attr}] needs a mapped field named `{field}` (e.g. `{field}: {}`)", if must_be_option { "Option<Timestamp>" } else { "Timestamp" }))
})?;
if must_be_option && !is_option_type(&found.ty) {
return Err(syn::Error::new_spanned(&found.ty, format!("#[{attr}]: `{field}` must be an Option (NULL means not deleted), e.g. Option<Timestamp>")));
}
Ok(found.col_name.clone())
};
let opt = |name: &str, c: Option<String>| -> proc_macro2::TokenStream {
let name = syn::Ident::new(name, proc_macro2::Span::call_site());
match c {
Some(c) => quote! {
fn #name() -> ::core::option::Option<&'static str> {
::core::option::Option::Some(#c)
}
},
None => quote! {},
}
};
let (created_at, updated_at) = if has_struct_attr("timestamps") {
(Some(column_of("created_at", "timestamps", false)?), Some(column_of("updated_at", "timestamps", false)?))
} else {
(None, None)
};
let deleted_at = if has_struct_attr("soft_delete") { Some(column_of("deleted_at", "soft_delete", true)?) } else { None };
let versions: Vec<&FieldMeta> = regular_fields.iter().filter(|f| f.is_version).collect();
if versions.len() > 1 {
return Err(syn::Error::new(span, "#[derive(Model)] takes at most one #[version] field"));
}
let version = versions.first().map(|f| f.col_name.clone());
let (created_at, updated_at, deleted_at, version) = (
opt("created_at_column", created_at),
opt("updated_at_column", updated_at),
opt("soft_delete_column", deleted_at),
opt("version_column", version),
);
let pk_ty = &pk.ty;
let is_integer = matches!(pk_ty, syn::Type::Path(tp) if tp.qself.is_none() && tp.path.segments.len() == 1
&& ["i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", "usize"].iter().any(|t| tp.path.segments[0].ident == t));
let repo_key = if is_integer { quote! { i64 } } else { quote! { #pk_ty } };
let mut all_col_names: Vec<String> = Vec::new();
all_col_names.push(pk.col_name.clone());
for f in regular_fields.iter().filter(|f| !f.is_ignored) {
all_col_names.push(f.col_name.clone());
}
let pk_col_name = pk.col_name.clone();
let pk_field_ident = &pk.field_ident;
let auto_inc = pk.is_auto_increment;
let pk_from_row = {
let col = &pk.col_name;
let fident = &pk.field_ident;
quote! { #fident: row.get(#col)? }
};
let regular_from_row: Vec<proc_macro2::TokenStream> = fields
.iter()
.filter(|f| {
let ident_str = f.ident.as_ref().unwrap().to_string();
let is_pk_field = ident_str == pk_field_ident.to_string();
!is_pk_field
})
.map(|f| {
let fident = f.ident.as_ref().unwrap();
let is_ignored = f.attrs.iter().any(|a| a.path().is_ident("ignore"));
if is_ignored {
quote! { #fident: ::core::default::Default::default() }
} else {
let mut col = fident.to_string();
for attr in &f.attrs {
if attr.path().is_ident("column") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
if let Ok(lit) = meta.value()?.parse::<LitStr>() {
col = lit.value();
}
}
Ok(())
});
}
}
quote! { #fident: row.get(#col)? }
}
})
.collect();
let pk_to_values = {
let col = &pk.col_name;
quote! {
(#col, ::rust_web_server::model::ToColumn::to_column(&self.#pk_field_ident))
}
};
let regular_to_values: Vec<proc_macro2::TokenStream> = fields
.iter()
.filter(|f| {
let ident_str = f.ident.as_ref().unwrap().to_string();
let is_pk_field = ident_str == pk_field_ident.to_string();
let is_ignored = f.attrs.iter().any(|a| a.path().is_ident("ignore"));
!is_pk_field && !is_ignored
})
.map(|f| {
let fident = f.ident.as_ref().unwrap();
let mut col = fident.to_string();
for attr in &f.attrs {
if attr.path().is_ident("column") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
if let Ok(lit) = meta.value()?.parse::<LitStr>() {
col = lit.value();
}
}
Ok(())
});
}
}
quote! {
(#col, ::rust_web_server::model::ToColumn::to_column(&self.#fident))
}
})
.collect();
Ok(quote! {
impl ::rust_web_server::model::Model for #struct_name {
fn table_name() -> &'static str {
#table_name
}
fn column_names() -> &'static [&'static str] {
&[#(#all_col_names),*]
}
fn primary_key_name() -> &'static str {
#pk_col_name
}
fn primary_key_value(&self) -> ::rust_web_server::model::Value {
::rust_web_server::model::ToColumn::to_column(&self.#pk_field_ident)
}
fn primary_key_auto_increment() -> bool {
#auto_inc
}
fn from_row(row: &::rust_web_server::model::ModelRow) -> ::core::result::Result<Self, ::rust_web_server::model::DbError> {
::core::result::Result::Ok(#struct_name {
#pk_from_row,
#(#regular_from_row),*
})
}
fn to_values(&self) -> ::std::vec::Vec<(&'static str, ::rust_web_server::model::Value)> {
vec![
#pk_to_values,
#(#regular_to_values),*
]
}
#created_at
#updated_at
#deleted_at
#version
}
impl #struct_name {
pub fn repository(pool: &::rust_web_server::model::DbPool) -> ::rust_web_server::model::ModelRepository<#struct_name, #repo_key> {
::rust_web_server::model::ModelRepository::new(pool)
}
pub fn query(pool: &::rust_web_server::model::DbPool) -> ::rust_web_server::model::QueryBuilder<#struct_name> {
::rust_web_server::model::QueryBuilder::new(pool)
}
}
})
}
fn parse_table_name(
attrs: &[syn::Attribute],
struct_name: &syn::Ident,
) -> syn::Result<String> {
for attr in attrs {
if attr.path().is_ident("table") {
let mut name: Option<String> = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let lit: LitStr = meta.value()?.parse()?;
name = Some(lit.value());
}
Ok(())
})?;
if let Some(n) = name {
return Ok(n);
}
}
}
Ok(struct_name.to_string().to_lowercase())
}
#[proc_macro_derive(Factory)]
pub fn derive_factory(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_factory(ast)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
fn impl_factory(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let span = input.ident.span();
let struct_name = &input.ident;
if !input.generics.params.is_empty() {
return Err(syn::Error::new(span, "#[derive(Factory)] doesn't support generic structs"));
}
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(f) => &f.named,
_ => return Err(syn::Error::new(span, "#[derive(Factory)] only supports structs with named fields")),
},
_ => return Err(syn::Error::new(span, "#[derive(Factory)] can only be derived on structs")),
};
let mut has_pk = false;
let mut inits: Vec<proc_macro2::TokenStream> = Vec::new();
for field in fields {
let ident = field.ident.as_ref().unwrap();
let ty = &field.ty;
let name = ident.to_string();
let name = name.strip_prefix("r#").unwrap_or(&name).to_string();
let mut is_pk = false;
let mut auto_increment = false;
let mut ignored = false;
for attr in &field.attrs {
if attr.path().is_ident("ignore") {
ignored = true;
} else if attr.path().is_ident("primary_key") {
is_pk = true;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("auto_increment") {
auto_increment = true;
}
Ok(())
});
}
}
has_pk |= is_pk;
let probe = quote! { (&&__Probe::<#ty>(::core::marker::PhantomData)) };
inits.push(if ignored {
quote! { #ident: ::core::default::Default::default() }
} else if is_pk && !auto_increment {
quote! { #ident: #probe.make_key(#name, __seq) }
} else {
quote! { #ident: #probe.make(#name, __seq) }
});
}
if !has_pk {
return Err(syn::Error::new(span, "#[derive(Factory)] needs the #[primary_key] field of #[derive(Model)]"));
}
Ok(quote! {
impl ::rust_web_server::test_client::factory::Factory for #struct_name {
fn factory_default(__seq: u64) -> Self {
#[allow(unused_imports)]
use ::rust_web_server::test_client::factory::__private::{
Probe as __Probe, ViaDefault as _, ViaValue as _,
};
#struct_name {
#(#inits),*
}
}
}
impl #struct_name {
pub fn factory() -> ::rust_web_server::test_client::factory::ModelFactory<#struct_name> {
::rust_web_server::test_client::factory::ModelFactory::new()
}
}
})
}
#[proc_macro_derive(JsonSchema, attributes(serde))]
pub fn derive_json_schema(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_json_schema(ast)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}
fn impl_json_schema(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let name = &input.ident;
let container = SerdeAttrs::parse(&input.attrs)?;
let description = doc_comment(&input.attrs);
let body = match &input.data {
Data::Struct(s) => {
let Fields::Named(fields) = &s.fields else {
return Err(syn::Error::new_spanned(
name,
"#[derive(JsonSchema)] supports structs with named fields and enums of unit variants",
));
};
let mut properties = Vec::new();
for field in &fields.named {
let attrs = SerdeAttrs::parse(&field.attrs)?;
if attrs.skip {
continue;
}
let ident = field.ident.as_ref().expect("named field");
let json_name = attrs.rename.clone().unwrap_or_else(|| {
rename_with(ident.to_string().trim_start_matches("r#"), container.rename_all.as_deref(), false)
});
let ty = &field.ty;
let field_doc = doc_comment(&field.attrs);
let may_be_absent = attrs.default || container.default || attrs.skip_serializing_if;
properties.push(quote! {
__schema.property(
#json_name,
&_rws::schema::nested::<#ty>(),
#field_doc,
!#may_be_absent && !<#ty as _rws::schema::JsonSchema>::is_optional(),
);
});
}
quote! {
let mut __schema = _rws::schema::ObjectSchema::new(#description);
#(#properties)*
__schema.finish()
}
}
Data::Enum(e) => {
let mut values = Vec::new();
for variant in &e.variants {
if !matches!(variant.fields, Fields::Unit) {
return Err(syn::Error::new_spanned(
variant,
"#[derive(JsonSchema)] supports only unit enum variants",
));
}
let attrs = SerdeAttrs::parse(&variant.attrs)?;
if attrs.skip {
continue;
}
values.push(attrs.rename.unwrap_or_else(|| {
rename_with(&variant.ident.to_string(), container.rename_all.as_deref(), true)
}));
}
quote! { _rws::schema::string_enum(#description, &[#(#values),*]) }
}
Data::Union(_) => {
return Err(syn::Error::new_spanned(name, "#[derive(JsonSchema)] does not support unions"));
}
};
let mut generics = input.generics.clone();
for param in generics.type_params_mut() {
param.bounds.push(syn::parse_quote!(::rust_web_server::schema::JsonSchema));
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
const _: () = {
use ::rust_web_server as _rws;
impl #impl_generics _rws::schema::JsonSchema for #name #ty_generics #where_clause {
fn json_schema() -> ::std::string::String {
#body
}
}
};
})
}
#[derive(Default)]
struct SerdeAttrs {
rename: Option<String>,
rename_all: Option<String>,
default: bool,
skip: bool,
skip_serializing_if: bool,
}
impl SerdeAttrs {
fn parse(attrs: &[syn::Attribute]) -> syn::Result<Self> {
let mut out = SerdeAttrs::default();
for attr in attrs.iter().filter(|a| a.path().is_ident("serde")) {
attr.parse_nested_meta(|meta| {
let key = meta.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
match key.as_str() {
"rename" | "rename_all" => {
if !meta.input.peek(Token![=]) {
return Err(meta.error(format!(
"#[derive(JsonSchema)] needs one `{key}` for both directions (one schema describes input and output)"
)));
}
let value = meta.value()?.parse::<LitStr>()?.value();
if key == "rename" {
out.rename = Some(value);
} else {
if rename_with("a_b", Some(&value), false).is_empty() {
return Err(meta.error(format!("unknown rename_all rule \"{value}\"")));
}
out.rename_all = Some(value);
}
}
"default" => {
out.default = true;
if meta.input.peek(Token![=]) {
meta.value()?.parse::<LitStr>()?;
}
}
"skip" | "skip_serializing" | "skip_deserializing" => out.skip = true,
"skip_serializing_if" => {
out.skip_serializing_if = true;
meta.value()?.parse::<LitStr>()?;
}
"flatten" | "tag" | "content" | "untagged" | "transparent" | "with" | "serialize_with"
| "deserialize_with" | "remote" | "from" | "try_from" | "into" | "getter" => {
return Err(meta.error(format!(
"#[derive(JsonSchema)] does not support #[serde({key})]: the schema would not match the JSON"
)));
}
_ => {
if meta.input.peek(Token![=]) {
meta.value()?.parse::<proc_macro2::TokenStream>()?;
} else if meta.input.peek(syn::token::Paren) {
meta.parse_nested_meta(|nested| {
if nested.input.peek(Token![=]) {
nested.value()?.parse::<proc_macro2::TokenTree>()?;
}
Ok(())
})?;
}
}
}
Ok(())
})?;
}
Ok(out)
}
}
fn rename_with(name: &str, rule: Option<&str>, variant: bool) -> String {
let Some(rule) = rule else { return name.to_string() };
let words: Vec<String> = if variant {
let mut words = Vec::new();
let mut current = String::new();
for c in name.chars() {
if c.is_uppercase() && !current.is_empty() {
words.push(std::mem::take(&mut current));
}
current.extend(c.to_lowercase());
}
if !current.is_empty() {
words.push(current);
}
words
} else {
name.split('_').map(str::to_string).collect()
};
let capitalize = |w: &String| {
let mut chars = w.chars();
chars.next().map(|c| c.to_uppercase().chain(chars).collect::<String>()).unwrap_or_default()
};
match rule {
"lowercase" => words.concat(),
"UPPERCASE" => words.concat().to_uppercase(),
"PascalCase" => words.iter().map(capitalize).collect(),
"camelCase" => {
let pascal: String = words.iter().map(capitalize).collect();
let mut chars = pascal.chars();
chars.next().map(|c| c.to_lowercase().chain(chars).collect()).unwrap_or_default()
}
"snake_case" => words.join("_"),
"SCREAMING_SNAKE_CASE" => words.join("_").to_uppercase(),
"kebab-case" => words.join("-"),
"SCREAMING-KEBAB-CASE" => words.join("-").to_uppercase(),
_ => String::new(),
}
}
fn doc_comment(attrs: &[syn::Attribute]) -> String {
let mut lines = Vec::new();
for attr in attrs.iter().filter(|a| a.path().is_ident("doc")) {
if let syn::Meta::NameValue(nv) = &attr.meta {
if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value {
let line = s.value().trim().to_string();
if !line.is_empty() {
lines.push(line);
}
}
}
}
lines.join(" ")
}
#[cfg(test)]
mod tests {
use super::rename_with;
#[test]
fn rename_all_rules_match_serde() {
let field = |rule| rename_with("line_item_id", Some(rule), false);
assert_eq!("lineitemid", field("lowercase"));
assert_eq!("LINEITEMID", field("UPPERCASE"));
assert_eq!("LineItemId", field("PascalCase"));
assert_eq!("lineItemId", field("camelCase"));
assert_eq!("line_item_id", field("snake_case"));
assert_eq!("LINE_ITEM_ID", field("SCREAMING_SNAKE_CASE"));
assert_eq!("line-item-id", field("kebab-case"));
assert_eq!("LINE-ITEM-ID", field("SCREAMING-KEBAB-CASE"));
let variant = |rule| rename_with("BackOrdered", Some(rule), true);
assert_eq!("backordered", variant("lowercase"));
assert_eq!("backOrdered", variant("camelCase"));
assert_eq!("back_ordered", variant("snake_case"));
assert_eq!("BACK-ORDERED", variant("SCREAMING-KEBAB-CASE"));
assert_eq!("BackOrdered", variant("PascalCase"));
assert_eq!("as_is", rename_with("as_is", None, false));
assert_eq!("", rename_with("a_b", Some("Train-Case"), false));
}
}