diff --git a/cargo-typify/README.md b/cargo-typify/README.md index 28d8e555..a4c788ef 100644 --- a/cargo-typify/README.md +++ b/cargo-typify/README.md @@ -204,5 +204,8 @@ default). Builder output lets you write code like this: let xy: MyStruct = MyStruct::builder().x_coord(x).y_coord(y).try_into(); ``` -The `--additional-derive` adds the specified derive macro to all generated -types. This may be specified more than once. \ No newline at end of file +The `--additional-derive` option adds the specified derive macro to all generated +types. This may be specified more than once. + +The `--additional-attr` option adds the specified attribute to all generated +types. This may be specified more than once. diff --git a/cargo-typify/src/lib.rs b/cargo-typify/src/lib.rs index cb25e2ea..0e592d45 100644 --- a/cargo-typify/src/lib.rs +++ b/cargo-typify/src/lib.rs @@ -31,9 +31,13 @@ pub struct CliArgs { pub no_builder: bool, /// Add an additional derive macro to apply to all defined types. - #[arg(short, long = "additional-derive", value_name = "derive")] + #[arg(short = 'd', long = "additional-derive", value_name = "derive")] pub additional_derives: Vec, + /// Add an additional attribute to apply to all defined types. + #[arg(short = 'a', long = "additional-attr", value_name = "attr")] + pub additional_attrs: Vec, + /// The output file to write to. If not specified, the input file name will /// be used with a `.rs` extension. /// @@ -146,6 +150,10 @@ pub fn convert(args: &CliArgs) -> Result { settings.with_derive(derive.clone()); } + for attr in &args.additional_attrs { + settings.with_attr(attr.clone()); + } + for CrateSpec { name, version, @@ -197,6 +205,7 @@ mod tests { input: PathBuf::from("input.json"), builder: false, additional_derives: vec![], + additional_attrs: vec![], output: Some(PathBuf::from("-")), no_builder: false, crates: vec![], @@ -213,6 +222,7 @@ mod tests { input: PathBuf::from("input.json"), builder: false, additional_derives: vec![], + additional_attrs: vec![], output: Some(PathBuf::from("some_file.rs")), no_builder: false, crates: vec![], @@ -229,6 +239,7 @@ mod tests { input: PathBuf::from("input.json"), builder: false, additional_derives: vec![], + additional_attrs: vec![], output: None, no_builder: false, crates: vec![], @@ -245,6 +256,7 @@ mod tests { input: PathBuf::from("input.json"), builder: false, additional_derives: vec![], + additional_attrs: vec![], output: None, no_builder: false, crates: vec![], @@ -264,6 +276,7 @@ mod tests { input: PathBuf::from("input.json"), builder: false, additional_derives: vec![], + additional_attrs: vec![], output: None, no_builder: false, crates: vec![], @@ -280,6 +293,7 @@ mod tests { input: PathBuf::from("input.json"), builder: false, additional_derives: vec![], + additional_attrs: vec![], output: None, no_builder: true, crates: vec![], @@ -296,6 +310,7 @@ mod tests { input: PathBuf::from("input.json"), builder: true, additional_derives: vec![], + additional_attrs: vec![], output: None, no_builder: false, crates: vec![], diff --git a/cargo-typify/tests/integration.rs b/cargo-typify/tests/integration.rs index e3b65335..267994d1 100644 --- a/cargo-typify/tests/integration.rs +++ b/cargo-typify/tests/integration.rs @@ -103,6 +103,31 @@ fn test_derive() { assert_contents("tests/outputs/derive.rs", &actual); } +#[test] +fn test_attr() { + let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); + + let temp = TempDir::new("cargo-typify").unwrap(); + let output_file = temp.path().join("output.rs"); + + assert_cmd::cargo::cargo_bin_cmd!() + .args([ + "typify", + input, + "--no-builder", + "--additional-attr", + "#[extra_attr]", + "--output", + output_file.to_str().unwrap(), + ]) + .assert() + .success(); + + let actual = std::fs::read_to_string(output_file).unwrap(); + + assert_contents("tests/outputs/attr.rs", &actual); +} + #[test] fn test_multi_derive() { let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); diff --git a/cargo-typify/tests/outputs/attr.rs b/cargo-typify/tests/outputs/attr.rs new file mode 100644 index 00000000..401f354f --- /dev/null +++ b/cargo-typify/tests/outputs/attr.rs @@ -0,0 +1,211 @@ +#![allow(clippy::redundant_closure_call)] +#![allow(clippy::needless_lifetimes)] +#![allow(clippy::match_single_binding)] +#![allow(clippy::clone_on_copy)] + +#[doc = r" Error types."] +pub mod error { + #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } +} +#[doc = "`Fruit`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"object\","] +#[doc = " \"additionalProperties\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[extra_attr] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +#[serde(transparent)] +pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>); +impl ::std::ops::Deref for Fruit { + type Target = ::std::collections::HashMap<::std::string::String, ::std::string::String>; + fn deref(&self) -> &::std::collections::HashMap<::std::string::String, ::std::string::String> { + &self.0 + } +} +impl ::std::convert::From + for ::std::collections::HashMap<::std::string::String, ::std::string::String> +{ + fn from(value: Fruit) -> Self { + value.0 + } +} +impl ::std::convert::From<&Fruit> for Fruit { + fn from(value: &Fruit) -> Self { + value.clone() + } +} +impl ::std::convert::From<::std::collections::HashMap<::std::string::String, ::std::string::String>> + for Fruit +{ + fn from( + value: ::std::collections::HashMap<::std::string::String, ::std::string::String>, + ) -> Self { + Self(value) + } +} +#[doc = "`FruitOrVeg`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"oneOf\": ["] +#[doc = " {"] +#[doc = " \"title\": \"veg\","] +#[doc = " \"anyOf\": ["] +#[doc = " {"] +#[doc = " \"$ref\": \"#/defs/veggie\""] +#[doc = " }"] +#[doc = " ]"] +#[doc = " },"] +#[doc = " {"] +#[doc = " \"title\": \"fruit\","] +#[doc = " \"anyOf\": ["] +#[doc = " {"] +#[doc = " \"$ref\": \"#/defs/fruit\""] +#[doc = " }"] +#[doc = " ]"] +#[doc = " }"] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[extra_attr] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +#[serde(untagged)] +pub enum FruitOrVeg { + Veg(Veggie), + Fruit(Fruit), +} +impl ::std::convert::From<&Self> for FruitOrVeg { + fn from(value: &FruitOrVeg) -> Self { + value.clone() + } +} +impl ::std::convert::From for FruitOrVeg { + fn from(value: Veggie) -> Self { + Self::Veg(value) + } +} +impl ::std::convert::From for FruitOrVeg { + fn from(value: Fruit) -> Self { + Self::Fruit(value) + } +} +#[doc = "`Veggie`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"veggieLike\","] +#[doc = " \"veggieName\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"veggieLike\": {"] +#[doc = " \"description\": \"Do I like this vegetable?\","] +#[doc = " \"type\": \"boolean\""] +#[doc = " },"] +#[doc = " \"veggieName\": {"] +#[doc = " \"description\": \"The name of the vegetable.\","] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[extra_attr] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct Veggie { + #[doc = "Do I like this vegetable?"] + #[serde(rename = "veggieLike")] + pub veggie_like: bool, + #[doc = "The name of the vegetable."] + #[serde(rename = "veggieName")] + pub veggie_name: ::std::string::String, +} +impl ::std::convert::From<&Veggie> for Veggie { + fn from(value: &Veggie) -> Self { + value.clone() + } +} +#[doc = "A representation of a person, company, organization, or place"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"$id\": \"https://example.com/arrays.schema.json\","] +#[doc = " \"title\": \"veggies\","] +#[doc = " \"description\": \"A representation of a person, company, organization, or place\","] +#[doc = " \"type\": \"object\","] +#[doc = " \"properties\": {"] +#[doc = " \"fruits\": {"] +#[doc = " \"type\": \"array\","] +#[doc = " \"items\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = " },"] +#[doc = " \"vegetables\": {"] +#[doc = " \"type\": \"array\","] +#[doc = " \"items\": {"] +#[doc = " \"$ref\": \"#/$defs/veggie\""] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[extra_attr] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct Veggies { + #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] + pub fruits: ::std::vec::Vec<::std::string::String>, + #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] + pub vegetables: ::std::vec::Vec, +} +impl ::std::convert::From<&Veggies> for Veggies { + fn from(value: &Veggies) -> Self { + value.clone() + } +} +impl ::std::default::Default for Veggies { + fn default() -> Self { + Self { + fruits: Default::default(), + vegetables: Default::default(), + } + } +} diff --git a/cargo-typify/tests/outputs/help.txt b/cargo-typify/tests/outputs/help.txt index 9daedee2..58c71886 100644 --- a/cargo-typify/tests/outputs/help.txt +++ b/cargo-typify/tests/outputs/help.txt @@ -13,9 +13,12 @@ Options: -B, --no-builder Inverse of `--builder`. When set the builder-style interface will not be included - -a, --additional-derive + -d, --additional-derive Add an additional derive macro to apply to all defined types + -a, --additional-attr + Add an additional attribute to apply to all defined types + -o, --output The output file to write to. If not specified, the input file name will be used with a `.rs` extension. diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs index c20f033e..658b2185 100644 --- a/typify-impl/src/defaults.rs +++ b/typify-impl/src/defaults.rs @@ -669,6 +669,7 @@ mod tests { let type_entry = TypeEntry { details: crate::type_entry::TypeEntryDetails::Box(type_id), extra_derives: Default::default(), + extra_attrs: Default::default(), }; assert!(type_entry diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 2f791ba2..812d0606 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -296,6 +296,7 @@ impl From for MapType { pub struct TypeSpaceSettings { type_mod: Option, extra_derives: Vec, + extra_attrs: Vec, struct_builder: bool, unknown_crates: UnknownPolicy, @@ -364,6 +365,7 @@ impl CrateVers { pub struct TypeSpacePatch { rename: Option, derives: Vec, + attrs: Vec, } /// Contains the attributes of a replacement of an existing type. @@ -422,6 +424,14 @@ impl TypeSpaceSettings { self } + /// Add an additional attribute to apply to all defined types. + pub fn with_attr(&mut self, attr: String) -> &mut Self { + if !self.extra_attrs.contains(&attr) { + self.extra_attrs.push(attr); + } + self + } + /// For structs, include a "builder" type that can be used to construct it. pub fn with_struct_builder(&mut self, struct_builder: bool) -> &mut Self { self.struct_builder = struct_builder; @@ -563,10 +573,16 @@ impl TypeSpacePatch { self.derives.push(derive.to_string()); self } + + /// Specify an additional attribute to apply to the patched type. + pub fn with_attr(&mut self, attr: S) -> &mut Self { + self.attrs.push(attr.to_string()); + self + } } impl TypeSpace { - /// Create a new TypeSpace with custom settings + /// Create a new TypeSpace with custom settings. pub fn new(settings: &TypeSpaceSettings) -> Self { let mut cache = SchemaCache::default(); diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 01ad2f48..d1eed0f8 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -13,7 +13,7 @@ use crate::{ output::{OutputSpace, OutputSpaceMod}, sanitize, structs::{generate_serde_attr, DefaultFunction}, - util::{get_type_name, metadata_description, type_patch, unique}, + util::{get_type_name, metadata_description, unique, TypePatch}, Case, DefaultImpl, Name, Result, TypeId, TypeSpace, TypeSpaceImpl, }; @@ -138,6 +138,7 @@ impl PartialOrd for WrappedValue { pub(crate) struct TypeEntry { pub details: TypeEntryDetails, pub extra_derives: BTreeSet, + pub extra_attrs: BTreeSet, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -290,10 +291,10 @@ impl TypeEntryEnum { let rename = None; let description = metadata_description(metadata); - let (name, extra_derives) = type_patch(type_space, name); + let type_patch = TypePatch::new(type_space, name); let details = TypeEntryDetails::Enum(Self { - name, + name: type_patch.name, rename, description, default: None, @@ -306,7 +307,8 @@ impl TypeEntryEnum { TypeEntry { details, - extra_derives, + extra_derives: type_patch.derives, + extra_attrs: type_patch.attrs, } } @@ -381,10 +383,10 @@ impl TypeEntryStruct { .cloned() .map(WrappedValue::new); - let (name, extra_derives) = type_patch(type_space, name); + let type_patch = TypePatch::new(type_space, name); let details = TypeEntryDetails::Struct(Self { - name, + name: type_patch.name, rename, description, default, @@ -395,7 +397,8 @@ impl TypeEntryStruct { TypeEntry { details, - extra_derives, + extra_derives: type_patch.derives, + extra_attrs: type_patch.attrs, } } } @@ -412,10 +415,10 @@ impl TypeEntryNewtype { let rename = None; let description = metadata_description(metadata); - let (name, extra_derives) = type_patch(type_space, name); + let type_patch = TypePatch::new(type_space, name); let details = TypeEntryDetails::Newtype(Self { - name, + name: type_patch.name, rename, description, default: None, @@ -426,7 +429,8 @@ impl TypeEntryNewtype { TypeEntry { details, - extra_derives, + extra_derives: type_patch.derives, + extra_attrs: type_patch.attrs, } } @@ -442,10 +446,10 @@ impl TypeEntryNewtype { let rename = None; let description = metadata_description(metadata); - let (name, extra_derives) = type_patch(type_space, name); + let type_patch = TypePatch::new(type_space, name); let details = TypeEntryDetails::Newtype(Self { - name, + name: type_patch.name, rename, description, default: None, @@ -458,7 +462,8 @@ impl TypeEntryNewtype { TypeEntry { details, - extra_derives, + extra_derives: type_patch.derives, + extra_attrs: type_patch.attrs, } } @@ -474,10 +479,10 @@ impl TypeEntryNewtype { let rename = None; let description = metadata_description(metadata); - let (name, extra_derives) = type_patch(type_space, name); + let type_patch = TypePatch::new(type_space, name); let details = TypeEntryDetails::Newtype(Self { - name, + name: type_patch.name, rename, description, default: None, @@ -490,7 +495,8 @@ impl TypeEntryNewtype { TypeEntry { details, - extra_derives, + extra_derives: type_patch.derives, + extra_attrs: type_patch.attrs, } } @@ -512,10 +518,10 @@ impl TypeEntryNewtype { pattern, } = validation.clone(); - let (name, extra_derives) = type_patch(type_space, name); + let type_patch = TypePatch::new(type_space, name); let details = TypeEntryDetails::Newtype(Self { - name, + name: type_patch.name, rename, description, default: None, @@ -530,7 +536,8 @@ impl TypeEntryNewtype { TypeEntry { details, - extra_derives, + extra_derives: type_patch.derives, + extra_attrs: type_patch.attrs, } } } @@ -540,6 +547,7 @@ impl From for TypeEntry { Self { details, extra_derives: Default::default(), + extra_attrs: Default::default(), } } } @@ -553,6 +561,7 @@ impl TypeEntry { parameters: Default::default(), }), extra_derives: Default::default(), + extra_attrs: Default::default(), } } pub(crate) fn new_native_params(type_name: S, params: &[TypeId]) -> Self { @@ -563,12 +572,14 @@ impl TypeEntry { parameters: params.to_vec(), }), extra_derives: Default::default(), + extra_attrs: Default::default(), } } pub(crate) fn new_boolean() -> Self { TypeEntry { details: TypeEntryDetails::Boolean, extra_derives: Default::default(), + extra_attrs: Default::default(), } } pub(crate) fn new_integer(type_name: S) -> Self { @@ -578,6 +589,7 @@ impl TypeEntry { TypeEntry { details: TypeEntryDetails::Float(type_name.to_string()), extra_derives: Default::default(), + extra_attrs: Default::default(), } } @@ -1074,8 +1086,11 @@ impl TypeEntry { &type_space.settings.extra_derives, ); + let attrs = strings_to_attrs(&self.extra_attrs, &type_space.settings.extra_attrs); + let item = quote! { #doc + #(#attrs)* #[derive(#(#derives),*)] #serde pub enum #type_name { @@ -1190,11 +1205,14 @@ impl TypeEntry { &type_space.settings.extra_derives, ); + let attrs = strings_to_attrs(&self.extra_attrs, &type_space.settings.extra_attrs); + output.add_item( OutputSpaceMod::Crate, name, quote! { #doc + #(#attrs)* #[derive(#(#derives),*)] #serde pub struct #type_name { @@ -1658,8 +1676,11 @@ impl TypeEntry { &type_space.settings.extra_derives, ); + let attrs = strings_to_attrs(&self.extra_attrs, &type_space.settings.extra_attrs); + let item = quote! { #doc + #(#attrs)* #[derive(#(#derives),*)] #[serde(transparent)] pub struct #type_name(#vis #inner_type_name); @@ -2019,6 +2040,18 @@ fn strings_to_derives<'a>( }) } +fn strings_to_attrs<'a>( + type_attrs: &'a BTreeSet, + extra_attrs: &'a [String], +) -> impl Iterator + 'a { + let mut combined_attrs = BTreeSet::new(); + combined_attrs.extend(extra_attrs.iter().map(String::as_str)); + combined_attrs.extend(type_attrs.iter().map(String::as_str)); + combined_attrs + .into_iter() + .map(|attr| attr.parse::().unwrap()) +} + /// Returns true iff... /// - the enum is untagged /// - all variants are single items (aka newtype variants) diff --git a/typify-impl/src/util.rs b/typify-impl/src/util.rs index 64cb813b..212b5b9b 100644 --- a/typify-impl/src/util.rs +++ b/typify-impl/src/util.rs @@ -835,17 +835,33 @@ pub(crate) fn get_type_name(type_name: &Name, metadata: &Option>) Some(sanitize(&name, Case::Pascal)) } -/// Check for patches which include potential type renames and additional -/// derive macros. -pub(crate) fn type_patch(type_space: &TypeSpace, type_name: String) -> (String, BTreeSet) { - match type_space.settings.patch.get(&type_name) { - None => (type_name, Default::default()), +pub(crate) struct TypePatch { + pub name: String, + pub derives: BTreeSet, + pub attrs: BTreeSet, +} + +impl TypePatch { + /// Creates a new TypePatch by resolving patches for the given type name. + pub fn new(type_space: &TypeSpace, type_name: String) -> Self { + match type_space.settings.patch.get(&type_name) { + None => Self { + name: type_name, + derives: Default::default(), + attrs: Default::default(), + }, - Some(patch) => { - let name = patch.rename.clone().unwrap_or(type_name); - let derives = patch.derives.iter().cloned().collect(); + Some(patch) => { + let name = patch.rename.clone().unwrap_or(type_name); + let derives = patch.derives.iter().cloned().collect(); + let attrs = patch.attrs.iter().cloned().collect(); - (name, derives) + Self { + name, + derives, + attrs, + } + } } } } diff --git a/typify-impl/src/value.rs b/typify-impl/src/value.rs index 61f9f066..4e9e13c8 100644 --- a/typify-impl/src/value.rs +++ b/typify-impl/src/value.rs @@ -482,6 +482,7 @@ mod tests { let type_entry = TypeEntry { details: crate::type_entry::TypeEntryDetails::Box(type_id), extra_derives: Default::default(), + extra_attrs: Default::default(), }; assert_eq!( diff --git a/typify-macro/src/lib.rs b/typify-macro/src/lib.rs index 29e4c128..ac18fbf1 100644 --- a/typify-macro/src/lib.rs +++ b/typify-macro/src/lib.rs @@ -9,7 +9,7 @@ use std::{collections::HashMap, path::Path}; use proc_macro::TokenStream; use quote::{quote, ToTokens}; use serde::Deserialize; -use serde_tokenstream::ParseWrapper; +use serde_tokenstream::{ParseWrapper, TokenStreamWrapper}; use syn::LitStr; use token_utils::TypeAndImpls; use typify_impl::{ @@ -34,6 +34,9 @@ mod token_utils; /// - `derives`: optional array of derive macro paths; the derive macros to be /// applied to all generated types /// +/// - `attrs`: optional array of attribute paths; the attributes to be applied +/// to all generated types +/// /// - `struct_builder`: optional boolean; (if true) generates a `::builder()` /// method for each generated struct that can be used to specify each /// property and construct the struct @@ -59,7 +62,7 @@ mod token_utils; /// - `replace`: optional map from definition name to a replacement type. This /// may be used to skip generation of the named type and use a existing Rust /// type. -/// +/// /// - `convert`: optional map from a JSON schema type defined in `$defs` to a /// replacement type. This may be used to skip generation of the schema and /// use an existing Rust type. @@ -77,6 +80,8 @@ struct MacroSettings { #[serde(default)] derives: Vec>, #[serde(default)] + attrs: Vec, + #[serde(default)] struct_builder: bool, #[serde(default)] @@ -163,6 +168,8 @@ struct MacroPatch { rename: Option, #[serde(default)] derives: Vec>, + #[serde(default)] + attrs: Vec, } impl From for TypeSpacePatch { @@ -174,6 +181,9 @@ impl From for TypeSpacePatch { a.derives.iter().for_each(|derive| { s.with_derive(derive.to_token_stream()); }); + a.attrs.iter().for_each(|attr| { + s.with_attr(attr.to_token_stream()); + }); s } } @@ -193,11 +203,15 @@ fn do_import_types(item: TokenStream) -> Result { unknown_crates, crates, map_type, + attrs, } = serde_tokenstream::from_tokenstream(&item.into())?; let mut settings = TypeSpaceSettings::default(); derives.into_iter().for_each(|derive| { settings.with_derive(derive.to_token_stream().to_string()); }); + attrs.into_iter().for_each(|attr| { + settings.with_attr(attr.to_token_stream().to_string()); + }); settings.with_struct_builder(struct_builder); patch.into_iter().for_each(|(type_name, patch)| {