Skip to content

Commit

Permalink
feat(indexmap): Add support for changing HashMaps to IndexMaps.
Browse files Browse the repository at this point in the history
  • Loading branch information
JosiahBull committed Dec 5, 2024
1 parent f409d37 commit dee9c58
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 28 deletions.
16 changes: 12 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ applied. Non-required properties with types that already have a default value
(such as a `Vec<T>`) simply get the `#[serde(default)]` attribute (so you won't
see e.g. `Option<Vec<T>>`).

### 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
Expand Down
1 change: 0 additions & 1 deletion typify-impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 8 additions & 1 deletion typify-impl/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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))
}

Expand Down
6 changes: 4 additions & 2 deletions typify-impl/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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!(),
};

Expand Down
32 changes: 28 additions & 4 deletions typify-impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,37 @@ pub(crate) enum DefaultImpl {
}

/// Settings that alter type generation.
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
pub struct TypeSpaceSettings {
type_mod: Option<String>,
extra_derives: Vec<String>,
struct_builder: bool,

unknown_crates: UnknownPolicy,
crates: BTreeMap<String, CrateSpec>,
map_to_use: String,

patch: BTreeMap<String, TypeSpacePatch>,
replace: BTreeMap<String, TypeSpaceReplace>,
convert: Vec<TypeSpaceConversion>,
}

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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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())),
Expand Down
28 changes: 23 additions & 5 deletions typify-impl/src/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -205,6 +206,7 @@ impl TypeSpace {

pub(crate) fn make_map(
&mut self,
map_to_use: String,
type_name: Option<String>,
property_names: &Option<Box<Schema>>,
additional_properties: &Option<Box<Schema>>,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,

Expand All @@ -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
Expand Down
25 changes: 19 additions & 6 deletions typify-impl/src/type_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeId>),
Expand Down Expand Up @@ -595,7 +599,7 @@ impl TypeEntry {
TypeEntryDetails::Unit
| TypeEntryDetails::Option(_)
| TypeEntryDetails::Vec(_)
| TypeEntryDetails::Map(_, _)
| TypeEntryDetails::Map { .. }
| TypeEntryDetails::Set(_) => {
matches!(impl_name, TypeSpaceImpl::Default)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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::<syn::TypePath>(map_to_use)
.expect("map type path wasn't valid");
quote! { #map_to_use<#key_ident, #value_ident> }
}
}

Expand Down Expand Up @@ -1746,7 +1757,7 @@ impl TypeEntry {
| TypeEntryDetails::Struct(_)
| TypeEntryDetails::Newtype(_)
| TypeEntryDetails::Vec(_)
| TypeEntryDetails::Map(..)
| TypeEntryDetails::Map { .. }
| TypeEntryDetails::Set(_)
| TypeEntryDetails::Box(_)
| TypeEntryDetails::Native(_)
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 4 additions & 2 deletions typify-impl/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ impl TypeEntry {
.collect::<Option<Vec<_>>>()?;
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();
Expand Down Expand Up @@ -424,7 +426,7 @@ fn value_for_struct_props(
match &type_entry.details {
TypeEntryDetails::Struct(_)
| TypeEntryDetails::Option(_)
| TypeEntryDetails::Map(..) => (),
| TypeEntryDetails::Map { .. } => (),
_ => unreachable!(),
}

Expand Down
3 changes: 2 additions & 1 deletion typify-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ 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"] }
prettyplease = "0.2.25"
schemars = "0.8.21"
serde = "1.0.215"
syn = "2.0.90"
typify = { path = "../typify" }
typify = { path = "../typify"}
Loading

0 comments on commit dee9c58

Please sign in to comment.