Skip to content

Commit dc8bb88

Browse files
authored
Handle arbitrary containment cycles, breaking them with a Box type (#300)
1 parent 12e9b04 commit dc8bb88

File tree

4 files changed

+264
-195
lines changed

4 files changed

+264
-195
lines changed

typify-impl/src/convert.rs

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1675,58 +1675,6 @@ mod tests {
16751675
assert_eq!(type_space.iter_types().count(), 4);
16761676
}
16771677

1678-
#[test]
1679-
fn test_trivial_cycle() {
1680-
#[derive(JsonSchema, Schema)]
1681-
#[allow(dead_code)]
1682-
struct A {
1683-
a: Box<A>,
1684-
}
1685-
1686-
validate_output::<A>();
1687-
}
1688-
1689-
#[test]
1690-
fn test_optional_trivial_cycle() {
1691-
#[derive(JsonSchema, Schema)]
1692-
#[allow(dead_code)]
1693-
struct A {
1694-
a: Option<Box<A>>,
1695-
}
1696-
1697-
validate_output::<A>();
1698-
}
1699-
1700-
#[test]
1701-
fn test_enum_trivial_cycles() {
1702-
#[derive(JsonSchema, Schema)]
1703-
#[allow(dead_code)]
1704-
enum A {
1705-
Variant0(u64),
1706-
Variant1 {
1707-
a: u64,
1708-
b: Vec<A>,
1709-
rop: Option<Box<A>>,
1710-
},
1711-
Variant2 {
1712-
a: Box<A>,
1713-
},
1714-
Variant3(u64, Box<A>),
1715-
Variant4(Option<Box<A>>, String),
1716-
}
1717-
1718-
validate_output::<A>();
1719-
}
1720-
1721-
#[test]
1722-
fn test_newtype_trivial_cycle() {
1723-
#[derive(JsonSchema, Schema)]
1724-
#[allow(dead_code)]
1725-
struct A(Box<A>);
1726-
1727-
validate_output::<A>();
1728-
}
1729-
17301678
#[test]
17311679
fn test_basic_option_flat() {
17321680
#[derive(JsonSchema, Schema)]

typify-impl/src/cycles.rs

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Copyright 2023 Oxide Computer Company
2+
3+
use std::{
4+
collections::{BTreeMap, BTreeSet},
5+
ops::Range,
6+
};
7+
8+
use crate::{
9+
type_entry::{
10+
TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct,
11+
VariantDetails,
12+
},
13+
TypeId, TypeSpace,
14+
};
15+
16+
impl TypeSpace {
17+
/// We need to root out any containment cycles, breaking them by inserting
18+
/// a `Box` type. Our choice of *where* to break cycles is more arbitrary
19+
/// than optimal, but is well beyond sufficient.
20+
pub fn break_cycles(&mut self, range: Range<u64>) {
21+
enum Node {
22+
Start {
23+
type_id: TypeId,
24+
},
25+
Processing {
26+
type_id: TypeId,
27+
children_ids: Vec<TypeId>,
28+
},
29+
}
30+
31+
let mut visited = BTreeSet::<TypeId>::new();
32+
33+
for id in range {
34+
let type_id = TypeId(id);
35+
36+
// This isn't strictly necessary, but we'll short-circuit some work
37+
// by checking this right away.
38+
if visited.contains(&type_id) {
39+
continue;
40+
}
41+
42+
let mut active = BTreeSet::<TypeId>::new();
43+
let mut stack = Vec::<Node>::new();
44+
45+
active.insert(type_id.clone());
46+
stack.push(Node::Start { type_id });
47+
48+
while let Some(top) = stack.last_mut() {
49+
match top {
50+
// Skip right to the end since we've already seen this type.
51+
Node::Start { type_id } if visited.contains(type_id) => {
52+
assert!(active.contains(type_id));
53+
54+
let type_id = type_id.clone();
55+
*top = Node::Processing {
56+
type_id,
57+
children_ids: Vec::new(),
58+
};
59+
}
60+
61+
// Break any immediate cycles and queue up this type for
62+
// descent into its child types.
63+
Node::Start { type_id } => {
64+
assert!(active.contains(type_id));
65+
66+
visited.insert(type_id.clone());
67+
68+
// Determine which child types form cycles--and
69+
// therefore need to be snipped--and the rest--into
70+
// which we should descend. We make this its own block
71+
// to clarify the lifetime of the exclusive reference
72+
// to the type. We don't really *need* to have an
73+
// exclusive reference here, but there's no point in
74+
// writing `get_child_ids` again for shared references.
75+
let (snip, descend) = {
76+
let type_entry = self.id_to_entry.get_mut(type_id).unwrap();
77+
78+
let child_ids = get_child_ids(type_entry)
79+
.into_iter()
80+
.map(|child_id| child_id.clone());
81+
82+
// If the child type is in active then we've found
83+
// a cycle (otherwise we'll descend).
84+
child_ids.partition::<Vec<_>, _>(|child_id| active.contains(child_id))
85+
};
86+
87+
// Note that while `snip` might contain duplicates,
88+
// `id_to_box` is idempotent insofar as the same input
89+
// TypeId will result in the same output TypeId. Ergo
90+
// the resulting pairs from which we construct the
91+
// mapping would contain exact duplicates; it would not
92+
// contain two values associated with the same key.
93+
let replace = snip
94+
.into_iter()
95+
.map(|type_id| {
96+
let box_id = self.id_to_box(&type_id);
97+
98+
(type_id, box_id)
99+
})
100+
.collect::<BTreeMap<_, _>>();
101+
102+
// Break any cycles by reassigning the child type to a box.
103+
let type_entry = self.id_to_entry.get_mut(type_id).unwrap();
104+
let child_ids = get_child_ids(type_entry);
105+
for child_id in child_ids {
106+
if let Some(replace_id) = replace.get(child_id) {
107+
*child_id = replace_id.clone();
108+
}
109+
}
110+
111+
// Descend into child types.
112+
let node = Node::Processing {
113+
type_id: type_id.clone(),
114+
children_ids: descend,
115+
};
116+
*top = node;
117+
}
118+
119+
// If there are children left, push the next child onto the
120+
// stack. If there are none left, pop this type.
121+
Node::Processing {
122+
type_id,
123+
children_ids,
124+
} => {
125+
if let Some(type_id) = children_ids.pop() {
126+
// Descend into the next child node.
127+
active.insert(type_id.clone());
128+
stack.push(Node::Start { type_id });
129+
} else {
130+
// All done; remove the item from the active list
131+
// and stack.
132+
active.remove(type_id);
133+
let _ = stack.pop();
134+
}
135+
}
136+
}
137+
}
138+
}
139+
}
140+
}
141+
142+
/// For types that could potentially participate in a cycle, return a list of
143+
/// mutable references to the child types.
144+
fn get_child_ids(type_entry: &mut TypeEntry) -> Vec<&mut TypeId> {
145+
match &mut type_entry.details {
146+
TypeEntryDetails::Enum(TypeEntryEnum { variants, .. }) => variants
147+
.iter_mut()
148+
.flat_map(|variant| match &mut variant.details {
149+
VariantDetails::Simple => Vec::new(),
150+
VariantDetails::Item(type_id) => vec![type_id],
151+
VariantDetails::Tuple(type_ids) => type_ids.iter_mut().collect(),
152+
VariantDetails::Struct(properties) => properties
153+
.iter_mut()
154+
.map(|prop| &mut prop.type_id)
155+
.collect(),
156+
})
157+
.collect::<Vec<_>>(),
158+
159+
TypeEntryDetails::Struct(TypeEntryStruct { properties, .. }) => properties
160+
.iter_mut()
161+
.map(|prop| &mut prop.type_id)
162+
.collect(),
163+
164+
TypeEntryDetails::Newtype(TypeEntryNewtype { type_id, .. }) => {
165+
vec![type_id]
166+
}
167+
168+
// Unnamed types that can participate in containment cycles.
169+
TypeEntryDetails::Option(type_id) => vec![type_id],
170+
TypeEntryDetails::Array(type_id, _) => vec![type_id],
171+
TypeEntryDetails::Tuple(type_ids) => type_ids.iter_mut().collect(),
172+
173+
_ => Vec::new(),
174+
}
175+
}
176+
177+
#[cfg(test)]
178+
mod tests {
179+
use schema::Schema;
180+
use schemars::JsonSchema;
181+
182+
use crate::test_util::validate_output;
183+
184+
#[test]
185+
fn test_trivial_cycle() {
186+
#[derive(JsonSchema, Schema)]
187+
#[allow(dead_code)]
188+
struct A {
189+
a: Box<A>,
190+
}
191+
192+
validate_output::<A>();
193+
}
194+
195+
#[test]
196+
fn test_optional_trivial_cycle() {
197+
#[derive(JsonSchema, Schema)]
198+
#[allow(dead_code)]
199+
struct A {
200+
a: Option<Box<A>>,
201+
}
202+
203+
validate_output::<A>();
204+
}
205+
206+
#[test]
207+
fn test_enum_trivial_cycles() {
208+
#[derive(JsonSchema, Schema)]
209+
#[allow(dead_code)]
210+
enum A {
211+
Variant0(u64),
212+
Variant1 {
213+
a: u64,
214+
b: Vec<A>,
215+
rop: Option<Box<A>>,
216+
},
217+
Variant2 {
218+
a: Box<A>,
219+
},
220+
Variant3(u64, Box<A>),
221+
Variant4(Option<Box<A>>, String),
222+
}
223+
224+
validate_output::<A>();
225+
}
226+
227+
#[test]
228+
fn test_newtype_trivial_cycle() {
229+
#[derive(JsonSchema, Schema)]
230+
#[allow(dead_code)]
231+
struct A(Box<A>);
232+
233+
validate_output::<A>();
234+
}
235+
236+
#[test]
237+
fn test_abab_cycle() {
238+
#[derive(JsonSchema, Schema)]
239+
#[allow(dead_code)]
240+
struct A(B);
241+
242+
#[derive(JsonSchema, Schema)]
243+
#[allow(dead_code)]
244+
struct B(Box<A>);
245+
246+
validate_output::<A>();
247+
}
248+
}

0 commit comments

Comments
 (0)