jtd_infer/
hints.rs

1use crate::inferred_number::NumType;
2
3/// Hints for [`Inferrer`][`crate::Inferrer`].
4///
5/// By default, [`Inferrer`][`crate::Inferrer`] will never produce enum, values,
6/// or discriminator forms. Hints tell [`Inferrer`][`crate::Inferrer`] to use
7/// these forms. See [`HintSet`] for details on how you can specify the "paths"
8/// to the pieces of the input that should use these forms.
9///
10/// `default_num_type` tells [`Inferrer`][`crate::Inferrer`] what numeric type
11/// to attempt to use by default when it encounters a JSON number. This default
12/// will be ignored if it doesn't contain the example data. When the default is
13/// ignored, the inferrer will infer the narrowest numerical type possible for
14/// input data, preferring unsigned integers over signed integers.
15///
16/// To adapt the example used at [the crate-level docs][`crate`], here's how you
17/// could change [`Inferrer`][`crate::Inferrer`] behavior using hints:
18///
19/// ```
20/// use serde_json::json;
21/// use jtd_infer::{Inferrer, Hints, HintSet, NumType};
22///
23/// let enum_path = vec!["bar".to_string()];
24/// let mut inferrer = Inferrer::new(Hints::new(
25///     NumType::Float32,
26///     HintSet::new(vec![&enum_path]),
27///     HintSet::new(vec![]),
28///     HintSet::new(vec![]),
29/// ));
30///
31/// inferrer = inferrer.infer(json!({ "foo": true, "bar": "xxx" }));
32/// inferrer = inferrer.infer(json!({ "foo": false, "bar": null, "baz": 5 }));
33///
34/// let inference = inferrer.into_schema();
35///
36/// assert_eq!(
37///     json!({
38///         "properties": {
39///             "foo": { "type": "boolean" },
40///             "bar": { "enum": ["xxx"], "nullable": true }, // now an enum
41///         },
42///         "optionalProperties": {
43///             "baz": { "type": "float32" }, // instead of uint8
44///         },
45///     }),
46///     serde_json::to_value(inference.into_serde_schema()).unwrap(),
47/// )
48/// ```
49pub struct Hints<'a> {
50    default_num_type: NumType,
51    enums: HintSet<'a>,
52    values: HintSet<'a>,
53    discriminator: HintSet<'a>,
54}
55
56impl<'a> Hints<'a> {
57    /// Constructs a new set of [`Hints`].
58    pub fn new(
59        default_num_type: NumType,
60        enums: HintSet<'a>,
61        values: HintSet<'a>,
62        discriminator: HintSet<'a>,
63    ) -> Self {
64        Hints {
65            default_num_type,
66            enums,
67            values,
68            discriminator,
69        }
70    }
71
72    pub(crate) fn default_num_type(&self) -> &NumType {
73        &self.default_num_type
74    }
75
76    pub(crate) fn sub_hints(&self, key: &str) -> Self {
77        Self::new(
78            self.default_num_type.clone(),
79            self.enums.sub_hints(key),
80            self.values.sub_hints(key),
81            self.discriminator.sub_hints(key),
82        )
83    }
84
85    pub(crate) fn is_enum_active(&self) -> bool {
86        self.enums.is_active()
87    }
88
89    pub(crate) fn is_values_active(&self) -> bool {
90        self.values.is_active()
91    }
92
93    pub(crate) fn peek_active_discriminator(&self) -> Option<&str> {
94        self.discriminator.peek_active()
95    }
96}
97
98const WILDCARD: &'static str = "-";
99
100/// A set of paths to parts of the input that are subject to a hint in
101/// [`Hints`].
102pub struct HintSet<'a> {
103    values: Vec<&'a [String]>,
104}
105
106impl<'a> HintSet<'a> {
107    /// Constructs a new [`HintSet`].
108    ///
109    /// Each element of `values` is a separate "path". Each element of a path is
110    /// treated as a path "segment". So, for example, this:
111    ///
112    /// ```
113    /// use jtd_infer::HintSet;
114    ///
115    /// let path1 = vec!["foo".to_string(), "bar".to_string()];
116    /// let path2 = vec!["baz".to_string()];
117    /// HintSet::new(vec![&path1, &path2]);
118    /// ```
119    ///
120    /// Creates a set of paths pointing to `/foo/bar` and `/baz` in an input.
121    ///
122    /// The `-` path segment value is special, and acts as a wildcard, matching
123    /// any property name. It also matches array elements, unlike ordinary path
124    /// segments.
125    pub fn new(values: Vec<&'a [String]>) -> Self {
126        HintSet { values }
127    }
128
129    pub(crate) fn sub_hints(&self, key: &str) -> Self {
130        Self::new(
131            self.values
132                .iter()
133                .filter(|values| {
134                    let first = values.first().map(String::as_str);
135                    first == Some(WILDCARD) || first == Some(key)
136                })
137                .map(|values| &values[1..])
138                .collect(),
139        )
140    }
141
142    pub(crate) fn is_active(&self) -> bool {
143        self.values.iter().any(|values| values.is_empty())
144    }
145
146    pub(crate) fn peek_active(&self) -> Option<&str> {
147        self.values
148            .iter()
149            .find(|values| values.len() == 1)
150            .and_then(|values| values.first().map(String::as_str))
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn hint_set() {
160        let path = vec!["a".to_string(), "b".to_string(), "c".to_string()];
161        let hint_set = HintSet::new(vec![&path]);
162        assert!(!hint_set.is_active());
163        assert_eq!(None, hint_set.peek_active());
164
165        assert!(!hint_set.sub_hints("a").is_active());
166        assert_eq!(None, hint_set.sub_hints("a").peek_active());
167
168        assert!(!hint_set.sub_hints("a").sub_hints("b").is_active());
169        assert_eq!(
170            Some("c"),
171            hint_set.sub_hints("a").sub_hints("b").peek_active()
172        );
173
174        assert!(hint_set
175            .sub_hints("a")
176            .sub_hints("b")
177            .sub_hints("c")
178            .is_active());
179
180        assert_eq!(
181            None,
182            hint_set
183                .sub_hints("a")
184                .sub_hints("b")
185                .sub_hints("c")
186                .peek_active()
187        );
188    }
189
190    #[test]
191    fn hint_set_wildcard() {
192        let path1 = vec!["a".to_string(), "b".to_string(), "c".to_string()];
193        let path2 = vec!["d".to_string(), "-".to_string(), "e".to_string()];
194        let hint_set = HintSet::new(vec![&path1, &path2]);
195
196        assert!(!hint_set
197            .sub_hints("a")
198            .sub_hints("x")
199            .sub_hints("c")
200            .is_active());
201
202        assert!(hint_set
203            .sub_hints("d")
204            .sub_hints("x")
205            .sub_hints("e")
206            .is_active());
207    }
208}