Skip to content
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
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 {
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);
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
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