]> git.proxmox.com Git - proxmox.git/blob - proxmox-schema/src/schema.rs
schema: FromIterator lifetime fixup
[proxmox.git] / proxmox-schema / src / schema.rs
1 //! Data types to decscribe data types.
2 //!
3 //! This is loosly based on JSON Schema, but uses static RUST data
4 //! types. This way we can build completely static API
5 //! definitions included with the programs read-only text segment.
6
7 use std::fmt;
8
9 use anyhow::{bail, format_err, Error};
10 use serde_json::{json, Value};
11
12 use crate::ConstRegexPattern;
13
14 /// Error type for schema validation
15 ///
16 /// The validation functions may produce several error message,
17 /// i.e. when validation objects, it can produce one message for each
18 /// erroneous object property.
19 #[derive(Default, Debug)]
20 pub struct ParameterError {
21 error_list: Vec<(String, Error)>,
22 }
23
24 impl std::error::Error for ParameterError {}
25
26 impl ParameterError {
27 pub fn new() -> Self {
28 Self {
29 error_list: Vec::new(),
30 }
31 }
32
33 pub fn push(&mut self, name: String, value: Error) {
34 self.error_list.push((name, value));
35 }
36
37 pub fn len(&self) -> usize {
38 self.error_list.len()
39 }
40
41 pub fn errors(&self) -> &[(String, Error)] {
42 &self.error_list
43 }
44
45 pub fn into_inner(self) -> Vec<(String, Error)> {
46 self.error_list
47 }
48
49 pub fn is_empty(&self) -> bool {
50 self.len() == 0
51 }
52
53 pub fn add_errors(&mut self, prefix: &str, err: Error) {
54 match err.downcast::<ParameterError>() {
55 Ok(param_err) => {
56 self.extend(
57 param_err
58 .into_iter()
59 .map(|(key, err)| (format!("{}/{}", prefix, key), err)),
60 );
61 }
62 Err(err) => self.push(prefix.to_string(), err),
63 }
64 }
65 }
66
67 impl fmt::Display for ParameterError {
68 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
69 let mut msg = String::new();
70
71 if !self.is_empty() {
72 msg.push_str("parameter verification errors\n\n");
73 }
74
75 for (name, err) in self.error_list.iter() {
76 msg.push_str(&format!("parameter '{}': {}\n", name, err));
77 }
78
79 write!(f, "{}", msg)
80 }
81 }
82
83 impl From<(String, Error)> for ParameterError {
84 fn from(err: (String, Error)) -> Self {
85 let mut this = Self::new();
86 this.push(err.0, err.1);
87 this
88 }
89 }
90
91 impl<'a> From<(&'a str, Error)> for ParameterError {
92 fn from(err: (&'a str, Error)) -> Self {
93 Self::from((err.0.to_string(), err.1))
94 }
95 }
96
97 impl std::iter::Extend<(String, Error)> for ParameterError {
98 fn extend<T>(&mut self, iter: T)
99 where
100 T: IntoIterator<Item = (String, Error)>,
101 {
102 self.error_list.extend(iter);
103 }
104 }
105
106 impl<'a> std::iter::Extend<(&'a str, Error)> for ParameterError {
107 fn extend<T>(&mut self, iter: T)
108 where
109 T: IntoIterator<Item = (&'a str, Error)>,
110 {
111 self.extend(iter.into_iter().map(|(s, e)| (s.to_string(), e)));
112 }
113 }
114
115 impl IntoIterator for ParameterError {
116 type Item = (String, Error);
117 type IntoIter = <Vec<(String, Error)> as IntoIterator>::IntoIter;
118
119 fn into_iter(self) -> Self::IntoIter {
120 self.into_inner().into_iter()
121 }
122 }
123
124 impl FromIterator<(String, Error)> for ParameterError {
125 fn from_iter<T>(iter: T) -> Self
126 where
127 T: IntoIterator<Item = (String, Error)>,
128 {
129 let mut this = Self::new();
130 this.extend(iter);
131 this
132 }
133 }
134
135 impl<'a> FromIterator<(&'a str, Error)> for ParameterError {
136 fn from_iter<T>(iter: T) -> Self
137 where
138 T: IntoIterator<Item = (&'a str, Error)>,
139 {
140 let mut this = Self::new();
141 this.extend(iter);
142 this
143 }
144 }
145
146 /// Data type to describe boolean values
147 #[derive(Debug)]
148 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
149 pub struct BooleanSchema {
150 pub description: &'static str,
151 /// Optional default value.
152 pub default: Option<bool>,
153 }
154
155 impl BooleanSchema {
156 pub const fn new(description: &'static str) -> Self {
157 BooleanSchema {
158 description,
159 default: None,
160 }
161 }
162
163 pub const fn default(mut self, default: bool) -> Self {
164 self.default = Some(default);
165 self
166 }
167
168 pub const fn schema(self) -> Schema {
169 Schema::Boolean(self)
170 }
171
172 /// Verify JSON value using a `BooleanSchema`.
173 pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
174 if !data.is_boolean() {
175 bail!("Expected boolean value.");
176 }
177 Ok(())
178 }
179 }
180
181 /// Data type to describe integer values.
182 #[derive(Debug)]
183 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
184 pub struct IntegerSchema {
185 pub description: &'static str,
186 /// Optional minimum.
187 pub minimum: Option<isize>,
188 /// Optional maximum.
189 pub maximum: Option<isize>,
190 /// Optional default.
191 pub default: Option<isize>,
192 }
193
194 impl IntegerSchema {
195 pub const fn new(description: &'static str) -> Self {
196 IntegerSchema {
197 description,
198 default: None,
199 minimum: None,
200 maximum: None,
201 }
202 }
203
204 pub const fn default(mut self, default: isize) -> Self {
205 self.default = Some(default);
206 self
207 }
208
209 pub const fn minimum(mut self, minimum: isize) -> Self {
210 self.minimum = Some(minimum);
211 self
212 }
213
214 pub const fn maximum(mut self, maximium: isize) -> Self {
215 self.maximum = Some(maximium);
216 self
217 }
218
219 pub const fn schema(self) -> Schema {
220 Schema::Integer(self)
221 }
222
223 fn check_constraints(&self, value: isize) -> Result<(), Error> {
224 if let Some(minimum) = self.minimum {
225 if value < minimum {
226 bail!(
227 "value must have a minimum value of {} (got {})",
228 minimum,
229 value
230 );
231 }
232 }
233
234 if let Some(maximum) = self.maximum {
235 if value > maximum {
236 bail!(
237 "value must have a maximum value of {} (got {})",
238 maximum,
239 value
240 );
241 }
242 }
243
244 Ok(())
245 }
246
247 /// Verify JSON value using an `IntegerSchema`.
248 pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
249 if let Some(value) = data.as_i64() {
250 self.check_constraints(value as isize)
251 } else {
252 bail!("Expected integer value.");
253 }
254 }
255 }
256
257 /// Data type to describe (JSON like) number value
258 #[derive(Debug)]
259 pub struct NumberSchema {
260 pub description: &'static str,
261 /// Optional minimum.
262 pub minimum: Option<f64>,
263 /// Optional maximum.
264 pub maximum: Option<f64>,
265 /// Optional default.
266 pub default: Option<f64>,
267 }
268
269 impl NumberSchema {
270 pub const fn new(description: &'static str) -> Self {
271 NumberSchema {
272 description,
273 default: None,
274 minimum: None,
275 maximum: None,
276 }
277 }
278
279 pub const fn default(mut self, default: f64) -> Self {
280 self.default = Some(default);
281 self
282 }
283
284 pub const fn minimum(mut self, minimum: f64) -> Self {
285 self.minimum = Some(minimum);
286 self
287 }
288
289 pub const fn maximum(mut self, maximium: f64) -> Self {
290 self.maximum = Some(maximium);
291 self
292 }
293
294 pub const fn schema(self) -> Schema {
295 Schema::Number(self)
296 }
297
298 fn check_constraints(&self, value: f64) -> Result<(), Error> {
299 if let Some(minimum) = self.minimum {
300 if value < minimum {
301 bail!(
302 "value must have a minimum value of {} (got {})",
303 minimum,
304 value
305 );
306 }
307 }
308
309 if let Some(maximum) = self.maximum {
310 if value > maximum {
311 bail!(
312 "value must have a maximum value of {} (got {})",
313 maximum,
314 value
315 );
316 }
317 }
318
319 Ok(())
320 }
321
322 /// Verify JSON value using an `NumberSchema`.
323 pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
324 if let Some(value) = data.as_f64() {
325 self.check_constraints(value)
326 } else {
327 bail!("Expected number value.");
328 }
329 }
330 }
331
332 #[cfg(feature = "test-harness")]
333 impl Eq for NumberSchema {}
334
335 #[cfg(feature = "test-harness")]
336 impl PartialEq for NumberSchema {
337 fn eq(&self, rhs: &Self) -> bool {
338 fn f64_eq(l: Option<f64>, r: Option<f64>) -> bool {
339 match (l, r) {
340 (None, None) => true,
341 (Some(l), Some(r)) => (l - r).abs() < 0.0001,
342 _ => false,
343 }
344 }
345
346 self.description == rhs.description
347 && f64_eq(self.minimum, rhs.minimum)
348 && f64_eq(self.maximum, rhs.maximum)
349 && f64_eq(self.default, rhs.default)
350 }
351 }
352
353 /// Data type to describe string values.
354 #[derive(Debug)]
355 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
356 pub struct StringSchema {
357 pub description: &'static str,
358 /// Optional default value.
359 pub default: Option<&'static str>,
360 /// Optional minimal length.
361 pub min_length: Option<usize>,
362 /// Optional maximal length.
363 pub max_length: Option<usize>,
364 /// Optional microformat.
365 pub format: Option<&'static ApiStringFormat>,
366 /// A text representation of the format/type (used to generate documentation).
367 pub type_text: Option<&'static str>,
368 }
369
370 impl StringSchema {
371 pub const fn new(description: &'static str) -> Self {
372 StringSchema {
373 description,
374 default: None,
375 min_length: None,
376 max_length: None,
377 format: None,
378 type_text: None,
379 }
380 }
381
382 pub const fn default(mut self, text: &'static str) -> Self {
383 self.default = Some(text);
384 self
385 }
386
387 pub const fn format(mut self, format: &'static ApiStringFormat) -> Self {
388 self.format = Some(format);
389 self
390 }
391
392 pub const fn type_text(mut self, type_text: &'static str) -> Self {
393 self.type_text = Some(type_text);
394 self
395 }
396
397 pub const fn min_length(mut self, min_length: usize) -> Self {
398 self.min_length = Some(min_length);
399 self
400 }
401
402 pub const fn max_length(mut self, max_length: usize) -> Self {
403 self.max_length = Some(max_length);
404 self
405 }
406
407 pub const fn schema(self) -> Schema {
408 Schema::String(self)
409 }
410
411 fn check_length(&self, length: usize) -> Result<(), Error> {
412 if let Some(min_length) = self.min_length {
413 if length < min_length {
414 bail!("value must be at least {} characters long", min_length);
415 }
416 }
417
418 if let Some(max_length) = self.max_length {
419 if length > max_length {
420 bail!("value may only be {} characters long", max_length);
421 }
422 }
423
424 Ok(())
425 }
426
427 pub fn check_constraints(&self, value: &str) -> Result<(), Error> {
428 self.check_length(value.chars().count())?;
429
430 if let Some(ref format) = self.format {
431 match format {
432 ApiStringFormat::Pattern(regex) => {
433 if !(regex.regex_obj)().is_match(value) {
434 bail!("value does not match the regex pattern");
435 }
436 }
437 ApiStringFormat::Enum(variants) => {
438 if !variants.iter().any(|e| e.value == value) {
439 bail!("value '{}' is not defined in the enumeration.", value);
440 }
441 }
442 ApiStringFormat::PropertyString(subschema) => {
443 subschema.parse_property_string(value)?;
444 }
445 ApiStringFormat::VerifyFn(verify_fn) => {
446 verify_fn(value)?;
447 }
448 }
449 }
450
451 Ok(())
452 }
453
454 /// Verify JSON value using this `StringSchema`.
455 pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
456 if let Some(value) = data.as_str() {
457 self.check_constraints(value)
458 } else {
459 bail!("Expected string value.");
460 }
461 }
462 }
463
464 /// Data type to describe array of values.
465 ///
466 /// All array elements are of the same type, as defined in the `items`
467 /// schema.
468 #[derive(Debug)]
469 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
470 pub struct ArraySchema {
471 pub description: &'static str,
472 /// Element type schema.
473 pub items: &'static Schema,
474 /// Optional minimal length.
475 pub min_length: Option<usize>,
476 /// Optional maximal length.
477 pub max_length: Option<usize>,
478 }
479
480 impl ArraySchema {
481 pub const fn new(description: &'static str, item_schema: &'static Schema) -> Self {
482 ArraySchema {
483 description,
484 items: item_schema,
485 min_length: None,
486 max_length: None,
487 }
488 }
489
490 pub const fn min_length(mut self, min_length: usize) -> Self {
491 self.min_length = Some(min_length);
492 self
493 }
494
495 pub const fn max_length(mut self, max_length: usize) -> Self {
496 self.max_length = Some(max_length);
497 self
498 }
499
500 pub const fn schema(self) -> Schema {
501 Schema::Array(self)
502 }
503
504 fn check_length(&self, length: usize) -> Result<(), Error> {
505 if let Some(min_length) = self.min_length {
506 if length < min_length {
507 bail!("array must contain at least {} elements", min_length);
508 }
509 }
510
511 if let Some(max_length) = self.max_length {
512 if length > max_length {
513 bail!("array may only contain {} elements", max_length);
514 }
515 }
516
517 Ok(())
518 }
519
520 /// Verify JSON value using an `ArraySchema`.
521 pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
522 let list = match data {
523 Value::Array(ref list) => list,
524 Value::Object(_) => bail!("Expected array - got object."),
525 _ => bail!("Expected array - got scalar value."),
526 };
527
528 self.check_length(list.len())?;
529
530 for (i, item) in list.iter().enumerate() {
531 let result = self.items.verify_json(item);
532 if let Err(err) = result {
533 return Err(ParameterError::from((format!("[{}]", i), err)).into());
534 }
535 }
536
537 Ok(())
538 }
539 }
540
541 /// Property entry in an object schema:
542 ///
543 /// - `name`: The name of the property
544 /// - `optional`: Set when the property is optional
545 /// - `schema`: Property type schema
546 pub type SchemaPropertyEntry = (&'static str, bool, &'static Schema);
547
548 /// Lookup table to Schema properties
549 ///
550 /// Stores a sorted list of `(name, optional, schema)` tuples:
551 ///
552 /// - `name`: The name of the property
553 /// - `optional`: Set when the property is optional
554 /// - `schema`: Property type schema
555 ///
556 /// **Note:** The list has to be storted by name, because we use
557 /// a binary search to find items.
558 ///
559 /// This is a workaround unless RUST can const_fn `Hash::new()`
560 pub type SchemaPropertyMap = &'static [SchemaPropertyEntry];
561
562 /// Data type to describe objects (maps).
563 #[derive(Debug)]
564 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
565 pub struct ObjectSchema {
566 pub description: &'static str,
567 /// If set, allow additional properties which are not defined in
568 /// the schema.
569 pub additional_properties: bool,
570 /// Property schema definitions.
571 pub properties: SchemaPropertyMap,
572 /// Default key name - used by `parse_parameter_string()`
573 pub default_key: Option<&'static str>,
574 }
575
576 impl ObjectSchema {
577 pub const fn new(description: &'static str, properties: SchemaPropertyMap) -> Self {
578 ObjectSchema {
579 description,
580 properties,
581 additional_properties: false,
582 default_key: None,
583 }
584 }
585
586 pub const fn additional_properties(mut self, additional_properties: bool) -> Self {
587 self.additional_properties = additional_properties;
588 self
589 }
590
591 pub const fn default_key(mut self, key: &'static str) -> Self {
592 self.default_key = Some(key);
593 self
594 }
595
596 pub const fn schema(self) -> Schema {
597 Schema::Object(self)
598 }
599
600 pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
601 if let Ok(ind) = self
602 .properties
603 .binary_search_by_key(&key, |(name, _, _)| name)
604 {
605 let (_name, optional, prop_schema) = self.properties[ind];
606 Some((optional, prop_schema))
607 } else {
608 None
609 }
610 }
611
612 /// Parse key/value pairs and verify with object schema
613 ///
614 /// - `test_required`: is set, checks if all required properties are
615 /// present.
616 pub fn parse_parameter_strings(
617 &'static self,
618 data: &[(String, String)],
619 test_required: bool,
620 ) -> Result<Value, ParameterError> {
621 ParameterSchema::from(self).parse_parameter_strings(data, test_required)
622 }
623 }
624
625 /// Combines multiple *object* schemas into one.
626 ///
627 /// Note that these are limited to object schemas. Other schemas will produce errors.
628 ///
629 /// Technically this could also contain an `additional_properties` flag, however, in the JSON
630 /// Schema, this is not supported, so here we simply assume additional properties to be allowed.
631 #[derive(Debug)]
632 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
633 pub struct AllOfSchema {
634 pub description: &'static str,
635
636 /// The parameter is checked against all of the schemas in the list. Note that all schemas must
637 /// be object schemas.
638 pub list: &'static [&'static Schema],
639 }
640
641 impl AllOfSchema {
642 pub const fn new(description: &'static str, list: &'static [&'static Schema]) -> Self {
643 Self { description, list }
644 }
645
646 pub const fn schema(self) -> Schema {
647 Schema::AllOf(self)
648 }
649
650 pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
651 for entry in self.list {
652 match entry {
653 Schema::AllOf(s) => {
654 if let Some(v) = s.lookup(key) {
655 return Some(v);
656 }
657 }
658 Schema::Object(s) => {
659 if let Some(v) = s.lookup(key) {
660 return Some(v);
661 }
662 }
663 _ => panic!("non-object-schema in `AllOfSchema`"),
664 }
665 }
666
667 None
668 }
669
670 /// Parse key/value pairs and verify with object schema
671 ///
672 /// - `test_required`: is set, checks if all required properties are
673 /// present.
674 pub fn parse_parameter_strings(
675 &'static self,
676 data: &[(String, String)],
677 test_required: bool,
678 ) -> Result<Value, ParameterError> {
679 ParameterSchema::from(self).parse_parameter_strings(data, test_required)
680 }
681 }
682
683 /// Beside [`ObjectSchema`] we also have an [`AllOfSchema`] which also represents objects.
684 pub trait ObjectSchemaType {
685 fn description(&self) -> &'static str;
686 fn lookup(&self, key: &str) -> Option<(bool, &Schema)>;
687 fn properties(&self) -> ObjectPropertyIterator;
688 fn additional_properties(&self) -> bool;
689
690 /// Verify JSON value using an object schema.
691 fn verify_json(&self, data: &Value) -> Result<(), Error> {
692 let map = match data {
693 Value::Object(ref map) => map,
694 Value::Array(_) => bail!("Expected object - got array."),
695 _ => bail!("Expected object - got scalar value."),
696 };
697
698 let mut errors = ParameterError::new();
699
700 let additional_properties = self.additional_properties();
701
702 for (key, value) in map {
703 if let Some((_optional, prop_schema)) = self.lookup(key) {
704 if let Err(err) = prop_schema.verify_json(value) {
705 errors.add_errors(key, err);
706 };
707 } else if !additional_properties {
708 errors.push(
709 key.to_string(),
710 format_err!("schema does not allow additional properties."),
711 );
712 }
713 }
714
715 for (name, optional, _prop_schema) in self.properties() {
716 if !(*optional) && data[name] == Value::Null {
717 errors.push(
718 name.to_string(),
719 format_err!("property is missing and it is not optional."),
720 );
721 }
722 }
723
724 if !errors.is_empty() {
725 Err(errors.into())
726 } else {
727 Ok(())
728 }
729 }
730 }
731
732 impl ObjectSchemaType for ObjectSchema {
733 fn description(&self) -> &'static str {
734 self.description
735 }
736
737 fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
738 ObjectSchema::lookup(self, key)
739 }
740
741 fn properties(&self) -> ObjectPropertyIterator {
742 ObjectPropertyIterator {
743 schemas: [].iter(),
744 properties: Some(self.properties.iter()),
745 nested: None,
746 }
747 }
748
749 fn additional_properties(&self) -> bool {
750 self.additional_properties
751 }
752 }
753
754 impl ObjectSchemaType for AllOfSchema {
755 fn description(&self) -> &'static str {
756 self.description
757 }
758
759 fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
760 AllOfSchema::lookup(self, key)
761 }
762
763 fn properties(&self) -> ObjectPropertyIterator {
764 ObjectPropertyIterator {
765 schemas: self.list.iter(),
766 properties: None,
767 nested: None,
768 }
769 }
770
771 fn additional_properties(&self) -> bool {
772 true
773 }
774 }
775
776 #[doc(hidden)]
777 pub struct ObjectPropertyIterator {
778 schemas: std::slice::Iter<'static, &'static Schema>,
779 properties: Option<std::slice::Iter<'static, SchemaPropertyEntry>>,
780 nested: Option<Box<ObjectPropertyIterator>>,
781 }
782
783 impl Iterator for ObjectPropertyIterator {
784 type Item = &'static SchemaPropertyEntry;
785
786 fn next(&mut self) -> Option<&'static SchemaPropertyEntry> {
787 loop {
788 match self.nested.as_mut().and_then(Iterator::next) {
789 Some(item) => return Some(item),
790 None => self.nested = None,
791 }
792
793 match self.properties.as_mut().and_then(Iterator::next) {
794 Some(item) => return Some(item),
795 None => match self.schemas.next()? {
796 Schema::AllOf(o) => self.nested = Some(Box::new(o.properties())),
797 Schema::Object(o) => self.properties = Some(o.properties.iter()),
798 _ => {
799 self.properties = None;
800 continue;
801 }
802 },
803 }
804 }
805 }
806 }
807
808 /// Schemas are used to describe complex data types.
809 ///
810 /// All schema types implement constant builder methods, and a final
811 /// `schema()` method to convert them into a `Schema`.
812 ///
813 /// ```
814 /// use proxmox_schema::{Schema, BooleanSchema, IntegerSchema, ObjectSchema};
815 ///
816 /// const SIMPLE_OBJECT: Schema = ObjectSchema::new(
817 /// "A very simple object with 2 properties",
818 /// &[ // this arrays needs to be storted by name!
819 /// (
820 /// "property_one",
821 /// false /* required */,
822 /// &IntegerSchema::new("A required integer property.")
823 /// .minimum(0)
824 /// .maximum(100)
825 /// .schema()
826 /// ),
827 /// (
828 /// "property_two",
829 /// true /* optional */,
830 /// &BooleanSchema::new("An optional boolean property.")
831 /// .default(true)
832 /// .schema()
833 /// ),
834 /// ],
835 /// ).schema();
836 /// ```
837 #[derive(Debug)]
838 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
839 pub enum Schema {
840 Null,
841 Boolean(BooleanSchema),
842 Integer(IntegerSchema),
843 Number(NumberSchema),
844 String(StringSchema),
845 Object(ObjectSchema),
846 Array(ArraySchema),
847 AllOf(AllOfSchema),
848 }
849
850 impl Schema {
851 /// Verify JSON value with `schema`.
852 pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
853 match self {
854 Schema::Null => {
855 if !data.is_null() {
856 bail!("Expected Null, but value is not Null.");
857 }
858 }
859 Schema::Object(s) => s.verify_json(data)?,
860 Schema::Array(s) => s.verify_json(data)?,
861 Schema::Boolean(s) => s.verify_json(data)?,
862 Schema::Integer(s) => s.verify_json(data)?,
863 Schema::Number(s) => s.verify_json(data)?,
864 Schema::String(s) => s.verify_json(data)?,
865 Schema::AllOf(s) => s.verify_json(data)?,
866 }
867 Ok(())
868 }
869
870 /// Parse a simple value (no arrays and no objects)
871 pub fn parse_simple_value(&self, value_str: &str) -> Result<Value, Error> {
872 let value = match self {
873 Schema::Null => {
874 bail!("internal error - found Null schema.");
875 }
876 Schema::Boolean(_boolean_schema) => {
877 let res = parse_boolean(value_str)?;
878 Value::Bool(res)
879 }
880 Schema::Integer(integer_schema) => {
881 let res: isize = value_str.parse()?;
882 integer_schema.check_constraints(res)?;
883 Value::Number(res.into())
884 }
885 Schema::Number(number_schema) => {
886 let res: f64 = value_str.parse()?;
887 number_schema.check_constraints(res)?;
888 Value::Number(serde_json::Number::from_f64(res).unwrap())
889 }
890 Schema::String(string_schema) => {
891 string_schema.check_constraints(value_str)?;
892 Value::String(value_str.into())
893 }
894 _ => bail!("unable to parse complex (sub) objects."),
895 };
896 Ok(value)
897 }
898
899 /// Parse a complex property string (`ApiStringFormat::PropertyString`)
900 pub fn parse_property_string(&'static self, value_str: &str) -> Result<Value, Error> {
901 // helper for object/allof schemas:
902 fn parse_object<T: Into<ParameterSchema>>(
903 value_str: &str,
904 schema: T,
905 default_key: Option<&'static str>,
906 ) -> Result<Value, Error> {
907 let mut param_list: Vec<(String, String)> = vec![];
908 let key_val_list: Vec<&str> = value_str
909 .split(|c: char| c == ',' || c == ';')
910 .filter(|s| !s.is_empty())
911 .collect();
912 for key_val in key_val_list {
913 let kv: Vec<&str> = key_val.splitn(2, '=').collect();
914 if kv.len() == 2 {
915 param_list.push((kv[0].trim().into(), kv[1].trim().into()));
916 } else if let Some(key) = default_key {
917 param_list.push((key.into(), kv[0].trim().into()));
918 } else {
919 bail!("Value without key, but schema does not define a default key.");
920 }
921 }
922
923 schema
924 .into()
925 .parse_parameter_strings(&param_list, true)
926 .map_err(Error::from)
927 }
928
929 match self {
930 Schema::Object(object_schema) => {
931 parse_object(value_str, object_schema, object_schema.default_key)
932 }
933 Schema::AllOf(all_of_schema) => parse_object(value_str, all_of_schema, None),
934 Schema::Array(array_schema) => {
935 let mut array: Vec<Value> = vec![];
936 let list: Vec<&str> = value_str
937 .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
938 .filter(|s| !s.is_empty())
939 .collect();
940
941 for value in list {
942 match array_schema.items.parse_simple_value(value.trim()) {
943 Ok(res) => array.push(res),
944 Err(err) => bail!("unable to parse array element: {}", err),
945 }
946 }
947 array_schema.check_length(array.len())?;
948
949 Ok(array.into())
950 }
951 _ => bail!("Got unexpected schema type."),
952 }
953 }
954 }
955
956 /// A string enum entry. An enum entry must have a value and a description.
957 #[derive(Clone, Debug)]
958 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
959 pub struct EnumEntry {
960 pub value: &'static str,
961 pub description: &'static str,
962 }
963
964 impl EnumEntry {
965 /// Convenience method as long as we only have 2 mandatory fields in an `EnumEntry`.
966 pub const fn new(value: &'static str, description: &'static str) -> Self {
967 Self { value, description }
968 }
969 }
970
971 /// String microformat definitions.
972 ///
973 /// Strings are probably the most flexible data type, and there are
974 /// several ways to define their content.
975 ///
976 /// ## Enumerations
977 ///
978 /// Simple list all possible values.
979 ///
980 /// ```
981 /// use proxmox_schema::{ApiStringFormat, EnumEntry};
982 ///
983 /// const format: ApiStringFormat = ApiStringFormat::Enum(&[
984 /// EnumEntry::new("vm", "A guest VM run via qemu"),
985 /// EnumEntry::new("ct", "A guest container run via lxc"),
986 /// ]);
987 /// ```
988 ///
989 /// ## Regular Expressions
990 ///
991 /// Use a regular expression to describe valid strings.
992 ///
993 /// ```
994 /// use proxmox_schema::{const_regex, ApiStringFormat};
995 ///
996 /// const_regex! {
997 /// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$";
998 /// }
999 /// const format: ApiStringFormat = ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
1000 /// ```
1001 ///
1002 /// ## Property Strings
1003 ///
1004 /// Use a schema to describe complex types encoded as string.
1005 ///
1006 /// Arrays are parsed as comma separated lists, i.e: `"1,2,3"`. The
1007 /// list may be sparated by comma, semicolon or whitespace.
1008 ///
1009 /// Objects are parsed as comma (or semicolon) separated `key=value` pairs, i.e:
1010 /// `"prop1=2,prop2=test"`. Any whitespace is trimmed from key and value.
1011 ///
1012 ///
1013 /// **Note:** This only works for types which does not allow using the
1014 /// comma, semicolon or whitespace separator inside the value,
1015 /// i.e. this only works for arrays of simple data types, and objects
1016 /// with simple properties (no nesting).
1017 ///
1018 /// ```
1019 /// use proxmox_schema::{ApiStringFormat, ArraySchema, IntegerSchema, Schema, StringSchema};
1020 /// use proxmox_schema::{parse_simple_value, parse_property_string};
1021 ///
1022 /// const PRODUCT_LIST_SCHEMA: Schema =
1023 /// ArraySchema::new("Product List.", &IntegerSchema::new("Product ID").schema())
1024 /// .min_length(1)
1025 /// .schema();
1026 ///
1027 /// const SCHEMA: Schema = StringSchema::new("A list of Product IDs, comma separated.")
1028 /// .format(&ApiStringFormat::PropertyString(&PRODUCT_LIST_SCHEMA))
1029 /// .schema();
1030 ///
1031 /// let res = parse_simple_value("", &SCHEMA);
1032 /// assert!(res.is_err());
1033 ///
1034 /// let res = parse_simple_value("1,2,3", &SCHEMA); // parse as String
1035 /// assert!(res.is_ok());
1036 ///
1037 /// let data = parse_property_string("1,2", &PRODUCT_LIST_SCHEMA); // parse as Array
1038 /// assert!(data.is_ok());
1039 /// ```
1040 pub enum ApiStringFormat {
1041 /// Enumerate all valid strings
1042 Enum(&'static [EnumEntry]),
1043 /// Use a regular expression to describe valid strings.
1044 Pattern(&'static ConstRegexPattern),
1045 /// Use a schema to describe complex types encoded as string.
1046 PropertyString(&'static Schema),
1047 /// Use a verification function.
1048 VerifyFn(fn(&str) -> Result<(), Error>),
1049 }
1050
1051 impl std::fmt::Debug for ApiStringFormat {
1052 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1053 match self {
1054 ApiStringFormat::VerifyFn(fnptr) => write!(f, "VerifyFn({:p}", fnptr),
1055 ApiStringFormat::Enum(variants) => write!(f, "Enum({:?}", variants),
1056 ApiStringFormat::Pattern(regex) => write!(f, "Pattern({:?}", regex),
1057 ApiStringFormat::PropertyString(schema) => write!(f, "PropertyString({:?}", schema),
1058 }
1059 }
1060 }
1061
1062 #[cfg(feature = "test-harness")]
1063 impl Eq for ApiStringFormat {}
1064
1065 #[cfg(feature = "test-harness")]
1066 impl PartialEq for ApiStringFormat {
1067 fn eq(&self, rhs: &Self) -> bool {
1068 match (self, rhs) {
1069 (ApiStringFormat::Enum(l), ApiStringFormat::Enum(r)) => l == r,
1070 (ApiStringFormat::Pattern(l), ApiStringFormat::Pattern(r)) => l == r,
1071 (ApiStringFormat::PropertyString(l), ApiStringFormat::PropertyString(r)) => l == r,
1072 (ApiStringFormat::VerifyFn(l), ApiStringFormat::VerifyFn(r)) => std::ptr::eq(l, r),
1073 (_, _) => false,
1074 }
1075 }
1076 }
1077
1078 /// Parameters are objects, but we have two types of object schemas, the regular one and the
1079 /// `AllOf` schema.
1080 #[derive(Clone, Copy, Debug)]
1081 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
1082 pub enum ParameterSchema {
1083 Object(&'static ObjectSchema),
1084 AllOf(&'static AllOfSchema),
1085 }
1086
1087 impl ParameterSchema {
1088 /// Parse key/value pairs and verify with object schema
1089 ///
1090 /// - `test_required`: is set, checks if all required properties are
1091 /// present.
1092 pub fn parse_parameter_strings(
1093 self,
1094 data: &[(String, String)],
1095 test_required: bool,
1096 ) -> Result<Value, ParameterError> {
1097 do_parse_parameter_strings(self, data, test_required)
1098 }
1099 }
1100
1101 impl ObjectSchemaType for ParameterSchema {
1102 fn description(&self) -> &'static str {
1103 match self {
1104 ParameterSchema::Object(o) => o.description(),
1105 ParameterSchema::AllOf(o) => o.description(),
1106 }
1107 }
1108
1109 fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
1110 match self {
1111 ParameterSchema::Object(o) => o.lookup(key),
1112 ParameterSchema::AllOf(o) => o.lookup(key),
1113 }
1114 }
1115
1116 fn properties(&self) -> ObjectPropertyIterator {
1117 match self {
1118 ParameterSchema::Object(o) => o.properties(),
1119 ParameterSchema::AllOf(o) => o.properties(),
1120 }
1121 }
1122
1123 fn additional_properties(&self) -> bool {
1124 match self {
1125 ParameterSchema::Object(o) => o.additional_properties(),
1126 ParameterSchema::AllOf(o) => o.additional_properties(),
1127 }
1128 }
1129 }
1130
1131 impl From<&'static ObjectSchema> for ParameterSchema {
1132 fn from(schema: &'static ObjectSchema) -> Self {
1133 ParameterSchema::Object(schema)
1134 }
1135 }
1136
1137 impl From<&'static AllOfSchema> for ParameterSchema {
1138 fn from(schema: &'static AllOfSchema) -> Self {
1139 ParameterSchema::AllOf(schema)
1140 }
1141 }
1142
1143 /// Helper function to parse boolean values
1144 ///
1145 /// - true: `1 | on | yes | true`
1146 /// - false: `0 | off | no | false`
1147 pub fn parse_boolean(value_str: &str) -> Result<bool, Error> {
1148 match value_str.to_lowercase().as_str() {
1149 "1" | "on" | "yes" | "true" => Ok(true),
1150 "0" | "off" | "no" | "false" => Ok(false),
1151 _ => bail!("Unable to parse boolean option."),
1152 }
1153 }
1154
1155 /// Parse a complex property string (`ApiStringFormat::PropertyString`)
1156 #[deprecated(note = "this is now a method of Schema")]
1157 pub fn parse_property_string(value_str: &str, schema: &'static Schema) -> Result<Value, Error> {
1158 schema.parse_property_string(value_str)
1159 }
1160
1161 /// Parse a simple value (no arrays and no objects)
1162 #[deprecated(note = "this is now a method of Schema")]
1163 pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result<Value, Error> {
1164 schema.parse_simple_value(value_str)
1165 }
1166
1167 /// Parse key/value pairs and verify with object schema
1168 ///
1169 /// - `test_required`: is set, checks if all required properties are
1170 /// present.
1171 #[deprecated(note = "this is now a method of parameter schema types")]
1172 pub fn parse_parameter_strings<T: Into<ParameterSchema>>(
1173 data: &[(String, String)],
1174 schema: T,
1175 test_required: bool,
1176 ) -> Result<Value, ParameterError> {
1177 do_parse_parameter_strings(schema.into(), data, test_required)
1178 }
1179
1180 fn do_parse_parameter_strings(
1181 schema: ParameterSchema,
1182 data: &[(String, String)],
1183 test_required: bool,
1184 ) -> Result<Value, ParameterError> {
1185 let mut params = json!({});
1186
1187 let mut errors = ParameterError::new();
1188
1189 let additional_properties = schema.additional_properties();
1190
1191 for (key, value) in data {
1192 if let Some((_optional, prop_schema)) = schema.lookup(key) {
1193 match prop_schema {
1194 Schema::Array(array_schema) => {
1195 if params[key] == Value::Null {
1196 params[key] = json!([]);
1197 }
1198 match params[key] {
1199 Value::Array(ref mut array) => {
1200 match array_schema.items.parse_simple_value(value) {
1201 Ok(res) => array.push(res), // fixme: check_length??
1202 Err(err) => errors.push(key.into(), err),
1203 }
1204 }
1205 _ => {
1206 errors.push(key.into(), format_err!("expected array - type missmatch"))
1207 }
1208 }
1209 }
1210 _ => match prop_schema.parse_simple_value(value) {
1211 Ok(res) => {
1212 if params[key] == Value::Null {
1213 params[key] = res;
1214 } else {
1215 errors.push(key.into(), format_err!("duplicate parameter."));
1216 }
1217 }
1218 Err(err) => errors.push(key.into(), err),
1219 },
1220 }
1221 } else if additional_properties {
1222 match params[key] {
1223 Value::Null => {
1224 params[key] = Value::String(value.to_owned());
1225 }
1226 Value::String(ref old) => {
1227 params[key] = Value::Array(vec![
1228 Value::String(old.to_owned()),
1229 Value::String(value.to_owned()),
1230 ]);
1231 }
1232 Value::Array(ref mut array) => {
1233 array.push(Value::String(value.to_string()));
1234 }
1235 _ => errors.push(key.into(), format_err!("expected array - type missmatch")),
1236 }
1237 } else {
1238 errors.push(
1239 key.into(),
1240 format_err!("schema does not allow additional properties."),
1241 );
1242 }
1243 }
1244
1245 if test_required && errors.is_empty() {
1246 for (name, optional, _prop_schema) in schema.properties() {
1247 if !(*optional) && params[name] == Value::Null {
1248 errors.push(
1249 name.to_string(),
1250 format_err!("parameter is missing and it is not optional."),
1251 );
1252 }
1253 }
1254 }
1255
1256 if !errors.is_empty() {
1257 Err(errors)
1258 } else {
1259 Ok(params)
1260 }
1261 }
1262
1263 /// Verify JSON value with `schema`.
1264 #[deprecated(note = "use the method schema.verify_json() instead")]
1265 pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> {
1266 schema.verify_json(data)
1267 }
1268
1269 /// Verify JSON value using a `StringSchema`.
1270 #[deprecated(note = "use the method string_schema.verify_json() instead")]
1271 pub fn verify_json_string(data: &Value, schema: &StringSchema) -> Result<(), Error> {
1272 schema.verify_json(data)
1273 }
1274
1275 /// Verify JSON value using a `BooleanSchema`.
1276 #[deprecated(note = "use the method boolean_schema.verify_json() instead")]
1277 pub fn verify_json_boolean(data: &Value, schema: &BooleanSchema) -> Result<(), Error> {
1278 schema.verify_json(data)
1279 }
1280
1281 /// Verify JSON value using an `IntegerSchema`.
1282 #[deprecated(note = "use the method integer_schema.verify_json() instead")]
1283 pub fn verify_json_integer(data: &Value, schema: &IntegerSchema) -> Result<(), Error> {
1284 schema.verify_json(data)
1285 }
1286
1287 /// Verify JSON value using an `NumberSchema`.
1288 #[deprecated(note = "use the method number_schema.verify_json() instead")]
1289 pub fn verify_json_number(data: &Value, schema: &NumberSchema) -> Result<(), Error> {
1290 schema.verify_json(data)
1291 }
1292
1293 /// Verify JSON value using an `ArraySchema`.
1294 #[deprecated(note = "use the method array_schema.verify_json() instead")]
1295 pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error> {
1296 schema.verify_json(data)
1297 }
1298
1299 /// Verify JSON value using an `ObjectSchema`.
1300 #[deprecated(note = "use the verify_json() method via the ObjectSchemaType trait instead")]
1301 pub fn verify_json_object(data: &Value, schema: &dyn ObjectSchemaType) -> Result<(), Error> {
1302 schema.verify_json(data)
1303 }
1304
1305 /// API types should define an "updater type" via this trait in order to support derived "Updater"
1306 /// structs more easily.
1307 ///
1308 /// Most trivial types can simply use an `Option<Self>` as updater. For types which do not use the
1309 /// `#[api]` macro, this will need to be explicitly created (or derived via
1310 /// `#[derive(UpdaterType)]`.
1311 pub trait UpdaterType: Sized {
1312 type Updater: Updater;
1313 }
1314
1315 #[cfg(feature = "api-macro")]
1316 pub use proxmox_api_macro::UpdaterType;
1317
1318 #[cfg(feature = "api-macro")]
1319 #[doc(hidden)]
1320 pub use proxmox_api_macro::Updater;
1321
1322 macro_rules! basic_updater_type {
1323 ($($ty:ty)*) => {
1324 $(
1325 impl UpdaterType for $ty {
1326 type Updater = Option<Self>;
1327 }
1328 )*
1329 };
1330 }
1331 basic_updater_type! { bool u8 u16 u32 u64 i8 i16 i32 i64 usize isize f32 f64 String char }
1332
1333 impl<T> UpdaterType for Option<T>
1334 where
1335 T: UpdaterType,
1336 {
1337 type Updater = T::Updater;
1338 }
1339
1340 // this will replace the whole Vec
1341 impl<T> UpdaterType for Vec<T> {
1342 type Updater = Option<Self>;
1343 }
1344
1345 pub trait ApiType {
1346 const API_SCHEMA: Schema;
1347 }
1348
1349 impl<T: ApiType> ApiType for Option<T> {
1350 const API_SCHEMA: Schema = T::API_SCHEMA;
1351 }
1352
1353 /// A helper type for "Updater" structs. This trait is *not* implemented for an api "base" type
1354 /// when deriving an `Updater` for it, though the generated *updater* type does implement this
1355 /// trait!
1356 ///
1357 /// This trait is mostly to figure out if an updater is empty (iow. it should not be applied).
1358 /// In that, it is useful when a type which should have an updater also has optional fields which
1359 /// should not be serialized. Instead of `#[serde(skip_serializing_if = "Option::is_none")]`, this
1360 /// trait's `is_empty` needs to be used via `#[serde(skip_serializing_if = "Updater::is_empty")]`.
1361 pub trait Updater {
1362 /// Check if the updater is "none" or "empty".
1363 fn is_empty(&self) -> bool;
1364 }
1365
1366 impl<T> Updater for Vec<T> {
1367 fn is_empty(&self) -> bool {
1368 self.is_empty()
1369 }
1370 }
1371
1372 impl<T> Updater for Option<T> {
1373 fn is_empty(&self) -> bool {
1374 self.is_none()
1375 }
1376 }
1377
1378 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
1379 pub struct ReturnType {
1380 /// A return type may be optional, meaning the method may return null or some fixed data.
1381 ///
1382 /// If true, the return type in pseudo openapi terms would be `"oneOf": [ "null", "T" ]`.
1383 pub optional: bool,
1384
1385 /// The method's return type.
1386 pub schema: &'static Schema,
1387 }
1388
1389 impl std::fmt::Debug for ReturnType {
1390 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1391 if self.optional {
1392 write!(f, "optional {:?}", self.schema)
1393 } else {
1394 write!(f, "{:?}", self.schema)
1395 }
1396 }
1397 }
1398
1399 impl ReturnType {
1400 pub const fn new(optional: bool, schema: &'static Schema) -> Self {
1401 Self { optional, schema }
1402 }
1403 }