]> git.proxmox.com Git - proxmox.git/blame - proxmox-schema/src/de/verify.rs
schema: implement oneOf schema support
[proxmox.git] / proxmox-schema / src / de / verify.rs
CommitLineData
5a64f325 1use std::borrow::Cow;
1a46283b 2use std::cell::RefCell;
12da5121 3use std::collections::{BTreeSet, HashSet};
5a64f325
WB
4use std::fmt;
5use std::mem;
6
7use anyhow::format_err;
8use serde::de::{self, Deserialize, Unexpected};
9
10use super::Schema;
11use crate::schema::ParameterError;
12
13struct VerifyState {
14 schema: Option<&'static Schema>,
15 path: String,
16}
17
18thread_local! {
1a46283b
WB
19 static VERIFY_SCHEMA: RefCell<Option<VerifyState>> = RefCell::new(None);
20 static ERRORS: RefCell<Vec<(String, anyhow::Error)>> = RefCell::new(Vec::new());
5a64f325
WB
21}
22
23pub(crate) struct SchemaGuard(Option<VerifyState>);
24
25impl Drop for SchemaGuard {
26 fn drop(&mut self) {
1a46283b 27 VERIFY_SCHEMA.with(|schema| {
5a64f325 28 if self.0.is_none() {
1a46283b 29 ERRORS.with(|errors| errors.borrow_mut().clear())
5a64f325 30 }
1a46283b 31 *schema.borrow_mut() = self.0.take();
5a64f325
WB
32 });
33 }
34}
35
36impl SchemaGuard {
37 /// If this is the "final" guard, take out the errors:
38 fn errors(self) -> Option<Vec<(String, anyhow::Error)>> {
39 if self.0.is_none() {
c702638b
WB
40 let errors = ERRORS.with(|e| mem::take(&mut *e.borrow_mut()));
41 (!errors.is_empty()).then_some(errors)
5a64f325
WB
42 } else {
43 None
44 }
45 }
46}
47
48pub(crate) fn push_schema(schema: Option<&'static Schema>, path: Option<&str>) -> SchemaGuard {
49 SchemaGuard(VERIFY_SCHEMA.with(|s| {
1a46283b 50 let prev = s.borrow_mut().take();
5a64f325
WB
51 let path = match (path, &prev) {
52 (Some(path), Some(prev)) => join_path(&prev.path, path),
53 (Some(path), None) => path.to_owned(),
54 (None, Some(prev)) => prev.path.clone(),
55 (None, None) => String::new(),
56 };
57
1a46283b 58 *s.borrow_mut() = Some(VerifyState { schema, path });
5a64f325
WB
59
60 prev
61 }))
62}
63
64fn get_path() -> Option<String> {
1a46283b 65 VERIFY_SCHEMA.with(|s| s.borrow().as_ref().map(|state| state.path.clone()))
5a64f325
WB
66}
67
68fn get_schema() -> Option<&'static Schema> {
1a46283b 69 VERIFY_SCHEMA.with(|s| s.borrow().as_ref().and_then(|state| state.schema))
5a64f325
WB
70}
71
72pub(crate) fn is_verifying() -> bool {
1a46283b 73 VERIFY_SCHEMA.with(|s| s.borrow().as_ref().is_some())
5a64f325
WB
74}
75
76fn join_path(a: &str, b: &str) -> String {
77 if a.is_empty() {
78 b.to_string()
79 } else {
80 format!("{}/{}", a, b)
81 }
82}
83
84fn push_errstr_path(err_path: &str, err: &str) {
85 if let Some(path) = get_path() {
86 push_err_do(join_path(&path, err_path), format_err!("{}", err));
87 }
88}
89
90fn push_err(err: impl fmt::Display) {
91 if let Some(path) = get_path() {
92 push_err_do(path, format_err!("{}", err));
93 }
94}
95
96fn push_err_do(path: String, err: anyhow::Error) {
1a46283b 97 ERRORS.with(move |errors| errors.borrow_mut().push((path, err)))
5a64f325
WB
98}
99
100/// Helper to collect multiple deserialization errors for better reporting.
101///
102/// This is similar to [`IgnoredAny`] in that it implements [`Deserialize`]
103/// but does not actually deserialize to anything, however, when a deserialization error occurs,
104/// it'll try to continue and collect further errors.
105///
106/// This only makes sense with the [`SchemaDeserializer`](super::SchemaDeserializer).
107pub struct Verifier;
108
109impl<'de> Deserialize<'de> for Verifier {
110 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111 where
112 D: de::Deserializer<'de>,
113 {
114 if let Some(schema) = get_schema() {
115 let visitor = Visitor(schema);
116 match schema {
117 Schema::Boolean(_) => deserializer.deserialize_bool(visitor),
118 Schema::Integer(_) => deserializer.deserialize_i64(visitor),
119 Schema::Number(_) => deserializer.deserialize_f64(visitor),
120 Schema::String(_) => deserializer.deserialize_str(visitor),
121 Schema::Object(_) => deserializer.deserialize_map(visitor),
122 Schema::AllOf(_) => deserializer.deserialize_map(visitor),
d48a1508 123 Schema::OneOf(_) => deserializer.deserialize_map(visitor),
5a64f325
WB
124 Schema::Array(_) => deserializer.deserialize_seq(visitor),
125 Schema::Null => deserializer.deserialize_unit(visitor),
126 }
127 } else {
128 Ok(Verifier)
129 }
130 }
131}
132
133pub fn verify(schema: &'static Schema, value: &str) -> Result<(), anyhow::Error> {
134 let guard = push_schema(Some(schema), None);
135 Verifier::deserialize(super::SchemaDeserializer::new(value, schema))?;
136
137 if let Some(errors) = guard.errors() {
138 Err(ParameterError::from_list(errors).into())
139 } else {
140 Ok(())
141 }
142}
143
144struct Visitor(&'static Schema);
145
146impl<'de> de::Visitor<'de> for Visitor {
147 type Value = Verifier;
148
149 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
150 match self.0 {
151 Schema::Boolean(_) => f.write_str("boolean"),
152 Schema::Integer(_) => f.write_str("integer"),
153 Schema::Number(_) => f.write_str("number"),
154 Schema::String(_) => f.write_str("string"),
155 Schema::Object(_) => f.write_str("object"),
156 Schema::AllOf(_) => f.write_str("allOf"),
d48a1508 157 Schema::OneOf(_) => f.write_str("oneOf"),
5a64f325
WB
158 Schema::Array(_) => f.write_str("Array"),
159 Schema::Null => f.write_str("null"),
160 }
161 }
162
163 fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
164 match self.0 {
165 Schema::Boolean(_) => (),
166 _ => return Err(E::invalid_type(Unexpected::Bool(v), &self)),
167 }
168 Ok(Verifier)
169 }
170
171 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
172 match self.0 {
173 Schema::Integer(schema) => match schema.check_constraints(v as isize) {
174 Ok(()) => Ok(Verifier),
175 Err(err) => Err(E::custom(err)),
176 },
177 _ => Err(E::invalid_type(Unexpected::Signed(v), &self)),
178 }
179 }
180
181 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
182 match self.0 {
183 Schema::Integer(schema) => match schema.check_constraints(v as isize) {
184 Ok(()) => Ok(Verifier),
185 Err(err) => Err(E::custom(err)),
186 },
187 _ => Err(E::invalid_type(Unexpected::Unsigned(v), &self)),
188 }
189 }
190
191 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
192 match self.0 {
193 Schema::Number(schema) => match schema.check_constraints(v) {
194 Ok(()) => Ok(Verifier),
195 Err(err) => Err(E::custom(err)),
196 },
197 _ => Err(E::invalid_type(Unexpected::Float(v), &self)),
198 }
199 }
200
201 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
202 use de::Error;
203
204 let schema = match self.0 {
205 Schema::Array(schema) => schema,
206 _ => return Err(A::Error::invalid_type(Unexpected::Seq, &self)),
207 };
208
209 let _guard = push_schema(Some(schema.items), None);
210
211 let mut count = 0;
212 loop {
213 match seq.next_element::<Verifier>() {
214 Ok(Some(_)) => count += 1,
215 Ok(None) => break,
216 Err(err) => push_err(err),
217 }
218 }
219
220 schema.check_length(count).map_err(de::Error::custom)?;
221
222 Ok(Verifier)
223 }
224
225 fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
226 use de::Error;
227
228 let schema: &'static dyn crate::schema::ObjectSchemaType = match self.0 {
229 Schema::Object(schema) => schema,
230 Schema::AllOf(schema) => schema,
d48a1508 231 Schema::OneOf(schema) => schema,
5a64f325
WB
232 _ => return Err(A::Error::invalid_type(Unexpected::Map, &self)),
233 };
234
12da5121
WB
235 // The tests need this to be in a predictable order, so HashSet won't work as it uses a
236 // randomized default state.
237 let mut required_keys = BTreeSet::<&'static str>::new();
5a64f325
WB
238 for (key, optional, _schema) in schema.properties() {
239 if !optional {
240 required_keys.insert(key);
241 }
242 }
243
244 let mut other_keys = HashSet::<String>::new();
245 loop {
246 let key: Cow<'de, str> = match map.next_key()? {
247 Some(key) => key,
248 None => break,
249 };
250
251 let _guard = match schema.lookup(&key) {
252 Some((optional, schema)) => {
253 if !optional {
254 // required keys are only tracked in the required_keys hashset
255 if !required_keys.remove(key.as_ref()) {
256 // duplicate key
257 push_errstr_path(&key, "duplicate key");
258 }
259 } else {
260 // optional keys
261 if !other_keys.insert(key.clone().into_owned()) {
262 push_errstr_path(&key, "duplicate key");
263 }
264 }
265
266 push_schema(Some(schema), Some(&key))
267 }
268 None => {
269 if !schema.additional_properties() {
270 push_errstr_path(&key, "schema does not allow additional properties");
271 } else if !other_keys.insert(key.clone().into_owned()) {
272 push_errstr_path(&key, "duplicate key");
273 }
274
275 push_schema(None, Some(&key))
276 }
277 };
278
279 match map.next_value::<Verifier>() {
280 Ok(Verifier) => (),
281 Err(err) => push_err(err),
282 }
283 }
284
285 for key in required_keys {
286 push_errstr_path(key, "property is missing and it is not optional");
287 }
288
289 Ok(Verifier)
290 }
291
292 fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
293 let schema = match self.0 {
294 Schema::String(schema) => schema,
295 _ => return Err(E::invalid_type(Unexpected::Str(value), &self)),
296 };
297
58b29dfb 298 #[allow(clippy::let_unit_value)]
5a64f325
WB
299 let _: () = schema.check_constraints(value).map_err(E::custom)?;
300
301 Ok(Verifier)
302 }
303}