Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(indexmap): Add support for changing HashMaps to IndexMaps. #708

Merged
merged 9 commits into from
Dec 20, 2024
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`.
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved

### 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_type.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_type.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_type.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)],
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
TypeEntryDetails::Map { value_id, .. } => return vec![(None, value_id, false)],
_ => unreachable!(),
};

Expand Down
43 changes: 39 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_type: String,

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

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,
Expand Down Expand Up @@ -454,6 +471,24 @@ impl TypeSpaceSettings {
);
self
}

/// 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`.
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
///
/// ## Examples
///
/// - `::std::collections::HashMap`
/// - `::std::collections::BTreeMap`
/// - `::indexmap::IndexMap`
///
pub fn with_map_type(&mut self, map_type: String) -> &mut Self {
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
self.map_type = map_type;
self
}
}

impl TypeSpacePatch {
Expand Down Expand Up @@ -970,9 +1005,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_type.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);
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
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,
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
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);
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
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)
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
.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
Loading