1 //! Data types to decscribe data types.
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.
9 use anyhow
::{bail, format_err, Error}
;
10 use serde_json
::{json, Value}
;
12 use crate::ConstRegexPattern
;
14 /// Error type for schema validation
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
)>,
24 impl std
::error
::Error
for ParameterError {}
27 pub fn new() -> Self {
29 error_list
: Vec
::new(),
33 pub fn push(&mut self, name
: String
, value
: Error
) {
34 self.error_list
.push((name
, value
));
37 pub fn len(&self) -> usize {
41 pub fn errors(&self) -> &[(String
, Error
)] {
45 pub fn into_inner(self) -> Vec
<(String
, Error
)> {
49 pub fn is_empty(&self) -> bool
{
53 pub fn add_errors(&mut self, prefix
: &str, err
: Error
) {
54 match err
.downcast
::<ParameterError
>() {
59 .map(|(key
, err
)| (format
!("{}/{}", prefix
, key
), err
)),
62 Err(err
) => self.push(prefix
.to_string(), err
),
67 impl fmt
::Display
for ParameterError
{
68 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
69 let mut msg
= String
::new();
72 msg
.push_str("parameter verification errors\n\n");
75 for (name
, err
) in self.error_list
.iter() {
76 msg
.push_str(&format
!("parameter '{}': {}\n", name
, err
));
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);
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))
97 impl std
::iter
::Extend
<(String
, Error
)> for ParameterError
{
98 fn extend
<T
>(&mut self, iter
: T
)
100 T
: IntoIterator
<Item
= (String
, Error
)>,
102 self.error_list
.extend(iter
);
106 impl<'a
> std
::iter
::Extend
<(&'a
str, Error
)> for ParameterError
{
107 fn extend
<T
>(&mut self, iter
: T
)
109 T
: IntoIterator
<Item
= (&'a
str, Error
)>,
111 self.extend(iter
.into_iter().map(|(s
, e
)| (s
.to_string(), e
)));
115 impl IntoIterator
for ParameterError
{
116 type Item
= (String
, Error
);
117 type IntoIter
= <Vec
<(String
, Error
)> as IntoIterator
>::IntoIter
;
119 fn into_iter(self) -> Self::IntoIter
{
120 self.into_inner().into_iter()
124 impl FromIterator
<(String
, Error
)> for ParameterError
{
125 fn from_iter
<T
>(iter
: T
) -> Self
127 T
: IntoIterator
<Item
= (String
, Error
)>,
129 let mut this
= Self::new();
135 impl<'a
> FromIterator
<(&'a
str, Error
)> for ParameterError
{
136 fn from_iter
<T
>(iter
: T
) -> Self
138 T
: IntoIterator
<Item
= (&'a
str, Error
)>,
140 let mut this
= Self::new();
146 /// Data type to describe boolean values
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
>,
156 pub const fn new(description
: &'
static str) -> Self {
163 pub const fn default(mut self, default: bool
) -> Self {
164 self.default = Some(default);
168 pub const fn schema(self) -> Schema
{
169 Schema
::Boolean(self)
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.");
181 /// Data type to describe integer values.
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>,
195 pub const fn new(description
: &'
static str) -> Self {
204 pub const fn default(mut self, default: isize) -> Self {
205 self.default = Some(default);
209 pub const fn minimum(mut self, minimum
: isize) -> Self {
210 self.minimum
= Some(minimum
);
214 pub const fn maximum(mut self, maximium
: isize) -> Self {
215 self.maximum
= Some(maximium
);
219 pub const fn schema(self) -> Schema
{
220 Schema
::Integer(self)
223 fn check_constraints(&self, value
: isize) -> Result
<(), Error
> {
224 if let Some(minimum
) = self.minimum
{
227 "value must have a minimum value of {} (got {})",
234 if let Some(maximum
) = self.maximum
{
237 "value must have a maximum value of {} (got {})",
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)
252 bail
!("Expected integer value.");
257 /// Data type to describe (JSON like) number value
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>,
270 pub const fn new(description
: &'
static str) -> Self {
279 pub const fn default(mut self, default: f64) -> Self {
280 self.default = Some(default);
284 pub const fn minimum(mut self, minimum
: f64) -> Self {
285 self.minimum
= Some(minimum
);
289 pub const fn maximum(mut self, maximium
: f64) -> Self {
290 self.maximum
= Some(maximium
);
294 pub const fn schema(self) -> Schema
{
298 fn check_constraints(&self, value
: f64) -> Result
<(), Error
> {
299 if let Some(minimum
) = self.minimum
{
302 "value must have a minimum value of {} (got {})",
309 if let Some(maximum
) = self.maximum
{
312 "value must have a maximum value of {} (got {})",
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
)
327 bail
!("Expected number value.");
332 #[cfg(feature = "test-harness")]
333 impl Eq
for NumberSchema {}
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
{
340 (None
, None
) => true,
341 (Some(l
), Some(r
)) => (l
- r
).abs() < 0.0001,
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)
353 /// Data type to describe string values.
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>,
371 pub const fn new(description
: &'
static str) -> Self {
382 pub const fn default(mut self, text
: &'
static str) -> Self {
383 self.default = Some(text
);
387 pub const fn format(mut self, format
: &'
static ApiStringFormat
) -> Self {
388 self.format
= Some(format
);
392 pub const fn type_text(mut self, type_text
: &'
static str) -> Self {
393 self.type_text
= Some(type_text
);
397 pub const fn min_length(mut self, min_length
: usize) -> Self {
398 self.min_length
= Some(min_length
);
402 pub const fn max_length(mut self, max_length
: usize) -> Self {
403 self.max_length
= Some(max_length
);
407 pub const fn schema(self) -> Schema
{
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
);
418 if let Some(max_length
) = self.max_length
{
419 if length
> max_length
{
420 bail
!("value may only be {} characters long", max_length
);
427 pub fn check_constraints(&self, value
: &str) -> Result
<(), Error
> {
428 self.check_length(value
.chars().count())?
;
430 if let Some(ref format
) = self.format
{
432 ApiStringFormat
::Pattern(regex
) => {
433 if !(regex
.regex_obj
)().is_match(value
) {
434 bail
!("value does not match the regex pattern");
437 ApiStringFormat
::Enum(variants
) => {
438 if !variants
.iter().any(|e
| e
.value
== value
) {
439 bail
!("value '{}' is not defined in the enumeration.", value
);
442 ApiStringFormat
::PropertyString(subschema
) => {
443 subschema
.parse_property_string(value
)?
;
445 ApiStringFormat
::VerifyFn(verify_fn
) => {
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
)
459 bail
!("Expected string value.");
464 /// Data type to describe array of values.
466 /// All array elements are of the same type, as defined in the `items`
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>,
481 pub const fn new(description
: &'
static str, item_schema
: &'
static Schema
) -> Self {
490 pub const fn min_length(mut self, min_length
: usize) -> Self {
491 self.min_length
= Some(min_length
);
495 pub const fn max_length(mut self, max_length
: usize) -> Self {
496 self.max_length
= Some(max_length
);
500 pub const fn schema(self) -> Schema
{
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
);
511 if let Some(max_length
) = self.max_length
{
512 if length
> max_length
{
513 bail
!("array may only contain {} elements", max_length
);
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."),
528 self.check_length(list
.len())?
;
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());
541 /// Property entry in an object schema:
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
);
548 /// Lookup table to Schema properties
550 /// Stores a sorted list of `(name, optional, schema)` tuples:
552 /// - `name`: The name of the property
553 /// - `optional`: Set when the property is optional
554 /// - `schema`: Property type schema
556 /// **Note:** The list has to be storted by name, because we use
557 /// a binary search to find items.
559 /// This is a workaround unless RUST can const_fn `Hash::new()`
560 pub type SchemaPropertyMap
= &'
static [SchemaPropertyEntry
];
562 /// Data type to describe objects (maps).
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
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>,
577 pub const fn new(description
: &'
static str, properties
: SchemaPropertyMap
) -> Self {
581 additional_properties
: false,
586 pub const fn additional_properties(mut self, additional_properties
: bool
) -> Self {
587 self.additional_properties
= additional_properties
;
591 pub const fn default_key(mut self, key
: &'
static str) -> Self {
592 self.default_key
= Some(key
);
596 pub const fn schema(self) -> Schema
{
600 pub fn lookup(&self, key
: &str) -> Option
<(bool
, &Schema
)> {
601 if let Ok(ind
) = self
603 .binary_search_by_key(&key
, |(name
, _
, _
)| name
)
605 let (_name
, optional
, prop_schema
) = self.properties
[ind
];
606 Some((optional
, prop_schema
))
612 /// Parse key/value pairs and verify with object schema
614 /// - `test_required`: is set, checks if all required properties are
616 pub fn parse_parameter_strings(
618 data
: &[(String
, String
)],
620 ) -> Result
<Value
, ParameterError
> {
621 ParameterSchema
::from(self).parse_parameter_strings(data
, test_required
)
625 /// Combines multiple *object* schemas into one.
627 /// Note that these are limited to object schemas. Other schemas will produce errors.
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.
632 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
633 pub struct AllOfSchema
{
634 pub description
: &'
static str,
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
],
642 pub const fn new(description
: &'
static str, list
: &'
static [&'
static Schema
]) -> Self {
643 Self { description, list }
646 pub const fn schema(self) -> Schema
{
650 pub fn lookup(&self, key
: &str) -> Option
<(bool
, &Schema
)> {
651 for entry
in self.list
{
653 Schema
::AllOf(s
) => {
654 if let Some(v
) = s
.lookup(key
) {
658 Schema
::Object(s
) => {
659 if let Some(v
) = s
.lookup(key
) {
663 _
=> panic
!("non-object-schema in `AllOfSchema`"),
670 /// Parse key/value pairs and verify with object schema
672 /// - `test_required`: is set, checks if all required properties are
674 pub fn parse_parameter_strings(
676 data
: &[(String
, String
)],
678 ) -> Result
<Value
, ParameterError
> {
679 ParameterSchema
::from(self).parse_parameter_strings(data
, test_required
)
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
;
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."),
698 let mut errors
= ParameterError
::new();
700 let additional_properties
= self.additional_properties();
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
);
707 } else if !additional_properties
{
710 format_err
!("schema does not allow additional properties."),
715 for (name
, optional
, _prop_schema
) in self.properties() {
716 if !(*optional
) && data
[name
] == Value
::Null
{
719 format_err
!("property is missing and it is not optional."),
724 if !errors
.is_empty() {
732 impl ObjectSchemaType
for ObjectSchema
{
733 fn description(&self) -> &'
static str {
737 fn lookup(&self, key
: &str) -> Option
<(bool
, &Schema
)> {
738 ObjectSchema
::lookup(self, key
)
741 fn properties(&self) -> ObjectPropertyIterator
{
742 ObjectPropertyIterator
{
744 properties
: Some(self.properties
.iter()),
749 fn additional_properties(&self) -> bool
{
750 self.additional_properties
754 impl ObjectSchemaType
for AllOfSchema
{
755 fn description(&self) -> &'
static str {
759 fn lookup(&self, key
: &str) -> Option
<(bool
, &Schema
)> {
760 AllOfSchema
::lookup(self, key
)
763 fn properties(&self) -> ObjectPropertyIterator
{
764 ObjectPropertyIterator
{
765 schemas
: self.list
.iter(),
771 fn additional_properties(&self) -> bool
{
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
>>,
783 impl Iterator
for ObjectPropertyIterator
{
784 type Item
= &'
static SchemaPropertyEntry
;
786 fn next(&mut self) -> Option
<&'
static SchemaPropertyEntry
> {
788 match self.nested
.as_mut().and_then(Iterator
::next
) {
789 Some(item
) => return Some(item
),
790 None
=> self.nested
= None
,
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()),
799 self.properties
= None
;
808 /// Schemas are used to describe complex data types.
810 /// All schema types implement constant builder methods, and a final
811 /// `schema()` method to convert them into a `Schema`.
814 /// use proxmox_schema::{Schema, BooleanSchema, IntegerSchema, ObjectSchema};
816 /// const SIMPLE_OBJECT: Schema = ObjectSchema::new(
817 /// "A very simple object with 2 properties",
818 /// &[ // this arrays needs to be storted by name!
821 /// false /* required */,
822 /// &IntegerSchema::new("A required integer property.")
829 /// true /* optional */,
830 /// &BooleanSchema::new("An optional boolean property.")
838 #[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
841 Boolean(BooleanSchema
),
842 Integer(IntegerSchema
),
843 Number(NumberSchema
),
844 String(StringSchema
),
845 Object(ObjectSchema
),
851 /// Verify JSON value with `schema`.
852 pub fn verify_json(&self, data
: &Value
) -> Result
<(), Error
> {
856 bail
!("Expected Null, but value is not Null.");
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
)?
,
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 {
874 bail
!("internal error - found Null schema.");
876 Schema
::Boolean(_boolean_schema
) => {
877 let res
= parse_boolean(value_str
)?
;
880 Schema
::Integer(integer_schema
) => {
881 let res
: isize = value_str
.parse()?
;
882 integer_schema
.check_constraints(res
)?
;
883 Value
::Number(res
.into())
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())
890 Schema
::String(string_schema
) => {
891 string_schema
.check_constraints(value_str
)?
;
892 Value
::String(value_str
.into())
894 _
=> bail
!("unable to parse complex (sub) objects."),
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
>>(
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())
912 for key_val
in key_val_list
{
913 let kv
: Vec
<&str> = key_val
.splitn(2, '
='
).collect();
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()));
919 bail
!("Value without key, but schema does not define a default key.");
925 .parse_parameter_strings(¶m_list
, true)
926 .map_err(Error
::from
)
930 Schema
::Object(object_schema
) => {
931 parse_object(value_str
, object_schema
, object_schema
.default_key
)
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())
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
),
947 array_schema
.check_length(array
.len())?
;
951 _
=> bail
!("Got unexpected schema type."),
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,
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 }
971 /// String microformat definitions.
973 /// Strings are probably the most flexible data type, and there are
974 /// several ways to define their content.
978 /// Simple list all possible values.
981 /// use proxmox_schema::{ApiStringFormat, EnumEntry};
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"),
989 /// ## Regular Expressions
991 /// Use a regular expression to describe valid strings.
994 /// use proxmox_schema::{const_regex, ApiStringFormat};
997 /// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$";
999 /// const format: ApiStringFormat = ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
1002 /// ## Property Strings
1004 /// Use a schema to describe complex types encoded as string.
1006 /// Arrays are parsed as comma separated lists, i.e: `"1,2,3"`. The
1007 /// list may be sparated by comma, semicolon or whitespace.
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.
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).
1019 /// use proxmox_schema::{ApiStringFormat, ArraySchema, IntegerSchema, Schema, StringSchema};
1020 /// use proxmox_schema::{parse_simple_value, parse_property_string};
1022 /// const PRODUCT_LIST_SCHEMA: Schema =
1023 /// ArraySchema::new("Product List.", &IntegerSchema::new("Product ID").schema())
1027 /// const SCHEMA: Schema = StringSchema::new("A list of Product IDs, comma separated.")
1028 /// .format(&ApiStringFormat::PropertyString(&PRODUCT_LIST_SCHEMA))
1031 /// let res = parse_simple_value("", &SCHEMA);
1032 /// assert!(res.is_err());
1034 /// let res = parse_simple_value("1,2,3", &SCHEMA); // parse as String
1035 /// assert!(res.is_ok());
1037 /// let data = parse_property_string("1,2", &PRODUCT_LIST_SCHEMA); // parse as Array
1038 /// assert!(data.is_ok());
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
>),
1051 impl std
::fmt
::Debug
for ApiStringFormat
{
1052 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
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
),
1062 #[cfg(feature = "test-harness")]
1063 impl Eq
for ApiStringFormat {}
1065 #[cfg(feature = "test-harness")]
1066 impl PartialEq
for ApiStringFormat
{
1067 fn eq(&self, rhs
: &Self) -> bool
{
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
),
1078 /// Parameters are objects, but we have two types of object schemas, the regular one and the
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
),
1087 impl ParameterSchema
{
1088 /// Parse key/value pairs and verify with object schema
1090 /// - `test_required`: is set, checks if all required properties are
1092 pub fn parse_parameter_strings(
1094 data
: &[(String
, String
)],
1095 test_required
: bool
,
1096 ) -> Result
<Value
, ParameterError
> {
1097 do_parse_parameter_strings(self, data
, test_required
)
1101 impl ObjectSchemaType
for ParameterSchema
{
1102 fn description(&self) -> &'
static str {
1104 ParameterSchema
::Object(o
) => o
.description(),
1105 ParameterSchema
::AllOf(o
) => o
.description(),
1109 fn lookup(&self, key
: &str) -> Option
<(bool
, &Schema
)> {
1111 ParameterSchema
::Object(o
) => o
.lookup(key
),
1112 ParameterSchema
::AllOf(o
) => o
.lookup(key
),
1116 fn properties(&self) -> ObjectPropertyIterator
{
1118 ParameterSchema
::Object(o
) => o
.properties(),
1119 ParameterSchema
::AllOf(o
) => o
.properties(),
1123 fn additional_properties(&self) -> bool
{
1125 ParameterSchema
::Object(o
) => o
.additional_properties(),
1126 ParameterSchema
::AllOf(o
) => o
.additional_properties(),
1131 impl From
<&'
static ObjectSchema
> for ParameterSchema
{
1132 fn from(schema
: &'
static ObjectSchema
) -> Self {
1133 ParameterSchema
::Object(schema
)
1137 impl From
<&'
static AllOfSchema
> for ParameterSchema
{
1138 fn from(schema
: &'
static AllOfSchema
) -> Self {
1139 ParameterSchema
::AllOf(schema
)
1143 /// Helper function to parse boolean values
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."),
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
)
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
)
1167 /// Parse key/value pairs and verify with object schema
1169 /// - `test_required`: is set, checks if all required properties are
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
)],
1175 test_required
: bool
,
1176 ) -> Result
<Value
, ParameterError
> {
1177 do_parse_parameter_strings(schema
.into(), data
, test_required
)
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
!({}
);
1187 let mut errors
= ParameterError
::new();
1189 let additional_properties
= schema
.additional_properties();
1191 for (key
, value
) in data
{
1192 if let Some((_optional
, prop_schema
)) = schema
.lookup(key
) {
1194 Schema
::Array(array_schema
) => {
1195 if params
[key
] == Value
::Null
{
1196 params
[key
] = json
!([]);
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
),
1206 errors
.push(key
.into(), format_err
!("expected array - type missmatch"))
1210 _
=> match prop_schema
.parse_simple_value(value
) {
1212 if params
[key
] == Value
::Null
{
1215 errors
.push(key
.into(), format_err
!("duplicate parameter."));
1218 Err(err
) => errors
.push(key
.into(), err
),
1221 } else if additional_properties
{
1224 params
[key
] = Value
::String(value
.to_owned());
1226 Value
::String(ref old
) => {
1227 params
[key
] = Value
::Array(vec
![
1228 Value
::String(old
.to_owned()),
1229 Value
::String(value
.to_owned()),
1232 Value
::Array(ref mut array
) => {
1233 array
.push(Value
::String(value
.to_string()));
1235 _
=> errors
.push(key
.into(), format_err
!("expected array - type missmatch")),
1240 format_err
!("schema does not allow additional properties."),
1245 if test_required
&& errors
.is_empty() {
1246 for (name
, optional
, _prop_schema
) in schema
.properties() {
1247 if !(*optional
) && params
[name
] == Value
::Null
{
1250 format_err
!("parameter is missing and it is not optional."),
1256 if !errors
.is_empty() {
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
)
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
)
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
)
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
)
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
)
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
)
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
)
1305 /// API types should define an "updater type" via this trait in order to support derived "Updater"
1306 /// structs more easily.
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
;
1315 #[cfg(feature = "api-macro")]
1316 pub use proxmox_api_macro
::UpdaterType
;
1318 #[cfg(feature = "api-macro")]
1320 pub use proxmox_api_macro
::Updater
;
1322 macro_rules
! basic_updater_type
{
1325 impl UpdaterType
for $ty
{
1326 type Updater
= Option
<Self>;
1331 basic_updater_type
! { bool u8 u16 u32 u64 i8 i16 i32 i64 usize isize f32 f64 String char }
1333 impl<T
> UpdaterType
for Option
<T
>
1337 type Updater
= T
::Updater
;
1340 // this will replace the whole Vec
1341 impl<T
> UpdaterType
for Vec
<T
> {
1342 type Updater
= Option
<Self>;
1346 const API_SCHEMA
: Schema
;
1349 impl<T
: ApiType
> ApiType
for Option
<T
> {
1350 const API_SCHEMA
: Schema
= T
::API_SCHEMA
;
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
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")]`.
1362 /// Check if the updater is "none" or "empty".
1363 fn is_empty(&self) -> bool
;
1366 impl<T
> Updater
for Vec
<T
> {
1367 fn is_empty(&self) -> bool
{
1372 impl<T
> Updater
for Option
<T
> {
1373 fn is_empty(&self) -> bool
{
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.
1382 /// If true, the return type in pseudo openapi terms would be `"oneOf": [ "null", "T" ]`.
1385 /// The method's return type.
1386 pub schema
: &'
static Schema
,
1389 impl std
::fmt
::Debug
for ReturnType
{
1390 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
1392 write
!(f
, "optional {:?}", self.schema
)
1394 write
!(f
, "{:?}", self.schema
)
1400 pub const fn new(optional
: bool
, schema
: &'
static Schema
) -> Self {
1401 Self { optional, schema }