diff --git a/README.md b/README.md index 8b63f2bd..cde7fb03 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,22 @@ applied. Non-required properties with types that already have a default value (such as a `Vec`) simply get the `#[serde(default)]` attribute (so you won't see e.g. `Option>`). +#### Alternate Map types + +By default, Typify uses `std::collections::HashMap` as described above. + +If you prefer to use `std::collections::BTreeMap` or a map type from a crate such +as `indexmap::IndexMap`, you can specify this by calling `with_map_type` on the +`TypeSpaceSettings` object, and providing the full path to the type you want to +use. E.g. `::std::collections::BTreeMap` or `::indexmap::IndexMap`. + +Note that for a custom map type to work you must have `T` defined to generate +a struct as described in [Objects](#objects). If `T` is not defined, typify +will generate code using a `serde_json::Map` instead. + +See the documentation for `TypeSpaceSettings::with_map_type` for the +requirements for a map type. + ### OneOf The `oneOf` construct maps to a Rust enum. Typify maps this to the various diff --git a/cargo-typify/src/lib.rs b/cargo-typify/src/lib.rs index 93822305..101453d3 100644 --- a/cargo-typify/src/lib.rs +++ b/cargo-typify/src/lib.rs @@ -46,6 +46,10 @@ pub struct CliArgs { #[arg(long = "crate")] crates: Vec, + /// Specify the map like type to use. + #[arg(long = "map-type")] + map_type: Option, + /// Specify the policy unknown crates found in schemas with the /// x-rust-type extension. #[arg( @@ -151,6 +155,10 @@ pub fn convert(args: &CliArgs) -> Result { settings.with_crate(name, version.clone(), rename.as_ref()); } + if let Some(map_type) = &args.map_type { + settings.with_map_type(map_type.clone()); + } + if let Some(unknown_crates) = &args.unknown_crates { let unknown_crates = match unknown_crates.as_str() { "generate" => UnknownPolicy::Generate, @@ -192,6 +200,7 @@ mod tests { output: Some(PathBuf::from("-")), no_builder: false, crates: vec![], + map_type: None, unknown_crates: Default::default(), }; @@ -207,6 +216,7 @@ mod tests { output: Some(PathBuf::from("some_file.rs")), no_builder: false, crates: vec![], + map_type: None, unknown_crates: Default::default(), }; @@ -222,12 +232,32 @@ mod tests { output: None, no_builder: false, crates: vec![], + map_type: None, unknown_crates: Default::default(), }; assert_eq!(args.output_path(), Some(PathBuf::from("input.rs"))); } + #[test] + fn test_use_btree_map() { + let args = CliArgs { + input: PathBuf::from("input.json"), + builder: false, + additional_derives: vec![], + output: None, + no_builder: false, + crates: vec![], + map_type: Some("::std::collections::BTreeMap".to_string()), + unknown_crates: Default::default(), + }; + + assert_eq!( + args.map_type, + Some("::std::collections::BTreeMap".to_string()) + ); + } + #[test] fn test_builder_as_default_style() { let args = CliArgs { @@ -237,6 +267,7 @@ mod tests { output: None, no_builder: false, crates: vec![], + map_type: None, unknown_crates: Default::default(), }; @@ -252,6 +283,7 @@ mod tests { output: None, no_builder: true, crates: vec![], + map_type: None, unknown_crates: Default::default(), }; @@ -267,6 +299,7 @@ mod tests { output: None, no_builder: false, crates: vec![], + map_type: None, unknown_crates: Default::default(), }; diff --git a/cargo-typify/tests/integration.rs b/cargo-typify/tests/integration.rs index 1f0332e7..46028107 100644 --- a/cargo-typify/tests/integration.rs +++ b/cargo-typify/tests/integration.rs @@ -158,3 +158,29 @@ fn test_help() { assert!(output.status.success()); assert_contents("tests/outputs/help.txt", &actual); } + +#[test] +fn test_btree_map() { + use assert_cmd::Command; + + let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); + + let temp = TempDir::new("cargo-typify").unwrap(); + let output_file = temp.path().join("output.rs"); + + let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); + cmd.args([ + "typify", + input, + "--map-type", + "::std::collections::BTreeMap", + "--output", + output_file.to_str().unwrap(), + ]) + .assert() + .success(); + + let actual = std::fs::read_to_string(output_file).unwrap(); + + assert_contents("tests/outputs/custom_btree_map.rs", &actual); +} diff --git a/cargo-typify/tests/outputs/builder.rs b/cargo-typify/tests/outputs/builder.rs index 60f3f187..8e2ebdf9 100644 --- a/cargo-typify/tests/outputs/builder.rs +++ b/cargo-typify/tests/outputs/builder.rs @@ -35,19 +35,22 @@ pub mod error { #[doc = r""] #[doc = r" ```json"] #[doc = "{"] -#[doc = " \"type\": \"object\""] +#[doc = " \"type\": \"object\","] +#[doc = " \"additionalProperties\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] #[doc = "}"] #[doc = r" ```"] #[doc = r" "] #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] -pub struct Fruit(pub ::serde_json::Map<::std::string::String, ::serde_json::Value>); +pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>); impl ::std::ops::Deref for Fruit { - type Target = ::serde_json::Map<::std::string::String, ::serde_json::Value>; - fn deref(&self) -> &::serde_json::Map<::std::string::String, ::serde_json::Value> { + 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 From for ::serde_json::Map<::std::string::String, ::serde_json::Value> { +impl From for ::std::collections::HashMap<::std::string::String, ::std::string::String> { fn from(value: Fruit) -> Self { value.0 } @@ -57,8 +60,10 @@ impl From<&Fruit> for Fruit { value.clone() } } -impl From<::serde_json::Map<::std::string::String, ::serde_json::Value>> for Fruit { - fn from(value: ::serde_json::Map<::std::string::String, ::serde_json::Value>) -> Self { +impl 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) } } diff --git a/cargo-typify/tests/outputs/custom_btree_map.rs b/cargo-typify/tests/outputs/custom_btree_map.rs new file mode 100644 index 00000000..b49d03fb --- /dev/null +++ b/cargo-typify/tests/outputs/custom_btree_map.rs @@ -0,0 +1,312 @@ +#![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"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct Fruit(pub ::std::collections::BTreeMap<::std::string::String, ::std::string::String>); +impl ::std::ops::Deref for Fruit { + type Target = ::std::collections::BTreeMap<::std::string::String, ::std::string::String>; + fn deref(&self) -> &::std::collections::BTreeMap<::std::string::String, ::std::string::String> { + &self.0 + } +} +impl From for ::std::collections::BTreeMap<::std::string::String, ::std::string::String> { + fn from(value: Fruit) -> Self { + value.0 + } +} +impl From<&Fruit> for Fruit { + fn from(value: &Fruit) -> Self { + value.clone() + } +} +impl From<::std::collections::BTreeMap<::std::string::String, ::std::string::String>> for Fruit { + fn from( + value: ::std::collections::BTreeMap<::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"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +#[serde(untagged)] +pub enum FruitOrVeg { + Veg(Veggie), + Fruit(Fruit), +} +impl From<&FruitOrVeg> for FruitOrVeg { + fn from(value: &FruitOrVeg) -> Self { + value.clone() + } +} +impl From for FruitOrVeg { + fn from(value: Veggie) -> Self { + Self::Veg(value) + } +} +impl 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"
"] +#[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 From<&Veggie> for Veggie { + fn from(value: &Veggie) -> Self { + value.clone() + } +} +impl Veggie { + pub fn builder() -> builder::Veggie { + Default::default() + } +} +#[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"
"] +#[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 From<&Veggies> for Veggies { + fn from(value: &Veggies) -> Self { + value.clone() + } +} +impl Veggies { + pub fn builder() -> builder::Veggies { + Default::default() + } +} +#[doc = r" Types for composing complex structures."] +pub mod builder { + #[derive(Clone, Debug)] + pub struct Veggie { + veggie_like: ::std::result::Result, + veggie_name: ::std::result::Result<::std::string::String, ::std::string::String>, + } + impl Default for Veggie { + fn default() -> Self { + Self { + veggie_like: Err("no value supplied for veggie_like".to_string()), + veggie_name: Err("no value supplied for veggie_name".to_string()), + } + } + } + impl Veggie { + pub fn veggie_like(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.veggie_like = value + .try_into() + .map_err(|e| format!("error converting supplied value for veggie_like: {}", e)); + self + } + pub fn veggie_name(mut self, value: T) -> Self + where + T: std::convert::TryInto<::std::string::String>, + T::Error: std::fmt::Display, + { + self.veggie_name = value + .try_into() + .map_err(|e| format!("error converting supplied value for veggie_name: {}", e)); + self + } + } + impl ::std::convert::TryFrom for super::Veggie { + type Error = super::error::ConversionError; + fn try_from(value: Veggie) -> ::std::result::Result { + Ok(Self { + veggie_like: value.veggie_like?, + veggie_name: value.veggie_name?, + }) + } + } + impl From for Veggie { + fn from(value: super::Veggie) -> Self { + Self { + veggie_like: Ok(value.veggie_like), + veggie_name: Ok(value.veggie_name), + } + } + } + #[derive(Clone, Debug)] + pub struct Veggies { + fruits: + ::std::result::Result<::std::vec::Vec<::std::string::String>, ::std::string::String>, + vegetables: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + } + impl Default for Veggies { + fn default() -> Self { + Self { + fruits: Ok(Default::default()), + vegetables: Ok(Default::default()), + } + } + } + impl Veggies { + pub fn fruits(mut self, value: T) -> Self + where + T: std::convert::TryInto<::std::vec::Vec<::std::string::String>>, + T::Error: std::fmt::Display, + { + self.fruits = value + .try_into() + .map_err(|e| format!("error converting supplied value for fruits: {}", e)); + self + } + pub fn vegetables(mut self, value: T) -> Self + where + T: std::convert::TryInto<::std::vec::Vec>, + T::Error: std::fmt::Display, + { + self.vegetables = value + .try_into() + .map_err(|e| format!("error converting supplied value for vegetables: {}", e)); + self + } + } + impl ::std::convert::TryFrom for super::Veggies { + type Error = super::error::ConversionError; + fn try_from(value: Veggies) -> ::std::result::Result { + Ok(Self { + fruits: value.fruits?, + vegetables: value.vegetables?, + }) + } + } + impl From for Veggies { + fn from(value: super::Veggies) -> Self { + Self { + fruits: Ok(value.fruits), + vegetables: Ok(value.vegetables), + } + } + } +} diff --git a/cargo-typify/tests/outputs/derive.rs b/cargo-typify/tests/outputs/derive.rs index 3e9bbc49..bc3b9b05 100644 --- a/cargo-typify/tests/outputs/derive.rs +++ b/cargo-typify/tests/outputs/derive.rs @@ -35,19 +35,22 @@ pub mod error { #[doc = r""] #[doc = r" ```json"] #[doc = "{"] -#[doc = " \"type\": \"object\""] +#[doc = " \"type\": \"object\","] +#[doc = " \"additionalProperties\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] #[doc = "}"] #[doc = r" ```"] #[doc = r" "] #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, ExtraDerive)] -pub struct Fruit(pub ::serde_json::Map<::std::string::String, ::serde_json::Value>); +pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>); impl ::std::ops::Deref for Fruit { - type Target = ::serde_json::Map<::std::string::String, ::serde_json::Value>; - fn deref(&self) -> &::serde_json::Map<::std::string::String, ::serde_json::Value> { + 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 From for ::serde_json::Map<::std::string::String, ::serde_json::Value> { +impl From for ::std::collections::HashMap<::std::string::String, ::std::string::String> { fn from(value: Fruit) -> Self { value.0 } @@ -57,8 +60,10 @@ impl From<&Fruit> for Fruit { value.clone() } } -impl From<::serde_json::Map<::std::string::String, ::serde_json::Value>> for Fruit { - fn from(value: ::serde_json::Map<::std::string::String, ::serde_json::Value>) -> Self { +impl 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) } } diff --git a/cargo-typify/tests/outputs/help.txt b/cargo-typify/tests/outputs/help.txt index 74172166..9daedee2 100644 --- a/cargo-typify/tests/outputs/help.txt +++ b/cargo-typify/tests/outputs/help.txt @@ -24,6 +24,9 @@ Options: --crate Specify each crate@version that can be assumed to be in use for types found in the schema with the x-rust-type extension + --map-type + Specify the map like type to use + --unknown-crates Specify the policy unknown crates found in schemas with the x-rust-type extension diff --git a/cargo-typify/tests/outputs/multi_derive.rs b/cargo-typify/tests/outputs/multi_derive.rs index 862849b0..ec31a143 100644 --- a/cargo-typify/tests/outputs/multi_derive.rs +++ b/cargo-typify/tests/outputs/multi_derive.rs @@ -35,21 +35,24 @@ pub mod error { #[doc = r""] #[doc = r" ```json"] #[doc = "{"] -#[doc = " \"type\": \"object\""] +#[doc = " \"type\": \"object\","] +#[doc = " \"additionalProperties\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] #[doc = "}"] #[doc = r" ```"] #[doc = r" "] #[derive( :: serde :: Deserialize, :: serde :: Serialize, AnotherDerive, Clone, Debug, ExtraDerive, )] -pub struct Fruit(pub ::serde_json::Map<::std::string::String, ::serde_json::Value>); +pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>); impl ::std::ops::Deref for Fruit { - type Target = ::serde_json::Map<::std::string::String, ::serde_json::Value>; - fn deref(&self) -> &::serde_json::Map<::std::string::String, ::serde_json::Value> { + 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 From for ::serde_json::Map<::std::string::String, ::serde_json::Value> { +impl From for ::std::collections::HashMap<::std::string::String, ::std::string::String> { fn from(value: Fruit) -> Self { value.0 } @@ -59,8 +62,10 @@ impl From<&Fruit> for Fruit { value.clone() } } -impl From<::serde_json::Map<::std::string::String, ::serde_json::Value>> for Fruit { - fn from(value: ::serde_json::Map<::std::string::String, ::serde_json::Value>) -> Self { +impl 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) } } diff --git a/cargo-typify/tests/outputs/no-builder.rs b/cargo-typify/tests/outputs/no-builder.rs index 6f2637fb..d354cc17 100644 --- a/cargo-typify/tests/outputs/no-builder.rs +++ b/cargo-typify/tests/outputs/no-builder.rs @@ -35,19 +35,22 @@ pub mod error { #[doc = r""] #[doc = r" ```json"] #[doc = "{"] -#[doc = " \"type\": \"object\""] +#[doc = " \"type\": \"object\","] +#[doc = " \"additionalProperties\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] #[doc = "}"] #[doc = r" ```"] #[doc = r" "] #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] -pub struct Fruit(pub ::serde_json::Map<::std::string::String, ::serde_json::Value>); +pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>); impl ::std::ops::Deref for Fruit { - type Target = ::serde_json::Map<::std::string::String, ::serde_json::Value>; - fn deref(&self) -> &::serde_json::Map<::std::string::String, ::serde_json::Value> { + 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 From for ::serde_json::Map<::std::string::String, ::serde_json::Value> { +impl From for ::std::collections::HashMap<::std::string::String, ::std::string::String> { fn from(value: Fruit) -> Self { value.0 } @@ -57,8 +60,10 @@ impl From<&Fruit> for Fruit { value.clone() } } -impl From<::serde_json::Map<::std::string::String, ::serde_json::Value>> for Fruit { - fn from(value: ::serde_json::Map<::std::string::String, ::serde_json::Value>) -> Self { +impl 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) } } diff --git a/example.json b/example.json index 2ce2f0cf..8987166f 100644 --- a/example.json +++ b/example.json @@ -37,7 +37,10 @@ } }, "fruit": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "fruit-or-veg": { "oneOf": [ diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index ff036eeb..e003caa9 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -237,8 +237,66 @@ pub(crate) enum DefaultImpl { NZU64, } +/// Type name to use in generated code. +#[derive(Clone)] +pub struct MapType(pub syn::Type); + +impl MapType { + /// Create a new MapType from a [`str`]. + pub fn new(s: &str) -> Self { + let map_type = syn::parse_str::(s).expect("valid ident"); + Self(map_type) + } +} + +impl Default for MapType { + fn default() -> Self { + Self::new("::std::collections::HashMap") + } +} + +impl std::fmt::Debug for MapType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "MapType({})", self.0.to_token_stream()) + } +} + +impl std::fmt::Display for MapType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.to_token_stream().fmt(f) + } +} + +impl<'de> serde::Deserialize<'de> for MapType { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let s = <&str>::deserialize(deserializer)?; + Ok(Self::new(s)) + } +} + +impl From for MapType { + fn from(s: String) -> Self { + Self::new(&s) + } +} + +impl From<&str> for MapType { + fn from(s: &str) -> Self { + Self::new(s) + } +} + +impl From for MapType { + fn from(t: syn::Type) -> Self { + Self(t) + } +} + /// Settings that alter type generation. -#[derive(Debug, Default, Clone)] +#[derive(Default, Debug, Clone)] pub struct TypeSpaceSettings { type_mod: Option, extra_derives: Vec, @@ -246,6 +304,7 @@ pub struct TypeSpaceSettings { unknown_crates: UnknownPolicy, crates: BTreeMap, + map_type: MapType, patch: BTreeMap, replace: BTreeMap, @@ -454,6 +513,28 @@ impl TypeSpaceSettings { ); self } + + /// Specify the map-like type to be used in generated code. + /// + /// ## Requirements + /// + /// - Have an `is_empty` method that returns a boolean. + /// - Have two generic parameters, `K` and `V`. + /// - Have a [`std::fmt::Debug`] impl. + /// - Have a [`serde::Serialize``] impl. + /// - Have a [`serde::Deserialize``] impl. + /// - Have a [`Clone`] impl. + /// + /// ## Examples + /// + /// - [`::std::collections::HashMap`] + /// - [`::std::collections::BTreeMap`] + /// - [`::indexmap::IndexMap`] + /// + pub fn with_map_type>(&mut self, map_type: T) -> &mut Self { + self.map_type = map_type.into(); + self + } } impl TypeSpacePatch { diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs index 5f8dfb47..a3e68f37 100644 --- a/typify-impl/src/structs.rs +++ b/typify-impl/src/structs.rs @@ -384,6 +384,7 @@ pub(crate) fn generate_serde_attr( (StructPropertyState::Optional, TypeEntryDetails::Map(key_id, value_id)) => { serde_options.push(quote! { default }); + let map_to_use = &type_space.settings.map_type; let key_ty = type_space .id_to_entry .get(key_id) @@ -400,8 +401,9 @@ pub(crate) fn generate_serde_attr( skip_serializing_if = "::serde_json::Map::is_empty" }); } else { + let is_empty = format!("{}::is_empty", map_to_use); serde_options.push(quote! { - skip_serializing_if = "::std::collections::HashMap::is_empty" + skip_serializing_if = #is_empty }); } DefaultFunction::Default diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 0d692d17..97a4d479 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -1619,6 +1619,7 @@ impl TypeEntry { } TypeEntryDetails::Map(key_id, value_id) => { + let map_to_use = &type_space.settings.map_type; let key_ty = type_space .id_to_entry .get(key_id) @@ -1635,7 +1636,9 @@ impl TypeEntry { } else { let key_ident = key_ty.type_ident(type_space, type_mod); let value_ident = value_ty.type_ident(type_space, type_mod); - quote! { ::std::collections::HashMap<#key_ident, #value_ident> } + let map_to_use = &map_to_use.0; + + quote! { #map_to_use<#key_ident, #value_ident> } } } diff --git a/typify-macro/src/lib.rs b/typify-macro/src/lib.rs index 264749d4..3262c1ac 100644 --- a/typify-macro/src/lib.rs +++ b/typify-macro/src/lib.rs @@ -12,7 +12,9 @@ use serde::Deserialize; use serde_tokenstream::ParseWrapper; use syn::LitStr; use token_utils::TypeAndImpls; -use typify_impl::{CrateVers, TypeSpace, TypeSpacePatch, TypeSpaceSettings, UnknownPolicy}; +use typify_impl::{ + CrateVers, MapType, TypeSpace, TypeSpacePatch, TypeSpaceSettings, UnknownPolicy, +}; mod token_utils; @@ -81,6 +83,8 @@ struct MacroSettings { unknown_crates: UnknownPolicy, #[serde(default)] crates: HashMap, + #[serde(default)] + map_type: MapType, #[serde(default)] patch: HashMap, MacroPatch>, @@ -188,6 +192,7 @@ fn do_import_types(item: TokenStream) -> Result { convert, unknown_crates, crates, + map_type, } = serde_tokenstream::from_tokenstream(&item.into())?; let mut settings = TypeSpaceSettings::default(); derives.into_iter().for_each(|derive| { @@ -218,6 +223,8 @@ fn do_import_types(item: TokenStream) -> Result { ); settings.with_unknown_crates(unknown_crates); + settings.with_map_type(map_type); + (schema.into_inner(), settings) }; diff --git a/typify-test/build.rs b/typify-test/build.rs index c8ffe9f6..34a87441 100644 --- a/typify-test/build.rs +++ b/typify-test/build.rs @@ -1,10 +1,10 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::{env, fs, path::Path}; use schemars::schema::Schema; use schemars::JsonSchema; use serde::Serialize; -use typify::TypeSpace; +use typify::{TypeSpace, TypeSpaceSettings}; #[allow(dead_code)] #[derive(JsonSchema)] @@ -53,6 +53,12 @@ struct WithSet { set: HashSet, } +#[allow(dead_code)] +#[derive(JsonSchema)] +struct WithMap { + map: HashMap, +} + struct LoginName; impl JsonSchema for LoginName { fn schema_name() -> String { @@ -112,6 +118,32 @@ fn main() { let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); out_file.push("codegen.rs"); fs::write(out_file, contents).unwrap(); + + // Generate with HashMap + let mut type_space = TypeSpace::new(&TypeSpaceSettings::default()); + + WithMap::add(&mut type_space); + + let contents = + prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); + + let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); + out_file.push("codegen_hashmap.rs"); + fs::write(out_file, contents).unwrap(); + + // Generate with a custom map type to validate requirements. + let mut settings = TypeSpaceSettings::default(); + settings.with_map_type("CustomMap".to_string()); + let mut type_space = TypeSpace::new(&settings); + + WithMap::add(&mut type_space); + + let contents = + prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); + + let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); + out_file.push("codegen_custommap.rs"); + fs::write(out_file, contents).unwrap(); } trait AddType { diff --git a/typify-test/src/main.rs b/typify-test/src/main.rs index a37401c9..f3e9d219 100644 --- a/typify-test/src/main.rs +++ b/typify-test/src/main.rs @@ -49,3 +49,37 @@ fn test_unknown_format() { pancakes: String::new(), }; } + +mod hashmap { + include!(concat!(env!("OUT_DIR"), "/codegen_hashmap.rs")); + + #[test] + fn test_with_map() { + // Validate that a map is currently represented as a HashMap by default. + let _ = WithMap { + map: std::collections::HashMap::new(), + }; + } +} + +mod custom_map { + #[allow(private_interfaces)] + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + pub struct CustomMap { + key: K, + value: V, + } + + include!(concat!(env!("OUT_DIR"), "/codegen_custommap.rs")); + + #[test] + fn test_with_map() { + // Validate that a map is represented as an CustomMap when requested. + let _ = WithMap { + map: CustomMap { + key: String::new(), + value: String::new(), + }, + }; + } +} diff --git a/typify/tests/schemas.rs b/typify/tests/schemas.rs index 4b46415c..78dfc8f2 100644 --- a/typify/tests/schemas.rs +++ b/typify/tests/schemas.rs @@ -15,17 +15,33 @@ fn test_schemas() { env_logger::init(); // Make sure output is up to date. for entry in glob("tests/schemas/*.json").expect("Failed to read glob pattern") { - validate_schema(entry.unwrap()).unwrap(); + let entry = entry.unwrap(); + let out_path = entry.clone().with_extension("rs"); + validate_schema(entry, out_path, &mut TypeSpaceSettings::default()).unwrap(); } // Make sure it all compiles. trybuild::TestCases::new().pass("tests/schemas/*.rs"); } -fn validate_schema(path: std::path::PathBuf) -> Result<(), Box> { - let mut out_path = path.clone(); - out_path.set_extension("rs"); +/// Ensure that setting the global config to use a custom map type works. +#[test] +fn test_custom_map() { + validate_schema( + "tests/schemas/maps.json".into(), + "tests/schemas/maps_custom.rs".into(), + TypeSpaceSettings::default().with_map_type("std::collections::BTreeMap".to_string()), + ) + .unwrap(); + + trybuild::TestCases::new().pass("tests/schemas/maps_custom.rs"); +} +fn validate_schema( + path: std::path::PathBuf, + out_path: std::path::PathBuf, + typespace: &mut TypeSpaceSettings, +) -> Result<(), Box> { let file = File::open(path)?; let reader = BufReader::new(file); @@ -40,7 +56,7 @@ fn validate_schema(path: std::path::PathBuf) -> Result<(), Box> { let schema = serde_json::from_value(schema_raw).unwrap(); let mut type_space = TypeSpace::new( - TypeSpaceSettings::default() + typespace .with_replacement( "HandGeneratedType", "String", diff --git a/typify/tests/schemas/maps_custom.rs b/typify/tests/schemas/maps_custom.rs new file mode 100644 index 00000000..5094dae0 --- /dev/null +++ b/typify/tests/schemas/maps_custom.rs @@ -0,0 +1,298 @@ +#[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 = "DeadSimple"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"title\": \"DeadSimple\","] +#[doc = " \"type\": \"object\","] +#[doc = " \"$comment\": \"usual case of a map whose name must come from its title\""] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct DeadSimple(pub ::serde_json::Map<::std::string::String, ::serde_json::Value>); +impl ::std::ops::Deref for DeadSimple { + type Target = ::serde_json::Map<::std::string::String, ::serde_json::Value>; + fn deref(&self) -> &::serde_json::Map<::std::string::String, ::serde_json::Value> { + &self.0 + } +} +impl From for ::serde_json::Map<::std::string::String, ::serde_json::Value> { + fn from(value: DeadSimple) -> Self { + value.0 + } +} +impl From<&DeadSimple> for DeadSimple { + fn from(value: &DeadSimple) -> Self { + value.clone() + } +} +impl From<::serde_json::Map<::std::string::String, ::serde_json::Value>> for DeadSimple { + fn from(value: ::serde_json::Map<::std::string::String, ::serde_json::Value>) -> Self { + Self(value) + } +} +#[doc = "Eh"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"string\","] +#[doc = " \"format\": \"^a*$\""] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub struct Eh(pub ::std::string::String); +impl ::std::ops::Deref for Eh { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl From for ::std::string::String { + fn from(value: Eh) -> Self { + value.0 + } +} +impl From<&Eh> for Eh { + fn from(value: &Eh) -> Self { + value.clone() + } +} +impl From<::std::string::String> for Eh { + fn from(value: ::std::string::String) -> Self { + Self(value) + } +} +impl ::std::str::FromStr for Eh { + type Err = ::std::convert::Infallible; + fn from_str(value: &str) -> ::std::result::Result { + Ok(Self(value.to_string())) + } +} +impl ::std::fmt::Display for Eh { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + self.0.fmt(f) + } +} +#[doc = "MapWithDateKeys"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"object\","] +#[doc = " \"additionalProperties\": {"] +#[doc = " \"$ref\": \"#/definitions/Value\""] +#[doc = " },"] +#[doc = " \"propertyNames\": {"] +#[doc = " \"format\": \"date\""] +#[doc = " },"] +#[doc = " \"$comment\": \"test that a type isn't needed for propertyNames\""] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct MapWithDateKeys(pub std::collections::BTreeMap); +impl ::std::ops::Deref for MapWithDateKeys { + type Target = std::collections::BTreeMap; + fn deref(&self) -> &std::collections::BTreeMap { + &self.0 + } +} +impl From for std::collections::BTreeMap { + fn from(value: MapWithDateKeys) -> Self { + value.0 + } +} +impl From<&MapWithDateKeys> for MapWithDateKeys { + fn from(value: &MapWithDateKeys) -> Self { + value.clone() + } +} +impl From> for MapWithDateKeys { + fn from(value: std::collections::BTreeMap) -> Self { + Self(value) + } +} +#[doc = "MapWithDateTimeKeys"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"object\","] +#[doc = " \"additionalProperties\": {"] +#[doc = " \"$ref\": \"#/definitions/Value\""] +#[doc = " },"] +#[doc = " \"propertyNames\": {"] +#[doc = " \"format\": \"date-time\""] +#[doc = " },"] +#[doc = " \"$comment\": \"test that a type isn't needed for propertyNames\""] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct MapWithDateTimeKeys( + pub std::collections::BTreeMap, Value>, +); +impl ::std::ops::Deref for MapWithDateTimeKeys { + type Target = std::collections::BTreeMap, Value>; + fn deref(&self) -> &std::collections::BTreeMap, Value> { + &self.0 + } +} +impl From + for std::collections::BTreeMap, Value> +{ + fn from(value: MapWithDateTimeKeys) -> Self { + value.0 + } +} +impl From<&MapWithDateTimeKeys> for MapWithDateTimeKeys { + fn from(value: &MapWithDateTimeKeys) -> Self { + value.clone() + } +} +impl From, Value>> + for MapWithDateTimeKeys +{ + fn from( + value: std::collections::BTreeMap, Value>, + ) -> Self { + Self(value) + } +} +#[doc = "MapWithKeys"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"object\","] +#[doc = " \"additionalProperties\": {"] +#[doc = " \"$ref\": \"#/definitions/Value\""] +#[doc = " },"] +#[doc = " \"propertyNames\": {"] +#[doc = " \"$ref\": \"#/definitions/Eh\""] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct MapWithKeys(pub std::collections::BTreeMap); +impl ::std::ops::Deref for MapWithKeys { + type Target = std::collections::BTreeMap; + fn deref(&self) -> &std::collections::BTreeMap { + &self.0 + } +} +impl From for std::collections::BTreeMap { + fn from(value: MapWithKeys) -> Self { + value.0 + } +} +impl From<&MapWithKeys> for MapWithKeys { + fn from(value: &MapWithKeys) -> Self { + value.clone() + } +} +impl From> for MapWithKeys { + fn from(value: std::collections::BTreeMap) -> Self { + Self(value) + } +} +#[doc = "Value"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"string\""] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub struct Value(pub ::std::string::String); +impl ::std::ops::Deref for Value { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl From for ::std::string::String { + fn from(value: Value) -> Self { + value.0 + } +} +impl From<&Value> for Value { + fn from(value: &Value) -> Self { + value.clone() + } +} +impl From<::std::string::String> for Value { + fn from(value: ::std::string::String) -> Self { + Self(value) + } +} +impl ::std::str::FromStr for Value { + type Err = ::std::convert::Infallible; + fn from_str(value: &str) -> ::std::result::Result { + Ok(Self(value.to_string())) + } +} +impl ::std::fmt::Display for Value { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + self.0.fmt(f) + } +} +fn main() {}