jtd_derive/gen.rs
1//! Schema generator and its settings.
2
3mod naming_strategy;
4
5use std::collections::{BTreeMap, HashMap, HashSet};
6use std::fmt::Debug;
7
8use self::naming_strategy::NamingStrategy;
9use crate::schema::{RootSchema, Schema, SchemaType};
10use crate::type_id::{type_id, TypeId};
11use crate::{JsonTypedef, Names};
12
13/// A configurable schema generator. An instance is meant to produce one
14/// [`RootSchema`] and be consumed in the process.
15///
16/// If you want to just use the sane defaults, try [`Generator::default()`].
17///
18/// Otherwise, you can configure schema generation using the builder.
19///
20/// # Examples
21///
22/// Using the default settings:
23///
24/// ```
25/// use jtd_derive::{JsonTypedef, Generator};
26///
27/// #[derive(JsonTypedef)]
28/// struct Foo {
29/// x: u32,
30/// }
31///
32/// let root_schema = Generator::default().into_root_schema::<Foo>().unwrap();
33/// let json_schema = serde_json::to_value(&root_schema).unwrap();
34///
35/// assert_eq!(json_schema, serde_json::json!{ {
36/// "properties": {
37/// "x": { "type": "uint32" }
38/// },
39/// "additionalProperties": true,
40/// } });
41/// ```
42///
43/// Using custom settings:
44///
45/// ```
46/// use jtd_derive::{JsonTypedef, Generator};
47///
48/// #[derive(JsonTypedef)]
49/// struct Foo {
50/// x: u32,
51/// }
52///
53/// let root_schema = Generator::builder()
54/// .top_level_ref()
55/// .naming_short()
56/// .build()
57/// .into_root_schema::<Foo>()
58/// .unwrap();
59/// let json_schema = serde_json::to_value(&root_schema).unwrap();
60///
61/// assert_eq!(json_schema, serde_json::json!{ {
62/// "definitions": {
63/// "Foo": {
64/// "properties": {
65/// "x": { "type": "uint32" }
66/// },
67/// "additionalProperties": true,
68/// }
69/// },
70/// "ref": "Foo",
71/// } });
72/// ```
73#[derive(Default, Debug)]
74pub struct Generator {
75 naming_strategy: NamingStrategy,
76 /// Types for which at least one ref was created during schema gen.
77 /// By keeping track of these, we can clean up unused definitions at the end.
78 refs: HashSet<TypeId>,
79 definitions: HashMap<TypeId, (Names, DefinitionState)>,
80 inlining: Inlining,
81}
82
83impl Generator {
84 /// Provide a `Generator` builder, allowing for some customization.
85 pub fn builder() -> GeneratorBuilder {
86 GeneratorBuilder::default()
87 }
88
89 /// Generate the root schema for the given type according to the settings.
90 /// This consumes the generator.
91 ///
92 /// This will return an error if a naming collision is detected, i.e. two
93 /// distinct Rust types produce the same identifier.
94 pub fn into_root_schema<T: JsonTypedef>(mut self) -> Result<RootSchema, GenError> {
95 let schema = self.sub_schema_impl::<T>(true);
96 self.clean_up_defs();
97
98 fn process_defs(
99 defs: HashMap<TypeId, (Names, DefinitionState)>,
100 ns: &mut NamingStrategy,
101 ) -> Result<BTreeMap<String, Schema>, GenError> {
102 // This could probably be optimized somehow.
103
104 let defs = defs
105 .into_iter()
106 .map(|(_, (n, s))| (ns.fun()(&n), (n, s.unwrap())));
107
108 let mut map = HashMap::new();
109
110 for (key, (names, schema)) in defs {
111 if let Some((other_names, _)) = map.get(&key) {
112 return Err(GenError::NameCollision {
113 id: key,
114 type1: NamingStrategy::long().fun()(other_names),
115 type2: NamingStrategy::long().fun()(&names),
116 });
117 } else {
118 map.insert(key, (names, schema));
119 }
120 }
121
122 Ok(map
123 .into_iter()
124 .map(|(key, (_, schema))| (key, schema))
125 .collect())
126 }
127
128 Ok(RootSchema {
129 definitions: process_defs(self.definitions, &mut self.naming_strategy)?,
130 schema,
131 })
132 }
133
134 /// Generate a [`Schema`] for a given type, adding definitions to the
135 /// generator as appropriate.
136 ///
137 /// This is meant to only be called when implementing [`JsonTypedef`] for
138 /// new types. Most commonly you'll derive that trait. It's unlikely you'll
139 /// need to call this method explicitly.
140 pub fn sub_schema<T: JsonTypedef + ?Sized>(&mut self) -> Schema {
141 self.sub_schema_impl::<T>(false)
142 }
143
144 fn sub_schema_impl<T: JsonTypedef + ?Sized>(&mut self, top_level: bool) -> Schema {
145 let id = type_id::<T>();
146 let inlining = match self.inlining {
147 Inlining::Always => true,
148 Inlining::Normal => top_level,
149 Inlining::Never => false,
150 };
151
152 let inlined_schema = match self.definitions.get(&id) {
153 Some((_, DefinitionState::Finished(schema))) => {
154 // we had already built a schema for this type.
155 // no need to do it again.
156
157 (!T::referenceable() || (inlining && !self.refs.contains(&id)))
158 .then_some(schema.clone())
159 }
160 Some((_, DefinitionState::Processing)) => {
161 // we're already in the process of building a schema for this type.
162 // this means it's recursive and the only way to keep things sane
163 // is to go by reference
164
165 None
166 }
167 None => {
168 // no schema available yet, so we have to build it
169 if T::referenceable() {
170 self.definitions
171 .insert(id, (T::names(), DefinitionState::Processing));
172 let schema = T::schema(self);
173 self.definitions
174 .get_mut(&id)
175 .unwrap()
176 .1
177 .finalize(schema.clone());
178
179 (inlining && !self.refs.contains(&id)).then_some(schema)
180 } else {
181 Some(T::schema(self))
182 }
183 }
184 };
185
186 inlined_schema.unwrap_or_else(|| {
187 let schema = Schema {
188 ty: SchemaType::Ref {
189 r#ref: self.naming_strategy.fun()(&T::names()),
190 },
191 ..Schema::default()
192 };
193 self.refs.insert(id);
194 schema
195 })
196 }
197
198 fn clean_up_defs(&mut self) {
199 let to_remove: Vec<_> = self
200 .definitions
201 .keys()
202 .filter(|names| !self.refs.contains(names))
203 .cloned()
204 .collect();
205
206 for names in to_remove {
207 self.definitions.remove(&names);
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy, Default)]
213enum Inlining {
214 Always,
215 #[default]
216 Normal,
217 Never,
218}
219
220/// Builder for [`Generator`]. For example usage, refer to [`Generator`].
221#[derive(Default, Debug)]
222pub struct GeneratorBuilder {
223 inlining: Inlining,
224 naming_strategy: Option<NamingStrategy>,
225}
226
227impl GeneratorBuilder {
228 /// Always try to inline complex types rather than provide them using
229 /// definitions/refs. The exception is recursive types - these cannot
230 /// be expressed without a ref.
231 pub fn prefer_inline(&mut self) -> &mut Self {
232 self.inlining = Inlining::Always;
233 self
234 }
235
236 /// Where possible, provide types by ref even for the top-level type.
237 pub fn top_level_ref(&mut self) -> &mut Self {
238 self.inlining = Inlining::Never;
239 self
240 }
241
242 /// A naming strategy that produces the stringified name
243 /// of the type with type parameters and const parameters in angle brackets.
244 ///
245 /// E.g. if you have a struct like this in the top-level of `my_crate`:
246 ///
247 /// ```
248 /// #[derive(jtd_derive::JsonTypedef)]
249 /// struct Foo<T, const N: usize> {
250 /// x: [T; N],
251 /// }
252 /// ```
253 ///
254 /// Then the concrete type `Foo<u32, 5>` will be named `"Foo<uint32, 5>"`
255 /// in the schema.
256 ///
257 /// Please note that this representation is prone to name collisions if you
258 /// use identically named types in different modules or crates.
259 pub fn naming_short(&mut self) -> &mut Self {
260 self.naming_strategy = Some(NamingStrategy::short());
261 self
262 }
263
264 /// Use the `long` naming strategy. This is the default.
265 ///
266 /// The `long` naming strategy produces the stringified full path
267 /// of the type with type parameters and const parameters in angle brackets.
268 ///
269 /// E.g. if you have a struct like this in the top-level of `my_crate`:
270 ///
271 /// ```
272 /// #[derive(jtd_derive::JsonTypedef)]
273 /// struct Foo<T, const N: usize> {
274 /// x: [T; N],
275 /// }
276 /// ```
277 ///
278 /// Then the concrete type `Foo<u32, 5>` will be named `"my_crate::Foo<uint32, 5>"`
279 /// in the schema.
280 ///
281 /// This representation will prevent name collisions under normal circumstances,
282 /// but it's technically possible some type will manually implement `names()`
283 /// in a weird way.
284 pub fn naming_long(&mut self) -> &mut Self {
285 self.naming_strategy = Some(NamingStrategy::long());
286 self
287 }
288
289 /// Use a custom naming strategy.
290 pub fn naming_custom(&mut self, f: impl Fn(&Names) -> String + 'static) -> &mut Self {
291 self.naming_strategy = Some(NamingStrategy::custom(f));
292 self
293 }
294
295 /// Finalize the configuration and get a `Generator`.
296 pub fn build(&mut self) -> Generator {
297 Generator {
298 inlining: self.inlining,
299 naming_strategy: self.naming_strategy.take().unwrap_or_default(),
300 ..Generator::default()
301 }
302 }
303}
304
305#[derive(Debug, Clone)]
306enum DefinitionState {
307 Finished(Schema),
308 Processing,
309}
310
311impl DefinitionState {
312 fn unwrap(self) -> Schema {
313 if let Self::Finished(schema) = self {
314 schema
315 } else {
316 panic!()
317 }
318 }
319
320 fn finalize(&mut self, schema: Schema) {
321 match self {
322 DefinitionState::Finished(_) => panic!("schema already finalized"),
323 DefinitionState::Processing => *self = DefinitionState::Finished(schema),
324 }
325 }
326}
327
328impl Default for DefinitionState {
329 fn default() -> Self {
330 Self::Processing
331 }
332}
333
334/// Schema generation errors.
335#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
336pub enum GenError {
337 /// A name collision was detected, i.e. two distinct types have the same
338 /// definition/ref identifiers.
339 #[error("definition/ref id \"{id}\" is shared by types `{type1}` and `{type2}`")]
340 NameCollision {
341 type1: String,
342 type2: String,
343 id: String,
344 },
345}