Skip to content

Commit 51c60fa

Browse files
committed
test: add runtime serde integration tests for all fork fixes
18 new runtime tests verifying actual serde behavior, not just codegen: - PR oxidecomputer#991: IntOrStr deserializes 42 as Integer (not swallowed by Number) - PR oxidecomputer#918: RequiredWithDefaults::default() has correct values; deserializing {} uses schema defaults - PR oxidecomputer#986: Dscp TryFrom rejects 64, accepts 0-63; serde rejects out-of-range JSON values - PR oxidecomputer#948: All 9 comparator symbols round-trip through serde correctly - PR oxidecomputer#414: anyOf with object+string+integer deserializes without panic - PR oxidecomputer#954: not:{type:"object"} accepts primitives; array items work Also fixes pre-existing CustomMap test compilation (missing Default and is_empty) and adds serde derive feature to typify-test.
1 parent 28b292a commit 51c60fa

File tree

3 files changed

+296
-2
lines changed

3 files changed

+296
-2
lines changed

typify-test/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ edition = "2021"
55

66
[dependencies]
77
regress = { workspace = true }
8-
serde = { workspace = true }
8+
serde = { workspace = true, features = ["derive"] }
99
serde_json = { workspace = true }
1010

1111
[build-dependencies]
@@ -15,4 +15,5 @@ ipnetwork = { workspace = true }
1515
prettyplease = { workspace = true }
1616
schemars = { workspace = true }
1717
serde = { workspace = true }
18+
serde_json = { workspace = true }
1819
syn = { workspace = true }

typify-test/build.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,118 @@ struct UnknownFormat {
125125
pancakes: Pancakes,
126126
}
127127

128+
fn generate_from_json_schema(json: &str, output_name: &str) {
129+
let root_schema: schemars::schema::RootSchema = serde_json::from_str(json).unwrap();
130+
let mut type_space = TypeSpace::new(TypeSpaceSettings::default().with_struct_builder(true));
131+
type_space.add_root_schema(root_schema).unwrap();
132+
let contents =
133+
prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream()).unwrap());
134+
let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();
135+
out_file.push(output_name);
136+
fs::write(out_file, contents).unwrap();
137+
}
138+
128139
fn main() {
140+
// Generate types for runtime serde integration tests.
141+
// Using inline JSON schemas to avoid dependency issues with external crates.
142+
143+
// PR #991: Integer before Number in untagged enums
144+
generate_from_json_schema(
145+
r#"{
146+
"definitions": {
147+
"IntOrStr": {
148+
"type": ["integer", "string"]
149+
}
150+
}
151+
}"#,
152+
"codegen_int_or_str.rs",
153+
);
154+
155+
// PR #918: Required fields with defaults
156+
generate_from_json_schema(
157+
r#"{
158+
"definitions": {
159+
"RequiredWithDefaults": {
160+
"type": "object",
161+
"required": ["name", "count"],
162+
"properties": {
163+
"name": { "type": "string", "default": "unnamed" },
164+
"count": { "type": "integer", "default": 0 },
165+
"label": { "type": "string" }
166+
}
167+
}
168+
}
169+
}"#,
170+
"codegen_required_defaults.rs",
171+
);
172+
173+
// PR #986: Bounded integer newtypes
174+
generate_from_json_schema(
175+
r#"{
176+
"definitions": {
177+
"Dscp": {
178+
"type": "integer",
179+
"format": "uint8",
180+
"minimum": 0,
181+
"maximum": 63
182+
}
183+
}
184+
}"#,
185+
"codegen_dscp.rs",
186+
);
187+
188+
// PR #948: Special char variant names
189+
generate_from_json_schema(
190+
r#"{
191+
"definitions": {
192+
"Comparator": {
193+
"anyOf": [
194+
{ "type": "string", "const": "=" },
195+
{ "type": "string", "const": ">" },
196+
{ "type": "string", "const": "<" },
197+
{ "type": "string", "const": "\u2265" },
198+
{ "type": "string", "const": ">=" },
199+
{ "type": "string", "const": "\u2264" },
200+
{ "type": "string", "const": "<=" },
201+
{ "type": "string", "const": "\u2260" },
202+
{ "type": "string", "const": "!=" }
203+
]
204+
}
205+
}
206+
}"#,
207+
"codegen_comparator.rs",
208+
);
209+
210+
// PR #414: anyOf with mixed types (would have panicked before)
211+
generate_from_json_schema(
212+
r#"{
213+
"definitions": {
214+
"AnyOfMixed": {
215+
"anyOf": [
216+
{ "type": "object", "properties": { "value": { "type": "string" } }, "required": ["value"] },
217+
{ "type": "string" },
218+
{ "type": "integer" }
219+
]
220+
}
221+
}
222+
}"#,
223+
"codegen_any_of_mixed.rs",
224+
);
225+
226+
// PR #954: not schema (would have panicked before)
227+
generate_from_json_schema(
228+
r#"{
229+
"definitions": {
230+
"NotObject": { "not": { "type": "object" } },
231+
"ArrayNonObjects": {
232+
"type": "array",
233+
"items": { "not": { "type": "object" } }
234+
}
235+
}
236+
}"#,
237+
"codegen_not_types.rs",
238+
);
239+
129240
let mut type_space = TypeSpace::default();
130241

131242
WithSet::add(&mut type_space);

typify-test/src/main.rs

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,18 @@ mod custom_map {
7474
#![allow(dead_code)]
7575

7676
#[allow(private_interfaces)]
77-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
77+
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
7878
pub struct CustomMap<K, V> {
7979
key: K,
8080
value: V,
8181
}
8282

83+
impl<K, V> CustomMap<K, V> {
84+
fn is_empty(&self) -> bool {
85+
false
86+
}
87+
}
88+
8389
include!(concat!(env!("OUT_DIR"), "/codegen_custommap.rs"));
8490

8591
#[test]
@@ -93,3 +99,179 @@ mod custom_map {
9399
};
94100
}
95101
}
102+
103+
// ========================================================================
104+
// Runtime serde integration tests for our fork fixes
105+
// ========================================================================
106+
107+
mod int_or_str {
108+
#![allow(dead_code)]
109+
include!(concat!(env!("OUT_DIR"), "/codegen_int_or_str.rs"));
110+
}
111+
112+
mod required_defaults {
113+
#![allow(dead_code)]
114+
include!(concat!(env!("OUT_DIR"), "/codegen_required_defaults.rs"));
115+
}
116+
117+
mod dscp {
118+
#![allow(dead_code)]
119+
include!(concat!(env!("OUT_DIR"), "/codegen_dscp.rs"));
120+
}
121+
122+
mod comparator {
123+
#![allow(dead_code)]
124+
include!(concat!(env!("OUT_DIR"), "/codegen_comparator.rs"));
125+
}
126+
127+
mod any_of_mixed {
128+
#![allow(dead_code)]
129+
include!(concat!(env!("OUT_DIR"), "/codegen_any_of_mixed.rs"));
130+
}
131+
132+
mod not_types {
133+
#![allow(dead_code)]
134+
include!(concat!(env!("OUT_DIR"), "/codegen_not_types.rs"));
135+
}
136+
137+
// --- PR #991: Integer before Number in untagged enums ---
138+
139+
#[test]
140+
fn test_int_or_str_integer_deserialization() {
141+
let v: int_or_str::IntOrStr = serde_json::from_str("42").unwrap();
142+
assert!(matches!(v, int_or_str::IntOrStr::Integer(42)));
143+
}
144+
145+
#[test]
146+
fn test_int_or_str_string_deserialization() {
147+
let v: int_or_str::IntOrStr = serde_json::from_str(r#""hello""#).unwrap();
148+
assert!(matches!(v, int_or_str::IntOrStr::String(_)));
149+
}
150+
151+
#[test]
152+
fn test_int_or_str_roundtrip() {
153+
let original = int_or_str::IntOrStr::Integer(99);
154+
let json = serde_json::to_string(&original).unwrap();
155+
let back: int_or_str::IntOrStr = serde_json::from_str(&json).unwrap();
156+
assert!(matches!(back, int_or_str::IntOrStr::Integer(99)));
157+
}
158+
159+
// --- PR #918: Default impl for required fields with defaults ---
160+
161+
#[test]
162+
fn test_required_with_defaults_default_impl() {
163+
let d = required_defaults::RequiredWithDefaults::default();
164+
assert_eq!(d.name, "unnamed");
165+
assert_eq!(d.count, 0);
166+
assert!(d.label.is_none());
167+
}
168+
169+
#[test]
170+
fn test_required_with_defaults_deserialize_empty() {
171+
let v: required_defaults::RequiredWithDefaults = serde_json::from_str("{}").unwrap();
172+
assert_eq!(v.name, "unnamed");
173+
assert_eq!(v.count, 0);
174+
}
175+
176+
#[test]
177+
fn test_required_with_defaults_deserialize_partial() {
178+
let v: required_defaults::RequiredWithDefaults =
179+
serde_json::from_str(r#"{"name": "foo"}"#).unwrap();
180+
assert_eq!(v.name, "foo");
181+
assert_eq!(v.count, 0);
182+
}
183+
184+
// --- PR #986: TryFrom for bounded integers ---
185+
186+
#[test]
187+
fn test_dscp_try_from_valid() {
188+
assert!(dscp::Dscp::try_from(42u8).is_ok());
189+
assert_eq!(*dscp::Dscp::try_from(42u8).unwrap(), 42);
190+
}
191+
192+
#[test]
193+
fn test_dscp_try_from_boundary() {
194+
assert!(dscp::Dscp::try_from(0u8).is_ok());
195+
assert!(dscp::Dscp::try_from(63u8).is_ok());
196+
assert!(dscp::Dscp::try_from(64u8).is_err());
197+
assert!(dscp::Dscp::try_from(255u8).is_err());
198+
}
199+
200+
#[test]
201+
fn test_dscp_deserialize_valid() {
202+
let d: dscp::Dscp = serde_json::from_str("42").unwrap();
203+
assert_eq!(*d, 42);
204+
}
205+
206+
#[test]
207+
fn test_dscp_deserialize_out_of_range() {
208+
assert!(serde_json::from_str::<dscp::Dscp>("64").is_err());
209+
assert!(serde_json::from_str::<dscp::Dscp>("255").is_err());
210+
}
211+
212+
// --- PR #948: Special char variant names ---
213+
214+
#[test]
215+
fn test_comparator_deserialize() {
216+
let v: comparator::Comparator = serde_json::from_str(r#""=""#).unwrap();
217+
assert!(matches!(v, comparator::Comparator::Eq));
218+
219+
let v: comparator::Comparator = serde_json::from_str(r#"">=""#).unwrap();
220+
assert!(matches!(v, comparator::Comparator::GtEq));
221+
222+
let v: comparator::Comparator = serde_json::from_str("\"\"").unwrap();
223+
assert!(matches!(v, comparator::Comparator::Gte));
224+
225+
let v: comparator::Comparator = serde_json::from_str("\"\"").unwrap();
226+
assert!(matches!(v, comparator::Comparator::Neq));
227+
228+
let v: comparator::Comparator = serde_json::from_str(r#""!=""#).unwrap();
229+
assert!(matches!(v, comparator::Comparator::BangEq));
230+
}
231+
232+
#[test]
233+
fn test_comparator_roundtrip() {
234+
for json in [
235+
r#""=""#, r#"">""#, r#""<""#, "\"\"", r#"">=""#, "\"\"", r#""<=""#, "\"\"", r#""!=""#,
236+
] {
237+
let v: comparator::Comparator = serde_json::from_str(json).unwrap();
238+
let back = serde_json::to_string(&v).unwrap();
239+
assert_eq!(json, back);
240+
}
241+
}
242+
243+
// --- PR #414: anyOf overhaul (no more panic on primitives) ---
244+
245+
#[test]
246+
fn test_any_of_mixed_object() {
247+
let v: any_of_mixed::AnyOfMixed = serde_json::from_str(r#"{"value": "test"}"#).unwrap();
248+
assert!(matches!(v, any_of_mixed::AnyOfMixed::Object { .. }));
249+
}
250+
251+
#[test]
252+
fn test_any_of_mixed_string() {
253+
let v: any_of_mixed::AnyOfMixed = serde_json::from_str(r#""hello""#).unwrap();
254+
assert!(matches!(v, any_of_mixed::AnyOfMixed::String(_)));
255+
}
256+
257+
#[test]
258+
fn test_any_of_mixed_integer() {
259+
let v: any_of_mixed::AnyOfMixed = serde_json::from_str("42").unwrap();
260+
assert!(matches!(v, any_of_mixed::AnyOfMixed::Integer(42)));
261+
}
262+
263+
// --- PR #954: not schema types don't panic ---
264+
265+
#[test]
266+
fn test_not_object_accepts_primitives() {
267+
// not: { type: "object" } falls back to serde_json::Value
268+
let _: not_types::NotObject = serde_json::from_str("42").unwrap();
269+
let _: not_types::NotObject = serde_json::from_str(r#""hello""#).unwrap();
270+
let _: not_types::NotObject = serde_json::from_str("true").unwrap();
271+
}
272+
273+
#[test]
274+
fn test_array_non_objects() {
275+
let v: not_types::ArrayNonObjects = serde_json::from_str(r#"[1, "two", true]"#).unwrap();
276+
assert_eq!(v.len(), 3);
277+
}

0 commit comments

Comments
 (0)