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}