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}