Skip to content

Commit 6d46d54

Browse files
(MAINT) Restructure dsc-lib-jsonschema
This change refactors the `dsc-lib-jsonschema` library without modifying any behavior. This change: - Splits the functions in the `transforms` module out into submodules and re-exports them from `transforms` - this keeps referencing the functions the way it was before but makes it easier to navigate the files, given their length. - Makes the unit tests for `schema_utility_extensions` mirror the structure from the source code. - Makes the integration tests for `transform` mirror the structure from the source code.
1 parent 192888a commit 6d46d54

File tree

12 files changed

+506
-516
lines changed

12 files changed

+506
-516
lines changed

lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ pub trait SchemaUtilityExtensions {
344344
/// None
345345
/// )
346346
/// ```
347-
fn get_keyword_as_object(&self, key: &str) -> Option<& Map<String, Value>>;
347+
fn get_keyword_as_object(&self, key: &str) -> Option<&Map<String, Value>>;
348348
/// Checks a JSON Schema for a given keyword and mutably borrows the value of that keyword,
349349
/// if it exists, as a [`Map`].
350350
///
@@ -395,7 +395,7 @@ pub trait SchemaUtilityExtensions {
395395
/// None
396396
/// )
397397
/// ```
398-
fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map<String, Value>>;
398+
fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map<String, Value>>;
399399
/// Checks a JSON schema for a given keyword and borrows the value of that keyword, if it
400400
/// exists, as a [`Number`].
401401
///
@@ -537,7 +537,7 @@ pub trait SchemaUtilityExtensions {
537537
/// Checks a JSON Schema for a given keyword and returns the value of that keyword, if it
538538
/// exists, as a [`Schema`].
539539
///
540-
/// If the keyword doesn't exist or isn't a subchema, this function returns [`None`].
540+
/// If the keyword doesn't exist or isn't a subschema, this function returns [`None`].
541541
///
542542
/// # Examples
543543
///
@@ -621,12 +621,12 @@ pub trait SchemaUtilityExtensions {
621621
/// });
622622
///
623623
/// assert_eq!(
624-
/// schema.get_keyword_as_object_mut("not_exist"),
624+
/// schema.get_keyword_as_subschema_mut("not_exist"),
625625
/// None
626626
/// );
627627
///
628628
/// assert_eq!(
629-
/// schema.get_keyword_as_object_mut("items"),
629+
/// schema.get_keyword_as_subschema_mut("items"),
630630
/// None
631631
/// )
632632
/// ```
@@ -800,7 +800,7 @@ pub trait SchemaUtilityExtensions {
800800
/// defs_json.as_object()
801801
/// );
802802
/// ```
803-
fn get_defs(&self) -> Option<& Map<String, Value>>;
803+
fn get_defs(&self) -> Option<&Map<String, Value>>;
804804
/// Retrieves the `$defs` keyword and mutably borrows the object if it exists.
805805
///
806806
/// If the keyword isn't defined or isn't an object, the function returns [`None`].
@@ -828,7 +828,7 @@ pub trait SchemaUtilityExtensions {
828828
/// defs_json.as_object_mut()
829829
/// );
830830
/// ```
831-
fn get_defs_mut(&mut self) -> Option<&mut Map<String, Value>>;
831+
fn get_defs_mut(&mut self) -> Option<&mut Map<String, Value>>;
832832
/// Looks up a reference in the `$defs` keyword by `$id` and returns the subschema entry as a
833833
/// [`Schema`] if it exists.
834834
///
@@ -1107,7 +1107,7 @@ pub trait SchemaUtilityExtensions {
11071107
/// Some(&mut new_definition)
11081108
/// )
11091109
/// ```
1110-
fn insert_defs_subschema(&mut self, definition_key: &str, definition_value: & Map<String, Value>) -> Option< Map<String, Value>>;
1110+
fn insert_defs_subschema(&mut self, definition_key: &str, definition_value: &Map<String, Value>) -> Option< Map<String, Value>>;
11111111
/// Looks up a subschema in the `$defs` keyword by reference and, if it exists, renames the
11121112
/// _key_ for the definition.
11131113
///
@@ -1177,7 +1177,7 @@ pub trait SchemaUtilityExtensions {
11771177
/// properties_json.as_object()
11781178
/// );
11791179
/// ```
1180-
fn get_properties(&self) -> Option<& Map<String, Value>>;
1180+
fn get_properties(&self) -> Option<&Map<String, Value>>;
11811181
/// Retrieves the `properties` keyword and mutably borrows the object if it exists.
11821182
///
11831183
/// If the keyword isn't defined or isn't an object, the function returns [`None`].
@@ -1205,7 +1205,7 @@ pub trait SchemaUtilityExtensions {
12051205
/// properties_json.as_object_mut()
12061206
/// );
12071207
/// ```
1208-
fn get_properties_mut(&mut self) -> Option<&mut Map<String, Value>>;
1208+
fn get_properties_mut(&mut self) -> Option<&mut Map<String, Value>>;
12091209
/// Looks up a property in the `properties` keyword by name and returns the subschema entry as
12101210
/// a [`Schema`] if it exists.
12111211
///
@@ -1292,11 +1292,11 @@ impl SchemaUtilityExtensions for Schema {
12921292
self.get(key)
12931293
.and_then(Value::as_number)
12941294
}
1295-
fn get_keyword_as_object(&self, key: &str) -> Option<& Map<String, Value>> {
1295+
fn get_keyword_as_object(&self, key: &str) -> Option<&Map<String, Value>> {
12961296
self.get(key)
12971297
.and_then(Value::as_object)
12981298
}
1299-
fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map<String, Value>> {
1299+
fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map<String, Value>> {
13001300
self.get_mut(key)
13011301
.and_then(Value::as_object_mut)
13021302
}
@@ -1321,10 +1321,10 @@ impl SchemaUtilityExtensions for Schema {
13211321
self.get(key)
13221322
.and_then(Value::as_u64)
13231323
}
1324-
fn get_defs(&self) -> Option<& Map<String, Value>> {
1324+
fn get_defs(&self) -> Option<&Map<String, Value>> {
13251325
self.get_keyword_as_object("$defs")
13261326
}
1327-
fn get_defs_mut(&mut self) -> Option<&mut Map<String, Value>> {
1327+
fn get_defs_mut(&mut self) -> Option<&mut Map<String, Value>> {
13281328
self.get_keyword_as_object_mut("$defs")
13291329
}
13301330
fn get_defs_subschema_from_id(&self, id: &str) -> Option<&Schema> {
@@ -1468,10 +1468,10 @@ impl SchemaUtilityExtensions for Schema {
14681468
self.insert("$id".to_string(), Value::String(id_uri.to_string()))
14691469
.and(old_id)
14701470
}
1471-
fn get_properties(&self) -> Option<& Map<String, Value>> {
1471+
fn get_properties(&self) -> Option<&Map<String, Value>> {
14721472
self.get_keyword_as_object("properties")
14731473
}
1474-
fn get_properties_mut(&mut self) -> Option<&mut Map<String, Value>> {
1474+
fn get_properties_mut(&mut self) -> Option<&mut Map<String, Value>> {
14751475
self.get_keyword_as_object_mut("properties")
14761476
}
14771477
fn get_property_subschema(&self, property_name: &str) -> Option<&Schema> {

lib/dsc-lib-jsonschema/src/tests/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,4 @@
1313
//! of the modules from the rest of the source tree.
1414
1515
#[cfg(test)] mod schema_utility_extensions;
16-
#[cfg(test)] mod transforms;
1716
#[cfg(test)] mod vscode;

lib/dsc-lib-jsonschema/src/tests/transforms/mod.rs

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
use schemars::Schema;
2+
use serde_json::{Map, Value, json};
3+
4+
use crate::vscode::VSCODE_KEYWORDS;
5+
6+
/// Munges the generated schema for externally tagged enums into an idiomatic object schema.
7+
///
8+
/// Schemars generates the schema for externally tagged enums as a schema with the `oneOf`
9+
/// keyword where every tag is a different item in the array. Each item defines a type with a
10+
/// single property, requires that property, and disallows specifying any other properties.
11+
///
12+
/// This transformer returns the schema as a single object schema with each of the tags defined
13+
/// as properties. It sets both the `minProperties` and `maxProperties` keywords to `1`. This
14+
/// is more idiomatic, shorter to read and parse, easier to reason about, and matches the
15+
/// underlying data semantics more accurately.
16+
///
17+
/// This transformer should _only_ be used on externally tagged enums. You must specify it with the
18+
/// [schemars `transform()` attribute][`transform`].
19+
///
20+
/// # Examples
21+
///
22+
/// The following struct derives [`JsonSchema`] without specifying the [`transform`] attribute
23+
/// with [`idiomaticize_externally_tagged_enum`]:
24+
///
25+
/// ```
26+
/// use pretty_assertions::assert_eq;
27+
/// use serde_json;
28+
/// use schemars::{schema_for, JsonSchema, json_schema};
29+
/// #[derive(JsonSchema)]
30+
/// pub enum ExternallyTaggedEnum {
31+
/// Name(String),
32+
/// Count(f32),
33+
/// }
34+
///
35+
/// let generated_schema = schema_for!(ExternallyTaggedEnum);
36+
/// let expected_schema = json_schema!({
37+
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
38+
/// "title": "ExternallyTaggedEnum",
39+
/// "oneOf": [
40+
/// {
41+
/// "type": "object",
42+
/// "properties": {
43+
/// "Name": {
44+
/// "type": "string"
45+
/// }
46+
/// },
47+
/// "additionalProperties": false,
48+
/// "required": ["Name"]
49+
/// },
50+
/// {
51+
/// "type": "object",
52+
/// "properties": {
53+
/// "Count": {
54+
/// "type": "number",
55+
/// "format": "float"
56+
/// }
57+
/// },
58+
/// "additionalProperties": false,
59+
/// "required": ["Count"]
60+
/// }
61+
/// ]
62+
/// });
63+
/// assert_eq!(generated_schema, expected_schema);
64+
/// ```
65+
///
66+
/// While the derived schema _does_ effectively validate the enum, it's difficult to understand
67+
/// without deep familiarity with JSON Schema. Compare it to the same enum with the
68+
/// [`idiomaticize_externally_tagged_enum`] transform applied:
69+
///
70+
/// ```
71+
/// use pretty_assertions::assert_eq;
72+
/// use serde_json;
73+
/// use schemars::{schema_for, JsonSchema, json_schema};
74+
/// use dsc_lib_jsonschema::transforms::idiomaticize_externally_tagged_enum;
75+
///
76+
/// #[derive(JsonSchema)]
77+
/// #[schemars(transform = idiomaticize_externally_tagged_enum)]
78+
/// pub enum ExternallyTaggedEnum {
79+
/// Name(String),
80+
/// Count(f32),
81+
/// }
82+
///
83+
/// let generated_schema = schema_for!(ExternallyTaggedEnum);
84+
/// let expected_schema = json_schema!({
85+
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
86+
/// "title": "ExternallyTaggedEnum",
87+
/// "type": "object",
88+
/// "properties": {
89+
/// "Name": {
90+
/// "type": "string"
91+
/// },
92+
/// "Count": {
93+
/// "type": "number",
94+
/// "format": "float"
95+
/// }
96+
/// },
97+
/// "minProperties": 1,
98+
/// "maxProperties": 1,
99+
/// "additionalProperties": false
100+
/// });
101+
/// assert_eq!(generated_schema, expected_schema);
102+
/// ```
103+
///
104+
/// The transformed schema is shorter, clearer, and idiomatic for JSON Schema draft 2019-09 and
105+
/// later. It validates values as effectively as the default output for an externally tagged
106+
/// enum, but is easier for your users and integrating developers to understand and work
107+
/// with.
108+
///
109+
/// # Panics
110+
///
111+
/// This transform panics when called against a generated schema that doesn't define the `oneOf`
112+
/// keyword. Schemars uses the `oneOf` keyword when generating subschemas for externally tagged
113+
/// enums. This transform panics on an invalid application of the transform to prevent unexpected
114+
/// behavior for the schema transformation. This ensures invalid applications are caught during
115+
/// development and CI instead of shipping broken schemas.
116+
///
117+
/// [`JsonSchema`]: schemars::JsonSchema
118+
/// [`transform`]: derive@schemars::JsonSchema
119+
pub fn idiomaticize_externally_tagged_enum(schema: &mut Schema) {
120+
// First, retrieve the oneOf keyword entries. If this transformer was called against an invalid
121+
// schema or subschema, it should fail fast.
122+
let one_ofs = schema.get("oneOf")
123+
.unwrap_or_else(|| panic_t!(
124+
"transforms.idiomaticize_externally_tagged_enum.applies_to",
125+
transforming_schema = serde_json::to_string_pretty(schema).unwrap()
126+
))
127+
.as_array()
128+
.unwrap_or_else(|| panic_t!(
129+
"transforms.idiomaticize_externally_tagged_enum.oneOf_array",
130+
transforming_schema = serde_json::to_string_pretty(schema).unwrap()
131+
));
132+
// Initialize the map of properties to fill in when introspecting on the items in the oneOf array.
133+
let mut properties_map = Map::new();
134+
135+
for item in one_ofs {
136+
let item_data: Map<String, Value> = item.as_object()
137+
.unwrap_or_else(|| panic_t!(
138+
"transforms.idiomaticize_externally_tagged_enum.oneOf_item_as_object",
139+
transforming_schema = serde_json::to_string_pretty(schema).unwrap(),
140+
invalid_item = serde_json::to_string_pretty(item).unwrap()
141+
))
142+
.clone();
143+
// If we're accidentally operating on an invalid schema, short-circuit.
144+
let item_data_type = item_data.get("type")
145+
.unwrap_or_else(|| panic_t!(
146+
"transforms.idiomaticize_externally_tagged_enum.oneOf_item_define_type",
147+
transforming_schema = serde_json::to_string_pretty(schema).unwrap(),
148+
invalid_item = serde_json::to_string_pretty(&item_data).unwrap()
149+
))
150+
.as_str()
151+
.unwrap_or_else(|| panic_t!(
152+
"transforms.idiomaticize_externally_tagged_enum.oneOf_item_type_string",
153+
transforming_schema = serde_json::to_string_pretty(schema).unwrap(),
154+
invalid_item = serde_json::to_string_pretty(&item_data).unwrap()
155+
));
156+
assert_t!(
157+
!item_data_type.ne("object"),
158+
"transforms.idiomaticize_externally_tagged_enum.oneOf_item_not_object_type",
159+
transforming_schema = serde_json::to_string_pretty(schema).unwrap(),
160+
invalid_item = serde_json::to_string_pretty(&item_data).unwrap(),
161+
invalid_type = item_data_type
162+
);
163+
// Retrieve the title and description from the top-level of the item, if any. Depending on
164+
// the implementation, these values might be set on the item, in the property, or both.
165+
let item_title = item_data.get("title");
166+
let item_desc = item_data.get("description");
167+
// Retrieve the property definitions. There should never be more than one property per item,
168+
// but this implementation doesn't guard against that edge case..
169+
let properties_data = item_data.get("properties")
170+
.unwrap_or_else(|| panic_t!(
171+
"transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_missing",
172+
transforming_schema = serde_json::to_string_pretty(schema).unwrap(),
173+
invalid_item = serde_json::to_string_pretty(&item_data).unwrap(),
174+
))
175+
.as_object()
176+
.unwrap_or_else(|| panic_t!(
177+
"transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_not_object",
178+
transforming_schema = serde_json::to_string_pretty(schema).unwrap(),
179+
invalid_item = serde_json::to_string_pretty(&item_data).unwrap(),
180+
))
181+
.clone();
182+
for property_name in properties_data.keys() {
183+
// Retrieve the property definition to munge as needed.
184+
let mut property_data = properties_data.get(property_name)
185+
.unwrap() // can't fail because we're iterating on keys in the map
186+
.as_object()
187+
.unwrap_or_else(|| panic_t!(
188+
"transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_entry_not_object",
189+
transforming_schema = serde_json::to_string_pretty(schema).unwrap(),
190+
invalid_item = serde_json::to_string_pretty(&item_data).unwrap(),
191+
name = property_name
192+
))
193+
.clone();
194+
// Process the annotation keywords. If they are defined on the item but not the property,
195+
// insert the item-defined keywords into the property data.
196+
if let Some(t) = item_title && property_data.get("title").is_none() {
197+
property_data.insert("title".into(), t.clone());
198+
}
199+
if let Some(d) = item_desc && property_data.get("description").is_none() {
200+
property_data.insert("description".into(), d.clone());
201+
}
202+
for keyword in VSCODE_KEYWORDS {
203+
if let Some(keyword_value) = item_data.get(keyword) && property_data.get(keyword).is_none() {
204+
property_data.insert(keyword.to_string(), keyword_value.clone());
205+
}
206+
}
207+
// Insert the processed property into the top-level properties definition.
208+
properties_map.insert(property_name.into(), serde_json::Value::Object(property_data));
209+
}
210+
}
211+
// Replace the oneOf array with an idiomatic object schema definition
212+
schema.remove("oneOf");
213+
schema.insert("type".to_string(), json!("object"));
214+
schema.insert("minProperties".to_string(), json!(1));
215+
schema.insert("maxProperties".to_string(), json!(1));
216+
schema.insert("additionalProperties".to_string(), json!(false));
217+
schema.insert("properties".to_string(), properties_map.into());
218+
}

0 commit comments

Comments
 (0)