Skip to content

Commit

Permalink
Add support for emitting #[schemars(...)] attributes to #[serde_as]
Browse files Browse the repository at this point in the history
This commit adds support for emitting

    #[schemars(with = "::serde_with::Schema<T, ...>")]

field annotations when any of the following are true:
- there is a #[derive(JsonSchema)] attribute, or,
- the user explicitly requests it via #[serde_as(schemars)]

This requires a bunch of extra work to properly scan for a few different
possible paths to JsonSchema (see the added docs for details) and also
to properly handle the case where the derive is within a #[cfg_attr]. eg

    #[cfg_attr(feature = "blah", derive(schemars::JsonSchema))]

While rustc will evaluate the cfgs for attributes declared by derive
macros unfortunately it does not do the same for attribute macros so we
need to do it ourselves.

The code here started from jonasbb#623 as a base and all changes have been made
on top of that.

Co-Authored-By: Jonas Bushart <[email protected]>
  • Loading branch information
swlynch99 and jonasbb committed Dec 6, 2023
1 parent 1e8e4e7 commit cfcf4a7
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 4 deletions.
3 changes: 3 additions & 0 deletions serde_with_macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ proc-macro = true
[badges]
maintenance = {status = "actively-developed"}

[features]
schemars_0_8 = []

[dependencies]
darling = "0.20.0"
proc-macro2 = "1.0.1"
Expand Down
62 changes: 59 additions & 3 deletions serde_with_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ extern crate proc_macro;
mod apply;
mod utils;

use crate::utils::{split_with_de_lifetime, DeriveOptions, IteratorExt as _};
use crate::utils::{split_with_de_lifetime, DeriveOptions, IteratorExt as _, SchemaFieldConfig};
use darling::{
ast::NestedMeta,
util::{Flag, Override},
Expand Down Expand Up @@ -590,6 +590,22 @@ fn field_has_attribute(field: &Field, namespace: &str, name: &str) -> bool {
/// }
/// ```
///
/// # A note on `schemars` integration
/// When the `schemars_0_8` feature is enabled this macro will scan for
/// `#[derive(JsonSchema)]` attributes and, if found, will add
/// `#[schemars(with = "Schema<T, ...>")]` annotations to any fields with a
/// `#[serde_as(as = ...)]` annotation. If you wish to override the default
/// behavior here you can add `#[serde_as(schemars = true)]` or
/// `#[serde_as(schemars = false)]`.
///
/// Note that this macro will check for any of the following derive paths:
/// * `JsonSchema`
/// * `schemars::JsonSchema`
/// * `::schemars::JsonSchema`
///
/// It will also work if the relevant derive is behind a `#[cfg_attr]` attribute
/// and propagate the `#[cfg_attr]` to the various `#[schemars]` field attributes.
///
/// [`serde_as`]: https://docs.rs/serde_with/3.4.0/serde_with/guide/index.html
/// [re-exporting `serde_as`]: https://docs.rs/serde_with/3.4.0/serde_with/guide/serde_as/index.html#re-exporting-serde_as
#[proc_macro_attribute]
Expand All @@ -598,6 +614,8 @@ pub fn serde_as(args: TokenStream, input: TokenStream) -> TokenStream {
struct SerdeContainerOptions {
#[darling(rename = "crate")]
alt_crate_path: Option<Path>,
#[darling(rename = "schemars")]
enable_schemars_support: Option<bool>,
}

match NestedMeta::parse_meta_list(args.into()) {
Expand All @@ -613,11 +631,18 @@ pub fn serde_as(args: TokenStream, input: TokenStream) -> TokenStream {
.alt_crate_path
.unwrap_or_else(|| syn::parse_quote!(::serde_with));

let schemars_config = match container_options.enable_schemars_support {
_ if cfg!(not(feature = "schemars_0_8")) => SchemaFieldConfig::Disabled,
Some(true) => SchemaFieldConfig::Unconditional,
Some(false) => SchemaFieldConfig::Disabled,
None => crate::utils::has_derive_jsonschema(input.clone()),
};

// Convert any error message into a nice compiler error
let res = match apply_function_to_struct_and_enum_fields_darling(
input,
&serde_with_crate_path,
|field| serde_as_add_attr_to_field(field, &serde_with_crate_path),
|field| serde_as_add_attr_to_field(field, &serde_with_crate_path, &schemars_config),
) {
Ok(res) => res,
Err(err) => err.write_errors(),
Expand All @@ -632,10 +657,14 @@ pub fn serde_as(args: TokenStream, input: TokenStream) -> TokenStream {
fn serde_as_add_attr_to_field(
field: &mut Field,
serde_with_crate_path: &Path,
schemars_config: &SchemaFieldConfig,
) -> Result<(), DarlingError> {
#[derive(FromField)]
#[darling(attributes(serde_as))]
struct SerdeAsOptions {
/// The original type of the field
ty: Type,

r#as: Option<Type>,
deserialize_as: Option<Type>,
serialize_as: Option<Type>,
Expand Down Expand Up @@ -747,6 +776,7 @@ fn serde_as_add_attr_to_field(
return Err(DarlingError::multiple(errors));
}

let type_original = &serde_as_options.ty;
let type_same = &syn::parse_quote!(#serde_with_crate_path::Same);
if let Some(type_) = &serde_as_options.r#as {
emit_borrow_annotation(&serde_options, type_, field);
Expand All @@ -756,6 +786,14 @@ fn serde_as_add_attr_to_field(
let attr_inner_tokens = quote!(#serde_with_crate_path::As::<#replacement_type>).to_string();
let attr = parse_quote!(#[serde(with = #attr_inner_tokens)]);
field.attrs.push(attr);

if let Some(cfg) = schemars_config.cfg_expr() {
let attr_inner_tokens =
quote!(#serde_with_crate_path::Schema::<#type_original, #replacement_type>)
.to_string();
let attr = parse_quote!(#[cfg_attr(#cfg, schemars(with = #attr_inner_tokens))]);
field.attrs.push(attr);
}
}
if let Some(type_) = &serde_as_options.deserialize_as {
emit_borrow_annotation(&serde_options, type_, field);
Expand All @@ -766,13 +804,31 @@ fn serde_as_add_attr_to_field(
quote!(#serde_with_crate_path::As::<#replacement_type>::deserialize).to_string();
let attr = parse_quote!(#[serde(deserialize_with = #attr_inner_tokens)]);
field.attrs.push(attr);

if let Some(cfg) = schemars_config.cfg_expr() {
let attr_inner_tokens =
quote!(#serde_with_crate_path::Schema::<#type_original, #replacement_type>::deserialize)
.to_string();
let attr =
parse_quote!(#[cfg_attr(#cfg, schemars(deserialize_with = #attr_inner_tokens))]);
field.attrs.push(attr);
}
}
if let Some(type_) = serde_as_options.serialize_as {
let replacement_type = replace_infer_type_with_type(type_, type_same);
let replacement_type = replace_infer_type_with_type(type_.clone(), type_same);
let attr_inner_tokens =
quote!(#serde_with_crate_path::As::<#replacement_type>::serialize).to_string();
let attr = parse_quote!(#[serde(serialize_with = #attr_inner_tokens)]);
field.attrs.push(attr);

if let Some(cfg) = schemars_config.cfg_expr() {
let attr_inner_tokens =
quote!(#serde_with_crate_path::Schema::<#type_original, #replacement_type>::serialize)
.to_string();
let attr =
parse_quote!(#[cfg_attr(#cfg, schemars(serialize_with = #attr_inner_tokens))]);
field.attrs.push(attr);
}
}

Ok(())
Expand Down
107 changes: 106 additions & 1 deletion serde_with_macros/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use darling::FromDeriveInput;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::ToTokens;
use syn::{parse_quote, Error, Generics, Path, TypeGenerics};
use syn::{parse_quote, punctuated::Punctuated, Error, Generics, Path, TypeGenerics};

/// Merge multiple [`syn::Error`] into one.
pub(crate) trait IteratorExt {
Expand Down Expand Up @@ -75,3 +75,108 @@ impl<'a> ToTokens for DeImplGenerics<'a> {
impl_generics.to_tokens(tokens);
}
}

/// Determine if there is a `#[derive(JsonSchema)]` on this struct.
pub(crate) fn has_derive_jsonschema(input: TokenStream) -> SchemaFieldConfig {
/// Represents the macro body of a `#[cfg_attr]` attribute.
///
/// ```text
/// #[cfg_attr(feature = "things", derive(Macro))]
/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/// ```
struct CfgAttr {
cfg: syn::Expr,
_comma: syn::Token![,],
meta: syn::Meta,
}

impl syn::parse::Parse for CfgAttr {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
cfg: input.parse()?,
_comma: input.parse()?,
meta: input.parse()?,
})
}
}

fn parse_derive_args(
input: syn::parse::ParseStream<'_>,
) -> syn::Result<Punctuated<Path, syn::Token![,]>> {
Punctuated::parse_terminated_with(input, Path::parse_mod_style)
}

fn eval_attribute(attr: &syn::Attribute) -> syn::Result<SchemaFieldConfig> {
let args: CfgAttr;
let mut meta = &attr.meta;
let mut config = SchemaFieldConfig::Unconditional;

if meta.path().is_ident("cfg_attr") {
let list = meta.require_list()?;
args = list.parse_args()?;

meta = &args.meta;
config = SchemaFieldConfig::Conditional(args.cfg);
}

let list = meta.require_list()?;
if !list.path.is_ident("derive") {
return Ok(SchemaFieldConfig::Disabled);
}

let derives = list.parse_args_with(parse_derive_args)?;
for derive in &derives {
let segments = &derive.segments;

// Check for $(::)? $(schemars::)? JsonSchema
match segments.len() {
1 if segments[0].ident == "JsonSchema" => (),
2 if segments[0].ident == "schemars" && segments[1].ident == "JsonSchema" => (),
_ => continue,
}

return Ok(config);
}

Ok(SchemaFieldConfig::Disabled)
}

let input: syn::DeriveInput = match syn::parse(input) {
Ok(input) => input,
Err(_) => return SchemaFieldConfig::Disabled,
};

for attr in input.attrs {
match eval_attribute(&attr) {
Ok(SchemaFieldConfig::Disabled) => continue,
Ok(config) => return config,
Err(_) => continue,
}
}

SchemaFieldConfig::Disabled
}

/// Enum controlling when we should emit a `#[schemars]` field attribute.
pub(crate) enum SchemaFieldConfig {
/// Emit a `#[cfg_attr(#cfg, schemars(...))]` attribute.
Conditional(syn::Expr),
/// Emit a `#[schemars(...)]` attribute (or equivalent).
Unconditional,
/// Do not emit an attribute.
Disabled,
}

impl SchemaFieldConfig {
/// Get a `#[cfg]` expression suitable for emitting the `#[schemars]` attribute.
///
/// If this config is `Unconditional` then it will just return `all()` which
/// is always true.
pub(crate) fn cfg_expr(&self) -> Option<syn::Expr> {
match self {
Self::Unconditional => Some(syn::parse_quote!(all())),
Self::Conditional(cfg) => Some(cfg.clone()),
Self::Disabled => None,
}
}
}

0 comments on commit cfcf4a7

Please sign in to comment.