Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions Cargo.lock

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

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

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
10 changes: 7 additions & 3 deletions 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 Expand Up @@ -458,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,

Expand All @@ -471,7 +473,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
10 changes: 7 additions & 3 deletions typify-impl/src/type_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ impl TypeEntry {
TypeEntryDetails::Unit
| TypeEntryDetails::Option(_)
| TypeEntryDetails::Vec(_)
| TypeEntryDetails::Map(_, _)
| TypeEntryDetails::Map { .. }
| TypeEntryDetails::Set(_) => {
matches!(impl_name, TypeSpaceImpl::Default)
}
Expand Down 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 Expand Up @@ -1746,7 +1750,7 @@ impl TypeEntry {
| TypeEntryDetails::Struct(_)
| TypeEntryDetails::Newtype(_)
| TypeEntryDetails::Vec(_)
| TypeEntryDetails::Map(..)
| TypeEntryDetails::Map { .. }
| TypeEntryDetails::Set(_)
| TypeEntryDetails::Box(_)
| TypeEntryDetails::Native(_)
Expand Down
4 changes: 2 additions & 2 deletions typify-impl/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ 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 +424,7 @@ fn value_for_struct_props(
match &type_entry.details {
TypeEntryDetails::Struct(_)
| TypeEntryDetails::Option(_)
| TypeEntryDetails::Map(..) => (),
| TypeEntryDetails::Map { .. } => (),
_ => unreachable!(),
}

Expand Down
2 changes: 1 addition & 1 deletion typify-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
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