From dee9c581d7e6db9f2737758362e0aa33f0c40c5d Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Tue, 3 Dec 2024 15:04:10 +1300 Subject: [PATCH 1/9] feat(indexmap): Add support for changing HashMaps to IndexMaps. --- Cargo.lock | 16 ++++++++++++---- README.md | 7 +++++++ typify-impl/Cargo.toml | 1 - typify-impl/src/convert.rs | 9 ++++++++- typify-impl/src/defaults.rs | 6 ++++-- typify-impl/src/lib.rs | 32 +++++++++++++++++++++++++++---- typify-impl/src/structs.rs | 28 ++++++++++++++++++++++----- typify-impl/src/type_entry.rs | 25 ++++++++++++++++++------ typify-impl/src/value.rs | 6 ++++-- typify-test/Cargo.toml | 3 ++- typify-test/build.rs | 36 +++++++++++++++++++++++++++++++++-- typify-test/src/main.rs | 26 +++++++++++++++++++++++++ 12 files changed, 167 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15bb7677..6f9eaacc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.5.0" @@ -506,12 +512,13 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.1.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", + "serde", ] [[package]] @@ -786,7 +793,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1541daf4e4ed43a0922b7969bdc2170178bcacc5dabf7e39bc508a9fa3953a7a" dependencies = [ - "hashbrown", + "hashbrown 0.14.3", "memchr", ] @@ -1260,6 +1267,7 @@ dependencies = [ name = "typify-test" version = "0.0.0" dependencies = [ + "indexmap", "ipnetwork", "prettyplease", "regress", diff --git a/README.md b/README.md index 8b63f2bd..55e13c38 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,13 @@ 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>`). +### IndexMap + +By default, Typify uses `HashMap` for objects. If you prefer to use `IndexMap` +or some other object, you can specify this by calling `with_map_to_use` on the +`TypeSpaceSettings` object, and providing the full path to the type you want to +use. E.g. `::std::collections::HashMap` or `::indexmap::IndexMap`. + ### OneOf The `oneOf` construct maps to a Rust enum. Typify maps this to the various diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml index dad413cf..f3971109 100644 --- a/typify-impl/Cargo.toml +++ b/typify-impl/Cargo.toml @@ -21,7 +21,6 @@ syn = { version = "2.0.90", features = ["full"] } thiserror = "2.0.3" unicode-ident = "1.0.14" - [dev-dependencies] env_logger = "0.10.2" expectorate = "1.1.0" diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index a0cfc4d9..2548fed1 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -1200,6 +1200,7 @@ impl TypeSpace { != Some(&Schema::Bool(false)) => { let type_entry = self.make_map( + self.settings.map_to_use.clone(), type_name.into_option(), property_names, additional_properties, @@ -1236,6 +1237,7 @@ impl TypeSpace { )); let type_entry = self.make_map( + self.settings.map_to_use.clone(), type_name.into_option(), &property_names, &additional_properties, @@ -1245,7 +1247,12 @@ impl TypeSpace { } None => { - let type_entry = self.make_map(type_name.into_option(), &None, &None)?; + let type_entry = self.make_map( + self.settings.map_to_use.clone(), + type_name.into_option(), + &None, + &None, + )?; Ok((type_entry, metadata)) } diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs index 94dc1b67..9d50b0a2 100644 --- a/typify-impl/src/defaults.rs +++ b/typify-impl/src/defaults.rs @@ -199,7 +199,9 @@ impl TypeEntry { Err(Error::invalid_value()) } } - TypeEntryDetails::Map(key_id, value_id) => { + TypeEntryDetails::Map { + key_id, value_id, .. + } => { if let serde_json::Value::Object(m) = default { if m.is_empty() { Ok(DefaultKind::Intrinsic) @@ -620,7 +622,7 @@ fn all_props<'a>( // TODO Rather than an option, this should probably be something // that lets us say "explicit name" or "type to validate against" - TypeEntryDetails::Map(_, value_id) => return vec![(None, value_id, false)], + TypeEntryDetails::Map { value_id, .. } => return vec![(None, value_id, false)], _ => unreachable!(), }; diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index ff036eeb..bf8563b1 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -238,7 +238,7 @@ pub(crate) enum DefaultImpl { } /// Settings that alter type generation. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct TypeSpaceSettings { type_mod: Option, extra_derives: Vec, @@ -246,12 +246,29 @@ pub struct TypeSpaceSettings { unknown_crates: UnknownPolicy, crates: BTreeMap, + map_to_use: String, patch: BTreeMap, replace: BTreeMap, convert: Vec, } +impl Default for TypeSpaceSettings { + fn default() -> Self { + Self { + map_to_use: "::std::collections::HashMap".to_string(), + type_mod: Default::default(), + extra_derives: Default::default(), + struct_builder: Default::default(), + unknown_crates: Default::default(), + crates: Default::default(), + patch: Default::default(), + replace: Default::default(), + convert: Default::default(), + } + } +} + #[derive(Debug, Clone)] struct CrateSpec { version: CrateVers, @@ -454,6 +471,13 @@ impl TypeSpaceSettings { ); self } + + /// Specify the map implementation to use in generated types. The default is + /// `std::collections::HashMap`. + pub fn with_map_to_use(&mut self, map_to_use: String) -> &mut Self { + self.map_to_use = map_to_use; + self + } } impl TypeSpacePatch { @@ -970,9 +994,9 @@ impl<'a> Type<'a> { // Compound types TypeEntryDetails::Option(type_id) => TypeDetails::Option(type_id.clone()), TypeEntryDetails::Vec(type_id) => TypeDetails::Vec(type_id.clone()), - TypeEntryDetails::Map(key_id, value_id) => { - TypeDetails::Map(key_id.clone(), value_id.clone()) - } + TypeEntryDetails::Map { + key_id, value_id, .. + } => TypeDetails::Map(key_id.clone(), value_id.clone()), TypeEntryDetails::Set(type_id) => TypeDetails::Set(type_id.clone()), TypeEntryDetails::Box(type_id) => TypeDetails::Box(type_id.clone()), TypeEntryDetails::Tuple(types) => TypeDetails::Tuple(Box::new(types.iter().cloned())), diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs index 5f8dfb47..f0bab169 100644 --- a/typify-impl/src/structs.rs +++ b/typify-impl/src/structs.rs @@ -119,6 +119,7 @@ impl TypeSpace { additional_properties @ Some(_) => { let sub_type_name = type_name.as_ref().map(|base| format!("{}_extra", base)); let map_type = self.make_map( + self.settings.map_to_use.clone(), sub_type_name, &validation.property_names, additional_properties, @@ -205,6 +206,7 @@ impl TypeSpace { pub(crate) fn make_map( &mut self, + map_to_use: String, type_name: Option, property_names: &Option>, additional_properties: &Option>, @@ -237,7 +239,12 @@ impl TypeSpace { None => self.id_for_schema(Name::Unknown, &Schema::Bool(true))?, }; - Ok(TypeEntryDetails::Map(key_id, value_id).into()) + Ok(TypeEntryDetails::Map { + map_to_use, + key_id, + value_id, + } + .into()) } /// Perform a schema conversion for a type that must be string-like. @@ -381,7 +388,14 @@ pub(crate) fn generate_serde_attr( serde_options.push(quote! { skip_serializing_if = "::std::vec::Vec::is_empty" }); DefaultFunction::Default } - (StructPropertyState::Optional, TypeEntryDetails::Map(key_id, value_id)) => { + ( + StructPropertyState::Optional, + TypeEntryDetails::Map { + map_to_use, + key_id, + value_id, + }, + ) => { serde_options.push(quote! { default }); let key_ty = type_space @@ -400,8 +414,10 @@ pub(crate) fn generate_serde_attr( skip_serializing_if = "::serde_json::Map::is_empty" }); } else { + // Append ::is_empty to the string. + let map_to_use = format!("{}::is_empty", map_to_use); serde_options.push(quote! { - skip_serializing_if = "::std::collections::HashMap::is_empty" + skip_serializing_if = #map_to_use }); } DefaultFunction::Default @@ -458,7 +474,7 @@ fn has_default( // No default specified. (Some(TypeEntryDetails::Option(_)), None) => StructPropertyState::Optional, (Some(TypeEntryDetails::Vec(_)), None) => StructPropertyState::Optional, - (Some(TypeEntryDetails::Map(..)), None) => StructPropertyState::Optional, + (Some(TypeEntryDetails::Map { .. }), None) => StructPropertyState::Optional, (Some(TypeEntryDetails::Unit), None) => StructPropertyState::Optional, (_, None) => StructPropertyState::Required, @@ -471,7 +487,9 @@ fn has_default( StructPropertyState::Optional } // Default specified is the same as the implicit default: {} - (Some(TypeEntryDetails::Map(..)), Some(serde_json::Value::Object(m))) if m.is_empty() => { + (Some(TypeEntryDetails::Map { .. }), Some(serde_json::Value::Object(m))) + if m.is_empty() => + { StructPropertyState::Optional } // Default specified is the same as the implicit default: false diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 0d692d17..a2620562 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -146,7 +146,11 @@ pub(crate) enum TypeEntryDetails { Option(TypeId), Box(TypeId), Vec(TypeId), - Map(TypeId, TypeId), + Map { + map_to_use: String, + key_id: TypeId, + value_id: TypeId, + }, Set(TypeId), Array(TypeId, usize), Tuple(Vec), @@ -595,7 +599,7 @@ impl TypeEntry { TypeEntryDetails::Unit | TypeEntryDetails::Option(_) | TypeEntryDetails::Vec(_) - | TypeEntryDetails::Map(_, _) + | TypeEntryDetails::Map { .. } | TypeEntryDetails::Set(_) => { matches!(impl_name, TypeSpaceImpl::Default) } @@ -1618,7 +1622,11 @@ impl TypeEntry { quote! { ::std::vec::Vec<#item> } } - TypeEntryDetails::Map(key_id, value_id) => { + TypeEntryDetails::Map { + map_to_use, + key_id, + value_id, + } => { let key_ty = type_space .id_to_entry .get(key_id) @@ -1635,7 +1643,10 @@ 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 = syn::parse_str::(map_to_use) + .expect("map type path wasn't valid"); + quote! { #map_to_use<#key_ident, #value_ident> } } } @@ -1746,7 +1757,7 @@ impl TypeEntry { | TypeEntryDetails::Struct(_) | TypeEntryDetails::Newtype(_) | TypeEntryDetails::Vec(_) - | TypeEntryDetails::Map(..) + | TypeEntryDetails::Map { .. } | TypeEntryDetails::Set(_) | TypeEntryDetails::Box(_) | TypeEntryDetails::Native(_) @@ -1815,7 +1826,9 @@ impl TypeEntry { TypeEntryDetails::Unit => "()".to_string(), TypeEntryDetails::Option(type_id) => format!("option {}", type_id.0), TypeEntryDetails::Vec(type_id) => format!("vec {}", type_id.0), - TypeEntryDetails::Map(key_id, value_id) => { + TypeEntryDetails::Map { + key_id, value_id, .. + } => { format!("map {} {}", key_id.0, value_id.0) } TypeEntryDetails::Set(type_id) => format!("set {}", type_id.0), diff --git a/typify-impl/src/value.rs b/typify-impl/src/value.rs index 58012f81..fc1d85f7 100644 --- a/typify-impl/src/value.rs +++ b/typify-impl/src/value.rs @@ -95,7 +95,9 @@ impl TypeEntry { .collect::>>()?; quote! { vec![#(#values),*] } } - TypeEntryDetails::Map(key_id, value_id) => { + TypeEntryDetails::Map { + key_id, value_id, .. + } => { let obj = value.as_object()?; let key_ty = type_space.id_to_entry.get(key_id).unwrap(); let value_ty = type_space.id_to_entry.get(value_id).unwrap(); @@ -424,7 +426,7 @@ fn value_for_struct_props( match &type_entry.details { TypeEntryDetails::Struct(_) | TypeEntryDetails::Option(_) - | TypeEntryDetails::Map(..) => (), + | TypeEntryDetails::Map { .. } => (), _ => unreachable!(), } diff --git a/typify-test/Cargo.toml b/typify-test/Cargo.toml index 2b4d7f50..6a536cc7 100644 --- a/typify-test/Cargo.toml +++ b/typify-test/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" regress = "0.10.1" serde = "1.0.215" serde_json = "1.0.133" +indexmap = { version = "2.7.0", features = ["serde"]} [build-dependencies] ipnetwork = { version = "0.20.0", features = ["schemars"] } @@ -14,4 +15,4 @@ prettyplease = "0.2.25" schemars = "0.8.21" serde = "1.0.215" syn = "2.0.90" -typify = { path = "../typify" } +typify = { path = "../typify"} diff --git a/typify-test/build.rs b/typify-test/build.rs index c8ffe9f6..83f1e36c 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 IndexMap + let mut settings = TypeSpaceSettings::default(); + settings.with_map_to_use("::indexmap::IndexMap".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_indexmap.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..7b7cdc14 100644 --- a/typify-test/src/main.rs +++ b/typify-test/src/main.rs @@ -49,3 +49,29 @@ 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 indexmap { + use indexmap::IndexMap; + + include!(concat!(env!("OUT_DIR"), "/codegen_indexmap.rs")); + + #[test] + fn test_with_map() { + // Validate that a map is represented as an IndexMap when requested. + let _ = WithMap { + map: IndexMap::new(), + }; + } +} From bb7dba660576e0e58cf906cd974e34649e7e8b36 Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Fri, 6 Dec 2024 09:39:27 +1300 Subject: [PATCH 2/9] refactor: rename map_to_use to map_type. --- typify-impl/src/convert.rs | 6 +++--- typify-impl/src/lib.rs | 23 +++++++++++++++++------ typify-impl/src/structs.rs | 2 +- typify-test/build.rs | 2 +- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 2548fed1..f581ae74 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -1200,7 +1200,7 @@ impl TypeSpace { != Some(&Schema::Bool(false)) => { let type_entry = self.make_map( - self.settings.map_to_use.clone(), + self.settings.map_type.clone(), type_name.into_option(), property_names, additional_properties, @@ -1237,7 +1237,7 @@ impl TypeSpace { )); let type_entry = self.make_map( - self.settings.map_to_use.clone(), + self.settings.map_type.clone(), type_name.into_option(), &property_names, &additional_properties, @@ -1248,7 +1248,7 @@ impl TypeSpace { None => { let type_entry = self.make_map( - self.settings.map_to_use.clone(), + self.settings.map_type.clone(), type_name.into_option(), &None, &None, diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index bf8563b1..f45ccf12 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -246,7 +246,7 @@ pub struct TypeSpaceSettings { unknown_crates: UnknownPolicy, crates: BTreeMap, - map_to_use: String, + map_type: String, patch: BTreeMap, replace: BTreeMap, @@ -256,7 +256,7 @@ pub struct TypeSpaceSettings { impl Default for TypeSpaceSettings { fn default() -> Self { Self { - map_to_use: "::std::collections::HashMap".to_string(), + map_type: "::std::collections::HashMap".to_string(), type_mod: Default::default(), extra_derives: Default::default(), struct_builder: Default::default(), @@ -472,10 +472,21 @@ impl TypeSpaceSettings { self } - /// Specify the map implementation to use in generated types. The default is - /// `std::collections::HashMap`. - pub fn with_map_to_use(&mut self, map_to_use: String) -> &mut Self { - self.map_to_use = map_to_use; + /// Specify the map-like type to be used in generated code. + /// + /// ## Requirements + /// + /// - The type must have an `is_empty` method that returns a boolean. + /// - The type must have two generic parameters, `K` and `V`. + /// + /// ## Examples + /// + /// - `::std::collections::HashMap` + /// - `::std::collections::BTreeMap` + /// - `::indexmap::IndexMap` + /// + pub fn with_map_type(&mut self, map_type: String) -> &mut Self { + self.map_type = map_type; self } } diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs index f0bab169..1293a965 100644 --- a/typify-impl/src/structs.rs +++ b/typify-impl/src/structs.rs @@ -119,7 +119,7 @@ impl TypeSpace { additional_properties @ Some(_) => { let sub_type_name = type_name.as_ref().map(|base| format!("{}_extra", base)); let map_type = self.make_map( - self.settings.map_to_use.clone(), + self.settings.map_type.clone(), sub_type_name, &validation.property_names, additional_properties, diff --git a/typify-test/build.rs b/typify-test/build.rs index 83f1e36c..c25216e4 100644 --- a/typify-test/build.rs +++ b/typify-test/build.rs @@ -133,7 +133,7 @@ fn main() { // Generate with IndexMap let mut settings = TypeSpaceSettings::default(); - settings.with_map_to_use("::indexmap::IndexMap".to_string()); + settings.with_map_type("::indexmap::IndexMap".to_string()); let mut type_space = TypeSpace::new(&settings); WithMap::add(&mut type_space); From 85c55d0ff4be511f057eaabdd25413084237ad48 Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Sat, 7 Dec 2024 09:18:28 +1300 Subject: [PATCH 3/9] refactor: move map_to_use into global namespace. Other: improve documentation & test cases. --- Cargo.lock | 2 -- README.md | 13 +++++++++---- typify-impl/src/convert.rs | 9 +-------- typify-impl/src/defaults.rs | 6 ++---- typify-impl/src/lib.rs | 20 ++++++++++++-------- typify-impl/src/structs.rs | 24 +++++------------------- typify-impl/src/type_entry.rs | 17 ++++------------- typify-impl/src/value.rs | 4 +--- typify-test/Cargo.toml | 1 - typify-test/build.rs | 8 ++++---- typify-test/src/main.rs | 18 +++++++++++++----- 11 files changed, 51 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f9eaacc..43a4c483 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,7 +518,6 @@ checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown 0.15.2", - "serde", ] [[package]] @@ -1267,7 +1266,6 @@ dependencies = [ name = "typify-test" version = "0.0.0" dependencies = [ - "indexmap", "ipnetwork", "prettyplease", "regress", diff --git a/README.md b/README.md index 55e13c38..82044c32 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,17 @@ 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>`). -### IndexMap +#### Alternate Map types -By default, Typify uses `HashMap` for objects. If you prefer to use `IndexMap` -or some other object, you can specify this by calling `with_map_to_use` on the +By default, Typify uses `std::collections::HashMap` as described above. + +If you prefer to use `std::collections::BTreeMap` or 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::HashMap` or `::indexmap::IndexMap`. +use. E.g. `::std::collections::BTreeMap` or `::indexmap::IndexMap`. + +See the documentation for `TypeSpaceSettings::with_map_type` for the +requirements for a map type. ### OneOf diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index f581ae74..a0cfc4d9 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -1200,7 +1200,6 @@ impl TypeSpace { != Some(&Schema::Bool(false)) => { let type_entry = self.make_map( - self.settings.map_type.clone(), type_name.into_option(), property_names, additional_properties, @@ -1237,7 +1236,6 @@ impl TypeSpace { )); let type_entry = self.make_map( - self.settings.map_type.clone(), type_name.into_option(), &property_names, &additional_properties, @@ -1247,12 +1245,7 @@ impl TypeSpace { } None => { - let type_entry = self.make_map( - self.settings.map_type.clone(), - type_name.into_option(), - &None, - &None, - )?; + let type_entry = self.make_map(type_name.into_option(), &None, &None)?; Ok((type_entry, metadata)) } diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs index 9d50b0a2..9a1cbbba 100644 --- a/typify-impl/src/defaults.rs +++ b/typify-impl/src/defaults.rs @@ -199,9 +199,7 @@ impl TypeEntry { Err(Error::invalid_value()) } } - TypeEntryDetails::Map { - key_id, value_id, .. - } => { + TypeEntryDetails::Map(key_id, value_id) => { if let serde_json::Value::Object(m) = default { if m.is_empty() { Ok(DefaultKind::Intrinsic) @@ -622,7 +620,7 @@ fn all_props<'a>( // TODO Rather than an option, this should probably be something // that lets us say "explicit name" or "type to validate against" - TypeEntryDetails::Map { value_id, .. } => return vec![(None, value_id, false)], + TypeEntryDetails::Map(value_id, _) => return vec![(None, value_id, false)], _ => unreachable!(), }; diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index f45ccf12..28f5cd33 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -476,14 +476,18 @@ impl TypeSpaceSettings { /// /// ## Requirements /// - /// - The type must have an `is_empty` method that returns a boolean. - /// - The type must have two generic parameters, `K` and `V`. + /// - 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` + /// - [`::std::collections::HashMap`] + /// - [`::std::collections::BTreeMap`] + /// - [`::indexmap::IndexMap`] /// pub fn with_map_type(&mut self, map_type: String) -> &mut Self { self.map_type = map_type; @@ -1005,9 +1009,9 @@ impl<'a> Type<'a> { // Compound types TypeEntryDetails::Option(type_id) => TypeDetails::Option(type_id.clone()), TypeEntryDetails::Vec(type_id) => TypeDetails::Vec(type_id.clone()), - TypeEntryDetails::Map { - key_id, value_id, .. - } => TypeDetails::Map(key_id.clone(), value_id.clone()), + TypeEntryDetails::Map(key_id, value_id) => { + TypeDetails::Map(key_id.clone(), value_id.clone()) + } TypeEntryDetails::Set(type_id) => TypeDetails::Set(type_id.clone()), TypeEntryDetails::Box(type_id) => TypeDetails::Box(type_id.clone()), TypeEntryDetails::Tuple(types) => TypeDetails::Tuple(Box::new(types.iter().cloned())), diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs index 1293a965..7e81919d 100644 --- a/typify-impl/src/structs.rs +++ b/typify-impl/src/structs.rs @@ -119,7 +119,6 @@ impl TypeSpace { additional_properties @ Some(_) => { let sub_type_name = type_name.as_ref().map(|base| format!("{}_extra", base)); let map_type = self.make_map( - self.settings.map_type.clone(), sub_type_name, &validation.property_names, additional_properties, @@ -206,7 +205,6 @@ impl TypeSpace { pub(crate) fn make_map( &mut self, - map_to_use: String, type_name: Option, property_names: &Option>, additional_properties: &Option>, @@ -239,12 +237,7 @@ impl TypeSpace { None => self.id_for_schema(Name::Unknown, &Schema::Bool(true))?, }; - Ok(TypeEntryDetails::Map { - map_to_use, - key_id, - value_id, - } - .into()) + Ok(TypeEntryDetails::Map(key_id, value_id).into()) } /// Perform a schema conversion for a type that must be string-like. @@ -388,16 +381,10 @@ pub(crate) fn generate_serde_attr( serde_options.push(quote! { skip_serializing_if = "::std::vec::Vec::is_empty" }); DefaultFunction::Default } - ( - StructPropertyState::Optional, - TypeEntryDetails::Map { - map_to_use, - key_id, - value_id, - }, - ) => { + (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) @@ -414,10 +401,9 @@ pub(crate) fn generate_serde_attr( skip_serializing_if = "::serde_json::Map::is_empty" }); } else { - // Append ::is_empty to the string. - let map_to_use = format!("{}::is_empty", map_to_use); + let is_empty = format!("{}::is_empty", map_to_use); serde_options.push(quote! { - skip_serializing_if = #map_to_use + skip_serializing_if = #is_empty }); } DefaultFunction::Default diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index a2620562..16c35413 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -146,11 +146,7 @@ pub(crate) enum TypeEntryDetails { Option(TypeId), Box(TypeId), Vec(TypeId), - Map { - map_to_use: String, - key_id: TypeId, - value_id: TypeId, - }, + Map(TypeId, TypeId), Set(TypeId), Array(TypeId, usize), Tuple(Vec), @@ -1622,11 +1618,8 @@ impl TypeEntry { quote! { ::std::vec::Vec<#item> } } - TypeEntryDetails::Map { - map_to_use, - key_id, - value_id, - } => { + 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) @@ -1826,9 +1819,7 @@ impl TypeEntry { TypeEntryDetails::Unit => "()".to_string(), TypeEntryDetails::Option(type_id) => format!("option {}", type_id.0), TypeEntryDetails::Vec(type_id) => format!("vec {}", type_id.0), - TypeEntryDetails::Map { - key_id, value_id, .. - } => { + TypeEntryDetails::Map(key_id, value_id) => { format!("map {} {}", key_id.0, value_id.0) } TypeEntryDetails::Set(type_id) => format!("set {}", type_id.0), diff --git a/typify-impl/src/value.rs b/typify-impl/src/value.rs index fc1d85f7..e4e5c68c 100644 --- a/typify-impl/src/value.rs +++ b/typify-impl/src/value.rs @@ -95,9 +95,7 @@ impl TypeEntry { .collect::>>()?; quote! { vec![#(#values),*] } } - TypeEntryDetails::Map { - key_id, value_id, .. - } => { + TypeEntryDetails::Map(key_id, value_id, ..) => { let obj = value.as_object()?; let key_ty = type_space.id_to_entry.get(key_id).unwrap(); let value_ty = type_space.id_to_entry.get(value_id).unwrap(); diff --git a/typify-test/Cargo.toml b/typify-test/Cargo.toml index 6a536cc7..d8626ab1 100644 --- a/typify-test/Cargo.toml +++ b/typify-test/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" regress = "0.10.1" serde = "1.0.215" serde_json = "1.0.133" -indexmap = { version = "2.7.0", features = ["serde"]} [build-dependencies] ipnetwork = { version = "0.20.0", features = ["schemars"] } diff --git a/typify-test/build.rs b/typify-test/build.rs index c25216e4..34a87441 100644 --- a/typify-test/build.rs +++ b/typify-test/build.rs @@ -56,7 +56,7 @@ struct WithSet { #[allow(dead_code)] #[derive(JsonSchema)] struct WithMap { - map: HashMap, + map: HashMap, } struct LoginName; @@ -131,9 +131,9 @@ fn main() { out_file.push("codegen_hashmap.rs"); fs::write(out_file, contents).unwrap(); - // Generate with IndexMap + // Generate with a custom map type to validate requirements. let mut settings = TypeSpaceSettings::default(); - settings.with_map_type("::indexmap::IndexMap".to_string()); + settings.with_map_type("CustomMap".to_string()); let mut type_space = TypeSpace::new(&settings); WithMap::add(&mut type_space); @@ -142,7 +142,7 @@ fn main() { 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_indexmap.rs"); + out_file.push("codegen_custommap.rs"); fs::write(out_file, contents).unwrap(); } diff --git a/typify-test/src/main.rs b/typify-test/src/main.rs index 7b7cdc14..f3e9d219 100644 --- a/typify-test/src/main.rs +++ b/typify-test/src/main.rs @@ -62,16 +62,24 @@ mod hashmap { } } -mod indexmap { - use indexmap::IndexMap; +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_indexmap.rs")); + include!(concat!(env!("OUT_DIR"), "/codegen_custommap.rs")); #[test] fn test_with_map() { - // Validate that a map is represented as an IndexMap when requested. + // Validate that a map is represented as an CustomMap when requested. let _ = WithMap { - map: IndexMap::new(), + map: CustomMap { + key: String::new(), + value: String::new(), + }, }; } } From 304665889c85a0ec6e4d8ff1141fdd812519fceb Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Sat, 7 Dec 2024 09:30:36 +1300 Subject: [PATCH 4/9] test: added maps_custom test case to schemas. --- typify/tests/schemas.rs | 26 ++- typify/tests/schemas/maps_custom.rs | 298 ++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+), 5 deletions(-) create mode 100644 typify/tests/schemas/maps_custom.rs 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() {} From f836f36012eb9eb292aec8d39d65ba08f4a244c8 Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Sat, 7 Dec 2024 09:39:20 +1300 Subject: [PATCH 5/9] chore: small formatting changes. --- Cargo.lock | 14 ++++---------- typify-impl/Cargo.toml | 1 + typify-impl/src/defaults.rs | 2 +- typify-impl/src/structs.rs | 6 ++---- typify-impl/src/type_entry.rs | 4 ++-- typify-impl/src/value.rs | 4 ++-- typify-test/Cargo.toml | 2 +- 7 files changed, 13 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43a4c483..15bb7677 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,12 +448,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - [[package]] name = "heck" version = "0.5.0" @@ -512,12 +506,12 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.7.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown", ] [[package]] @@ -792,7 +786,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1541daf4e4ed43a0922b7969bdc2170178bcacc5dabf7e39bc508a9fa3953a7a" dependencies = [ - "hashbrown 0.14.3", + "hashbrown", "memchr", ] diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml index f3971109..dad413cf 100644 --- a/typify-impl/Cargo.toml +++ b/typify-impl/Cargo.toml @@ -21,6 +21,7 @@ syn = { version = "2.0.90", features = ["full"] } thiserror = "2.0.3" unicode-ident = "1.0.14" + [dev-dependencies] env_logger = "0.10.2" expectorate = "1.1.0" diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs index 9a1cbbba..94dc1b67 100644 --- a/typify-impl/src/defaults.rs +++ b/typify-impl/src/defaults.rs @@ -620,7 +620,7 @@ fn all_props<'a>( // TODO Rather than an option, this should probably be something // that lets us say "explicit name" or "type to validate against" - TypeEntryDetails::Map(value_id, _) => return vec![(None, value_id, false)], + TypeEntryDetails::Map(_, value_id) => return vec![(None, value_id, false)], _ => unreachable!(), }; diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs index 7e81919d..a3e68f37 100644 --- a/typify-impl/src/structs.rs +++ b/typify-impl/src/structs.rs @@ -460,7 +460,7 @@ fn has_default( // No default specified. (Some(TypeEntryDetails::Option(_)), None) => StructPropertyState::Optional, (Some(TypeEntryDetails::Vec(_)), None) => StructPropertyState::Optional, - (Some(TypeEntryDetails::Map { .. }), None) => StructPropertyState::Optional, + (Some(TypeEntryDetails::Map(..)), None) => StructPropertyState::Optional, (Some(TypeEntryDetails::Unit), None) => StructPropertyState::Optional, (_, None) => StructPropertyState::Required, @@ -473,9 +473,7 @@ fn has_default( StructPropertyState::Optional } // Default specified is the same as the implicit default: {} - (Some(TypeEntryDetails::Map { .. }), Some(serde_json::Value::Object(m))) - if m.is_empty() => - { + (Some(TypeEntryDetails::Map(..)), Some(serde_json::Value::Object(m))) if m.is_empty() => { StructPropertyState::Optional } // Default specified is the same as the implicit default: false diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 16c35413..cdb9585b 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -595,7 +595,7 @@ impl TypeEntry { TypeEntryDetails::Unit | TypeEntryDetails::Option(_) | TypeEntryDetails::Vec(_) - | TypeEntryDetails::Map { .. } + | TypeEntryDetails::Map(_, _) | TypeEntryDetails::Set(_) => { matches!(impl_name, TypeSpaceImpl::Default) } @@ -1750,7 +1750,7 @@ impl TypeEntry { | TypeEntryDetails::Struct(_) | TypeEntryDetails::Newtype(_) | TypeEntryDetails::Vec(_) - | TypeEntryDetails::Map { .. } + | TypeEntryDetails::Map(..) | TypeEntryDetails::Set(_) | TypeEntryDetails::Box(_) | TypeEntryDetails::Native(_) diff --git a/typify-impl/src/value.rs b/typify-impl/src/value.rs index e4e5c68c..58012f81 100644 --- a/typify-impl/src/value.rs +++ b/typify-impl/src/value.rs @@ -95,7 +95,7 @@ impl TypeEntry { .collect::>>()?; quote! { vec![#(#values),*] } } - TypeEntryDetails::Map(key_id, value_id, ..) => { + TypeEntryDetails::Map(key_id, value_id) => { let obj = value.as_object()?; let key_ty = type_space.id_to_entry.get(key_id).unwrap(); let value_ty = type_space.id_to_entry.get(value_id).unwrap(); @@ -424,7 +424,7 @@ fn value_for_struct_props( match &type_entry.details { TypeEntryDetails::Struct(_) | TypeEntryDetails::Option(_) - | TypeEntryDetails::Map { .. } => (), + | TypeEntryDetails::Map(..) => (), _ => unreachable!(), } diff --git a/typify-test/Cargo.toml b/typify-test/Cargo.toml index d8626ab1..2b4d7f50 100644 --- a/typify-test/Cargo.toml +++ b/typify-test/Cargo.toml @@ -14,4 +14,4 @@ prettyplease = "0.2.25" schemars = "0.8.21" serde = "1.0.215" syn = "2.0.90" -typify = { path = "../typify"} +typify = { path = "../typify" } From 0d114304f26e8c034a1318b04ac4ae8b8a71432a Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Sun, 8 Dec 2024 10:00:56 +1300 Subject: [PATCH 6/9] Update README.md Co-authored-by: Adam Leventhal --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82044c32..05ebed48 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ see e.g. `Option>`). By default, Typify uses `std::collections::HashMap` as described above. -If you prefer to use `std::collections::BTreeMap` or map type from a crate such +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`. From eb67706f919aa880df221fc4c8c91a5640af4ec2 Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Sun, 8 Dec 2024 10:31:29 +1300 Subject: [PATCH 7/9] feat: support custom map types in cargo-typify and typify-macro. --- cargo-typify/src/lib.rs | 33 ++ cargo-typify/tests/integration.rs | 26 ++ cargo-typify/tests/outputs/builder.rs | 19 +- .../tests/outputs/custom_btree_map.rs | 312 ++++++++++++++++++ cargo-typify/tests/outputs/derive.rs | 19 +- cargo-typify/tests/outputs/help.txt | 3 + cargo-typify/tests/outputs/multi_derive.rs | 19 +- cargo-typify/tests/outputs/no-builder.rs | 19 +- example.json | 5 +- typify-impl/src/lib.rs | 33 +- typify-impl/src/structs.rs | 2 +- typify-impl/src/type_entry.rs | 2 +- typify-macro/src/lib.rs | 9 +- 13 files changed, 450 insertions(+), 51 deletions(-) create mode 100644 cargo-typify/tests/outputs/custom_btree_map.rs 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 28f5cd33..b72a5ef7 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -237,8 +237,19 @@ pub(crate) enum DefaultImpl { NZU64, } +/// Type name to use in generated code. +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(transparent)] +pub struct MapType(pub String); + +impl Default for MapType { + fn default() -> Self { + Self("::std::collections::HashMap".to_string()) + } +} + /// Settings that alter type generation. -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct TypeSpaceSettings { type_mod: Option, extra_derives: Vec, @@ -246,29 +257,13 @@ pub struct TypeSpaceSettings { unknown_crates: UnknownPolicy, crates: BTreeMap, - map_type: String, + map_type: MapType, patch: BTreeMap, replace: BTreeMap, convert: Vec, } -impl Default for TypeSpaceSettings { - fn default() -> Self { - Self { - map_type: "::std::collections::HashMap".to_string(), - type_mod: Default::default(), - extra_derives: Default::default(), - struct_builder: Default::default(), - unknown_crates: Default::default(), - crates: Default::default(), - patch: Default::default(), - replace: Default::default(), - convert: Default::default(), - } - } -} - #[derive(Debug, Clone)] struct CrateSpec { version: CrateVers, @@ -490,7 +485,7 @@ impl TypeSpaceSettings { /// - [`::indexmap::IndexMap`] /// pub fn with_map_type(&mut self, map_type: String) -> &mut Self { - self.map_type = map_type; + self.map_type = MapType(map_type); self } } diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs index a3e68f37..d111c270 100644 --- a/typify-impl/src/structs.rs +++ b/typify-impl/src/structs.rs @@ -401,7 +401,7 @@ pub(crate) fn generate_serde_attr( skip_serializing_if = "::serde_json::Map::is_empty" }); } else { - let is_empty = format!("{}::is_empty", map_to_use); + let is_empty = format!("{}::is_empty", map_to_use.0); serde_options.push(quote! { skip_serializing_if = #is_empty }); diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index cdb9585b..055c73aa 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -1637,7 +1637,7 @@ impl TypeEntry { let key_ident = key_ty.type_ident(type_space, type_mod); let value_ident = value_ty.type_ident(type_space, type_mod); - let map_to_use = syn::parse_str::(map_to_use) + let map_to_use = syn::parse_str::(&map_to_use.0) .expect("map type path wasn't valid"); quote! { #map_to_use<#key_ident, #value_ident> } } diff --git a/typify-macro/src/lib.rs b/typify-macro/src/lib.rs index 264749d4..0d96b190 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.0); + (schema.into_inner(), settings) }; From f4f18d6d4e19f1e60211227aa8d6f2c9efd3eec1 Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Mon, 16 Dec 2024 10:40:19 +1300 Subject: [PATCH 8/9] chore: update docs --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 05ebed48..cde7fb03 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ 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. From f17ac89d12fa1fb19f008de2e5930b409d55655d Mon Sep 17 00:00:00 2001 From: Josiah Bull Date: Mon, 16 Dec 2024 11:31:20 +1300 Subject: [PATCH 9/9] refactor: use syn::Type instead of String to store MapType. --- typify-impl/src/lib.rs | 59 +++++++++++++++++++++++++++++++---- typify-impl/src/structs.rs | 2 +- typify-impl/src/type_entry.rs | 3 +- typify-macro/src/lib.rs | 2 +- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index b72a5ef7..e003caa9 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -238,13 +238,60 @@ pub(crate) enum DefaultImpl { } /// Type name to use in generated code. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -#[serde(transparent)] -pub struct MapType(pub String); +#[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("::std::collections::HashMap".to_string()) + 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) } } @@ -484,8 +531,8 @@ impl TypeSpaceSettings { /// - [`::std::collections::BTreeMap`] /// - [`::indexmap::IndexMap`] /// - pub fn with_map_type(&mut self, map_type: String) -> &mut Self { - self.map_type = MapType(map_type); + pub fn with_map_type>(&mut self, map_type: T) -> &mut Self { + self.map_type = map_type.into(); self } } diff --git a/typify-impl/src/structs.rs b/typify-impl/src/structs.rs index d111c270..a3e68f37 100644 --- a/typify-impl/src/structs.rs +++ b/typify-impl/src/structs.rs @@ -401,7 +401,7 @@ pub(crate) fn generate_serde_attr( skip_serializing_if = "::serde_json::Map::is_empty" }); } else { - let is_empty = format!("{}::is_empty", map_to_use.0); + let is_empty = format!("{}::is_empty", map_to_use); serde_options.push(quote! { skip_serializing_if = #is_empty }); diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 055c73aa..97a4d479 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -1636,9 +1636,8 @@ 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); + let map_to_use = &map_to_use.0; - let map_to_use = syn::parse_str::(&map_to_use.0) - .expect("map type path wasn't valid"); quote! { #map_to_use<#key_ident, #value_ident> } } } diff --git a/typify-macro/src/lib.rs b/typify-macro/src/lib.rs index 0d96b190..3262c1ac 100644 --- a/typify-macro/src/lib.rs +++ b/typify-macro/src/lib.rs @@ -223,7 +223,7 @@ fn do_import_types(item: TokenStream) -> Result { ); settings.with_unknown_crates(unknown_crates); - settings.with_map_type(map_type.0); + settings.with_map_type(map_type); (schema.into_inner(), settings) };