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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ 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>>`).

#### Alternate Map types

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
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
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`.

See the documentation for `TypeSpaceSettings::with_map_type` for the
requirements for a map type.

### OneOf

The `oneOf` construct maps to a Rust enum. Typify maps this to the various
Expand Down
41 changes: 40 additions & 1 deletion 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,28 @@ impl TypeSpaceSettings {
);
self
}

/// Specify the map-like type to be used in generated code.
///
/// ## Requirements
///
/// - Have an `is_empty` method that returns a boolean.
/// - Have two generic parameters, `K` and `V`.
/// - Have a [`std::fmt::Debug`] impl.
/// - Have a [`serde::Serialize``] impl.
/// - Have a [`serde::Deserialize``] impl.
/// - Have a [`Clone`] impl.
///
/// ## Examples
///
/// - [`::std::collections::HashMap`]
/// - [`::std::collections::BTreeMap`]
/// - [`::indexmap::IndexMap`]
///
pub fn with_map_type(&mut self, map_type: String) -> &mut Self {
JosiahBull marked this conversation as resolved.
Show resolved Hide resolved
self.map_type = map_type;
self
}
}

impl TypeSpacePatch {
Expand Down
4 changes: 3 additions & 1 deletion typify-impl/src/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ pub(crate) fn generate_serde_attr(
(StructPropertyState::Optional, TypeEntryDetails::Map(key_id, value_id)) => {
serde_options.push(quote! { default });

let map_to_use = &type_space.settings.map_type;
let key_ty = type_space
.id_to_entry
.get(key_id)
Expand All @@ -400,8 +401,9 @@ pub(crate) fn generate_serde_attr(
skip_serializing_if = "::serde_json::Map::is_empty"
});
} else {
let is_empty = format!("{}::is_empty", map_to_use);
serde_options.push(quote! {
skip_serializing_if = "::std::collections::HashMap::is_empty"
skip_serializing_if = #is_empty
});
}
DefaultFunction::Default
Expand Down
6 changes: 5 additions & 1 deletion typify-impl/src/type_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,7 @@ impl TypeEntry {
}

TypeEntryDetails::Map(key_id, value_id) => {
let map_to_use = &type_space.settings.map_type;
let key_ty = type_space
.id_to_entry
.get(key_id)
Expand All @@ -1635,7 +1636,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
36 changes: 34 additions & 2 deletions typify-test/build.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -53,6 +53,12 @@ struct WithSet {
set: HashSet<TestStruct>,
}

#[allow(dead_code)]
#[derive(JsonSchema)]
struct WithMap {
map: HashMap<String, String>,
}

struct LoginName;
impl JsonSchema for LoginName {
fn schema_name() -> String {
Expand Down Expand Up @@ -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::<syn::File>(type_space.to_stream()).unwrap());

let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();
out_file.push("codegen_hashmap.rs");
fs::write(out_file, contents).unwrap();

// Generate with a custom map type to validate requirements.
let mut settings = TypeSpaceSettings::default();
settings.with_map_type("CustomMap".to_string());
let mut type_space = TypeSpace::new(&settings);

WithMap::add(&mut type_space);

let contents =
prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream()).unwrap());

let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();
out_file.push("codegen_custommap.rs");
fs::write(out_file, contents).unwrap();
}

trait AddType {
Expand Down
34 changes: 34 additions & 0 deletions typify-test/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,37 @@ fn test_unknown_format() {
pancakes: String::new(),
};
}

mod hashmap {
include!(concat!(env!("OUT_DIR"), "/codegen_hashmap.rs"));

#[test]
fn test_with_map() {
// Validate that a map is currently represented as a HashMap by default.
let _ = WithMap {
map: std::collections::HashMap::new(),
};
}
}

mod custom_map {
#[allow(private_interfaces)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CustomMap<K, V> {
key: K,
value: V,
}

include!(concat!(env!("OUT_DIR"), "/codegen_custommap.rs"));

#[test]
fn test_with_map() {
// Validate that a map is represented as an CustomMap when requested.
let _ = WithMap {
map: CustomMap {
key: String::new(),
value: String::new(),
},
};
}
}
26 changes: 21 additions & 5 deletions typify/tests/schemas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Error>> {
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<dyn Error>> {
let file = File::open(path)?;
let reader = BufReader::new(file);

Expand All @@ -40,7 +56,7 @@ fn validate_schema(path: std::path::PathBuf) -> Result<(), Box<dyn Error>> {
let schema = serde_json::from_value(schema_raw).unwrap();

let mut type_space = TypeSpace::new(
TypeSpaceSettings::default()
typespace
.with_replacement(
"HandGeneratedType",
"String",
Expand Down
Loading
Loading