proxmox/proxmox-schema/src/schema.rs
Wolfgang Bumiller 49b2bdf9c6 schema: drop periods after errors
lower case start + period = wrong

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2024-02-21 12:05:44 +01:00

1853 lines
56 KiB
Rust

//! Data types to decscribe data types.
//!
//! This is loosly based on JSON Schema, but uses static rust data types. This way we can build
//! completely static API definitions that can be included within the programs read-only text
//! segment.
use std::collections::HashSet;
use std::fmt;
use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use crate::ConstRegexPattern;
/// Error type for schema validation
///
/// The validation functions may produce several error message,
/// i.e. when validation objects, it can produce one message for each
/// erroneous object property.
#[derive(Default, Debug)]
pub struct ParameterError {
error_list: Vec<(String, Error)>,
}
/// Like anyhow's `format_err` but producing a `ParameterError`.
#[macro_export]
macro_rules! param_format_err {
($field:expr, $err:expr) => {
$crate::ParameterError::from(($field, $err))
};
($field:expr, $($msg:tt)+) => {
$crate::ParameterError::from(($field, ::anyhow::format_err!($($msg)+)))
};
}
/// Like anyhow's `bail` but enclosing a `ParameterError`, so
/// a `downcast` can extract it later. This is useful for
/// API calls that need to do parameter checking manually.
#[macro_export]
macro_rules! param_bail {
($field:expr, $err:expr) => {{
return Err($crate::param_format_err!($field, $err).into());
}};
($field:expr, $($msg:tt)+) => {{
return Err($crate::param_format_err!($field, $($msg)+).into());
}};
}
impl std::error::Error for ParameterError {}
impl ParameterError {
pub fn new() -> Self {
Self {
error_list: Vec::new(),
}
}
pub fn push(&mut self, name: String, value: Error) {
self.error_list.push((name, value));
}
pub fn len(&self) -> usize {
self.error_list.len()
}
pub fn errors(&self) -> &[(String, Error)] {
&self.error_list
}
pub fn into_inner(self) -> Vec<(String, Error)> {
self.error_list
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn add_errors(&mut self, prefix: &str, err: Error) {
match err.downcast::<ParameterError>() {
Ok(param_err) => {
self.extend(
param_err
.into_iter()
.map(|(key, err)| (format!("{}/{}", prefix, key), err)),
);
}
Err(err) => self.push(prefix.to_string(), err),
}
}
pub(crate) fn from_list(error_list: Vec<(String, Error)>) -> Self {
Self { error_list }
}
}
impl fmt::Display for ParameterError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use std::fmt::Write;
let mut msg = String::new();
if !self.is_empty() {
if self.len() == 1 {
msg.push_str("parameter verification failed - ");
let _ = write!(msg, "'{}': {}", self.error_list[0].0, self.error_list[0].1);
} else {
msg.push_str("parameter verification failed:\n");
for (name, err) in self.error_list.iter() {
let _ = writeln!(msg, "- '{}': {}", name, err);
}
}
}
write!(f, "{}", msg.trim())
}
}
impl From<(String, Error)> for ParameterError {
fn from(err: (String, Error)) -> Self {
let mut this = Self::new();
this.push(err.0, err.1);
this
}
}
impl<'a> From<(&'a str, Error)> for ParameterError {
fn from(err: (&'a str, Error)) -> Self {
Self::from((err.0.to_string(), err.1))
}
}
impl std::iter::Extend<(String, Error)> for ParameterError {
fn extend<T>(&mut self, iter: T)
where
T: IntoIterator<Item = (String, Error)>,
{
self.error_list.extend(iter);
}
}
impl<'a> std::iter::Extend<(&'a str, Error)> for ParameterError {
fn extend<T>(&mut self, iter: T)
where
T: IntoIterator<Item = (&'a str, Error)>,
{
self.extend(iter.into_iter().map(|(s, e)| (s.to_string(), e)));
}
}
impl IntoIterator for ParameterError {
type Item = (String, Error);
type IntoIter = <Vec<(String, Error)> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.into_inner().into_iter()
}
}
impl FromIterator<(String, Error)> for ParameterError {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (String, Error)>,
{
let mut this = Self::new();
this.extend(iter);
this
}
}
impl<'a> FromIterator<(&'a str, Error)> for ParameterError {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (&'a str, Error)>,
{
let mut this = Self::new();
this.extend(iter);
this
}
}
/// Data type to describe boolean values
#[derive(Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct BooleanSchema {
pub description: &'static str,
/// Optional default value.
pub default: Option<bool>,
}
impl BooleanSchema {
pub const fn new(description: &'static str) -> Self {
BooleanSchema {
description,
default: None,
}
}
pub const fn default(mut self, default: bool) -> Self {
self.default = Some(default);
self
}
pub const fn schema(self) -> Schema {
Schema::Boolean(self)
}
/// Verify JSON value using a `BooleanSchema`.
pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
if !data.is_boolean() {
bail!("Expected boolean value.");
}
Ok(())
}
}
/// Data type to describe integer values.
#[derive(Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct IntegerSchema {
pub description: &'static str,
/// Optional minimum.
pub minimum: Option<isize>,
/// Optional maximum.
pub maximum: Option<isize>,
/// Optional default.
pub default: Option<isize>,
}
impl IntegerSchema {
pub const fn new(description: &'static str) -> Self {
IntegerSchema {
description,
default: None,
minimum: None,
maximum: None,
}
}
pub const fn default(mut self, default: isize) -> Self {
self.default = Some(default);
self
}
pub const fn minimum(mut self, minimum: isize) -> Self {
self.minimum = Some(minimum);
self
}
pub const fn maximum(mut self, maximium: isize) -> Self {
self.maximum = Some(maximium);
self
}
pub const fn schema(self) -> Schema {
Schema::Integer(self)
}
pub fn check_constraints(&self, value: isize) -> Result<(), Error> {
if let Some(minimum) = self.minimum {
if value < minimum {
bail!(
"value must have a minimum value of {} (got {})",
minimum,
value
);
}
}
if let Some(maximum) = self.maximum {
if value > maximum {
bail!(
"value must have a maximum value of {} (got {})",
maximum,
value
);
}
}
Ok(())
}
/// Verify JSON value using an `IntegerSchema`.
pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
if let Some(value) = data.as_i64() {
self.check_constraints(value as isize)
} else {
bail!("Expected integer value.");
}
}
}
/// Data type to describe (JSON like) number value
#[derive(Debug)]
pub struct NumberSchema {
pub description: &'static str,
/// Optional minimum.
pub minimum: Option<f64>,
/// Optional maximum.
pub maximum: Option<f64>,
/// Optional default.
pub default: Option<f64>,
}
impl NumberSchema {
pub const fn new(description: &'static str) -> Self {
NumberSchema {
description,
default: None,
minimum: None,
maximum: None,
}
}
pub const fn default(mut self, default: f64) -> Self {
self.default = Some(default);
self
}
pub const fn minimum(mut self, minimum: f64) -> Self {
self.minimum = Some(minimum);
self
}
pub const fn maximum(mut self, maximium: f64) -> Self {
self.maximum = Some(maximium);
self
}
pub const fn schema(self) -> Schema {
Schema::Number(self)
}
pub fn check_constraints(&self, value: f64) -> Result<(), Error> {
if let Some(minimum) = self.minimum {
if value < minimum {
bail!(
"value must have a minimum value of {} (got {})",
minimum,
value
);
}
}
if let Some(maximum) = self.maximum {
if value > maximum {
bail!(
"value must have a maximum value of {} (got {})",
maximum,
value
);
}
}
Ok(())
}
/// Verify JSON value using an `NumberSchema`.
pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
if let Some(value) = data.as_f64() {
self.check_constraints(value)
} else {
bail!("Expected number value.");
}
}
}
#[cfg(feature = "test-harness")]
impl Eq for NumberSchema {}
#[cfg(feature = "test-harness")]
impl PartialEq for NumberSchema {
fn eq(&self, rhs: &Self) -> bool {
fn f64_eq(l: Option<f64>, r: Option<f64>) -> bool {
match (l, r) {
(None, None) => true,
(Some(l), Some(r)) => (l - r).abs() < 0.0001,
_ => false,
}
}
self.description == rhs.description
&& f64_eq(self.minimum, rhs.minimum)
&& f64_eq(self.maximum, rhs.maximum)
&& f64_eq(self.default, rhs.default)
}
}
/// Data type to describe string values.
#[derive(Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct StringSchema {
pub description: &'static str,
/// Optional default value.
pub default: Option<&'static str>,
/// Optional minimal length.
pub min_length: Option<usize>,
/// Optional maximal length.
pub max_length: Option<usize>,
/// Optional microformat.
pub format: Option<&'static ApiStringFormat>,
/// A text representation of the format/type (used to generate documentation).
pub type_text: Option<&'static str>,
}
impl StringSchema {
pub const fn new(description: &'static str) -> Self {
StringSchema {
description,
default: None,
min_length: None,
max_length: None,
format: None,
type_text: None,
}
}
pub const fn default(mut self, text: &'static str) -> Self {
self.default = Some(text);
self
}
pub const fn format(mut self, format: &'static ApiStringFormat) -> Self {
self.format = Some(format);
self
}
pub const fn type_text(mut self, type_text: &'static str) -> Self {
self.type_text = Some(type_text);
self
}
pub const fn min_length(mut self, min_length: usize) -> Self {
self.min_length = Some(min_length);
self
}
pub const fn max_length(mut self, max_length: usize) -> Self {
self.max_length = Some(max_length);
self
}
pub const fn schema(self) -> Schema {
Schema::String(self)
}
pub(crate) fn check_length(&self, length: usize) -> Result<(), Error> {
if let Some(min_length) = self.min_length {
if length < min_length {
bail!("value must be at least {} characters long", min_length);
}
}
if let Some(max_length) = self.max_length {
if length > max_length {
bail!("value may only be {} characters long", max_length);
}
}
Ok(())
}
pub fn check_constraints(&self, value: &str) -> Result<(), Error> {
self.check_length(value.chars().count())?;
if let Some(ref format) = self.format {
match format {
ApiStringFormat::Pattern(regex) => {
if !(regex.regex_obj)().is_match(value) {
bail!("value does not match the regex pattern");
}
}
ApiStringFormat::Enum(variants) => {
if !variants.iter().any(|e| e.value == value) {
bail!("value '{}' is not defined in the enumeration.", value);
}
}
ApiStringFormat::PropertyString(subschema) => {
crate::de::verify::verify(subschema, value)?;
}
ApiStringFormat::VerifyFn(verify_fn) => {
verify_fn(value)?;
}
}
}
Ok(())
}
/// Verify JSON value using this `StringSchema`.
pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
if let Some(value) = data.as_str() {
self.check_constraints(value)
} else {
bail!("Expected string value.");
}
}
/// Get the [`format`](ApiStringFormat), panics if there is no format.
pub const fn unwrap_format(&self) -> &'static ApiStringFormat {
match self.format {
Some(v) => v,
None => panic!("unwrap_format on StringSchema without format"),
}
}
}
/// Data type to describe array of values.
///
/// All array elements are of the same type, as defined in the `items`
/// schema.
#[derive(Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct ArraySchema {
pub description: &'static str,
/// Element type schema.
pub items: &'static Schema,
/// Optional minimal length.
pub min_length: Option<usize>,
/// Optional maximal length.
pub max_length: Option<usize>,
}
impl ArraySchema {
pub const fn new(description: &'static str, item_schema: &'static Schema) -> Self {
ArraySchema {
description,
items: item_schema,
min_length: None,
max_length: None,
}
}
pub const fn min_length(mut self, min_length: usize) -> Self {
self.min_length = Some(min_length);
self
}
pub const fn max_length(mut self, max_length: usize) -> Self {
self.max_length = Some(max_length);
self
}
pub const fn schema(self) -> Schema {
Schema::Array(self)
}
pub(crate) fn check_length(&self, length: usize) -> Result<(), Error> {
if let Some(min_length) = self.min_length {
if length < min_length {
bail!("array must contain at least {} elements", min_length);
}
}
if let Some(max_length) = self.max_length {
if length > max_length {
bail!("array may only contain {} elements", max_length);
}
}
Ok(())
}
/// Verify JSON value using an `ArraySchema`.
pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
let list = match data {
Value::Array(ref list) => list,
Value::Object(_) => bail!("Expected array - got object."),
_ => bail!("Expected array - got scalar value."),
};
self.check_length(list.len())?;
for (i, item) in list.iter().enumerate() {
let result = self.items.verify_json(item);
if let Err(err) = result {
param_bail!(format!("[{}]", i), err);
}
}
Ok(())
}
}
/// Property entry in an object schema:
///
/// - `name`: The name of the property
/// - `optional`: Set when the property is optional
/// - `schema`: Property type schema
pub type SchemaPropertyEntry = (&'static str, bool, &'static Schema);
/// Lookup table to Schema properties
///
/// Stores a sorted list of `(name, optional, schema)` tuples:
///
/// - `name`: The name of the property
/// - `optional`: Set when the property is optional
/// - `schema`: Property type schema
///
/// **Note:** The list has to be storted by name, because we use
/// a binary search to find items.
///
/// This is a workaround unless RUST can const_fn `Hash::new()`
pub type SchemaPropertyMap = &'static [SchemaPropertyEntry];
/// Data type to describe objects (maps).
#[derive(Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct ObjectSchema {
pub description: &'static str,
/// If set, allow additional properties which are not defined in
/// the schema.
pub additional_properties: bool,
/// Property schema definitions.
pub properties: SchemaPropertyMap,
/// Default key name - used by `parse_parameter_string()`
pub default_key: Option<&'static str>,
}
impl ObjectSchema {
pub const fn new(description: &'static str, properties: SchemaPropertyMap) -> Self {
ObjectSchema {
description,
properties,
additional_properties: false,
default_key: None,
}
}
pub const fn additional_properties(mut self, additional_properties: bool) -> Self {
self.additional_properties = additional_properties;
self
}
pub const fn default_key(mut self, key: &'static str) -> Self {
self.default_key = Some(key);
self
}
pub const fn schema(self) -> Schema {
Schema::Object(self)
}
pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
if let Ok(ind) = self
.properties
.binary_search_by_key(&key, |(name, _, _)| name)
{
let (_name, optional, prop_schema) = self.properties[ind];
Some((optional, prop_schema))
} else {
None
}
}
/// Parse key/value pairs and verify with object schema
///
/// - `test_required`: is set, checks if all required properties are
/// present.
pub fn parse_parameter_strings(
&'static self,
data: &[(String, String)],
test_required: bool,
) -> Result<Value, ParameterError> {
ParameterSchema::from(self).parse_parameter_strings(data, test_required)
}
}
/// Combines multiple *object* schemas into one.
///
/// Note that these are limited to object schemas. Other schemas will produce errors.
///
/// Technically this could also contain an `additional_properties` flag, however, in the JSON
/// Schema, this is not supported, so here we simply assume additional properties to be allowed.
#[derive(Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct AllOfSchema {
pub description: &'static str,
/// The parameter is checked against all of the schemas in the list. Note that all schemas must
/// be object schemas.
pub list: &'static [&'static Schema],
}
impl AllOfSchema {
pub const fn new(description: &'static str, list: &'static [&'static Schema]) -> Self {
Self { description, list }
}
pub const fn schema(self) -> Schema {
Schema::AllOf(self)
}
pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
for entry in self.list {
if let Some(v) = entry
.any_object()
.expect("non-object-schema in `AllOfSchema`")
.lookup(key)
{
return Some(v);
}
}
None
}
/// Parse key/value pairs and verify with object schema
///
/// - `test_required`: is set, checks if all required properties are
/// present.
pub fn parse_parameter_strings(
&'static self,
data: &[(String, String)],
test_required: bool,
) -> Result<Value, ParameterError> {
ParameterSchema::from(self).parse_parameter_strings(data, test_required)
}
}
/// An object schema which is basically like a rust enum: exactly one variant may match.
///
/// Contrary to JSON Schema, we require there be a 'type' property to distinguish the types.
/// In serde-language, we use an internally tagged enum representation.
///
/// Note that these are limited to object schemas. Other schemas will produce errors.
#[derive(Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct OneOfSchema {
pub description: &'static str,
/// The type property entry.
///
/// This must be a static reference due to how we implemented the property iterator.
pub type_property_entry: &'static SchemaPropertyEntry,
/// The parameter is checked against all of the schemas in the list. Note that all schemas must
/// be object schemas.
pub list: &'static [(&'static str, &'static Schema)],
}
impl OneOfSchema {
pub const fn new(
description: &'static str,
type_property_entry: &'static SchemaPropertyEntry,
list: &'static [(&'static str, &'static Schema)],
) -> Self {
Self {
description,
type_property_entry,
list,
}
}
pub const fn schema(self) -> Schema {
Schema::OneOf(self)
}
pub fn type_property(&self) -> &'static str {
self.type_property_entry.0
}
pub fn type_schema(&self) -> &'static Schema {
self.type_property_entry.2
}
pub fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
if key == self.type_property() {
return Some((false, self.type_schema()));
}
for (_variant, entry) in self.list {
if let Some(v) = entry
.any_object()
.expect("non-object-schema in `OneOfSchema`")
.lookup(key)
{
return Some(v);
}
}
None
}
pub fn lookup_variant(&self, name: &str) -> Option<&Schema> {
Some(
self.list[self
.list
.binary_search_by_key(&name, |(name, _)| name)
.ok()?]
.1,
)
}
/// Parse key/value pairs and verify with object schema
///
/// - `test_required`: is set, checks if all required properties are
/// present.
pub fn parse_parameter_strings(
&'static self,
data: &[(String, String)],
test_required: bool,
) -> Result<Value, ParameterError> {
ParameterSchema::from(self).parse_parameter_strings(data, test_required)
}
}
/// Beside [`ObjectSchema`] we also have an [`AllOfSchema`] which also represents objects.
pub trait ObjectSchemaType {
fn description(&self) -> &'static str;
fn lookup(&self, key: &str) -> Option<(bool, &Schema)>;
fn properties(&self) -> ObjectPropertyIterator;
fn additional_properties(&self) -> bool;
fn default_key(&self) -> Option<&'static str>;
/// Verify JSON value using an object schema.
fn verify_json(&self, data: &Value) -> Result<(), Error> {
let map = match data {
Value::Object(ref map) => map,
Value::Array(_) => bail!("Expected object - got array."),
_ => bail!("Expected object - got scalar value."),
};
let mut errors = ParameterError::new();
let additional_properties = self.additional_properties();
for (key, value) in map {
if let Some((_optional, prop_schema)) = self.lookup(key) {
if let Err(err) = prop_schema.verify_json(value) {
errors.add_errors(key, err);
};
} else if !additional_properties {
errors.push(
key.to_string(),
format_err!("schema does not allow additional properties"),
);
}
}
for (name, optional, _prop_schema) in self.properties() {
if !(*optional) && data[name] == Value::Null {
errors.push(
name.to_string(),
format_err!("property is missing and it is not optional"),
);
}
}
if !errors.is_empty() {
Err(errors.into())
} else {
Ok(())
}
}
}
#[doc(hidden)]
pub enum ObjectPropertyIterator {
Simple(SimpleObjectPropertyIterator),
OneOf(OneOfPropertyIterator),
}
impl Iterator for ObjectPropertyIterator {
type Item = &'static SchemaPropertyEntry;
fn next(&mut self) -> Option<&'static SchemaPropertyEntry> {
match self {
Self::Simple(iter) => iter.next(),
Self::OneOf(iter) => iter.next(),
}
}
}
impl ObjectSchemaType for ObjectSchema {
fn description(&self) -> &'static str {
self.description
}
fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
ObjectSchema::lookup(self, key)
}
fn properties(&self) -> ObjectPropertyIterator {
ObjectPropertyIterator::Simple(SimpleObjectPropertyIterator {
schemas: [].iter(),
properties: Some(self.properties.iter()),
nested: None,
})
}
fn additional_properties(&self) -> bool {
self.additional_properties
}
fn default_key(&self) -> Option<&'static str> {
self.default_key
}
}
impl ObjectSchemaType for AllOfSchema {
fn description(&self) -> &'static str {
self.description
}
fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
AllOfSchema::lookup(self, key)
}
fn properties(&self) -> ObjectPropertyIterator {
ObjectPropertyIterator::Simple(SimpleObjectPropertyIterator {
schemas: self.list.iter(),
properties: None,
nested: None,
})
}
fn additional_properties(&self) -> bool {
self.list.iter().any(|schema| {
schema
.any_object()
.expect("non-object-schema in `AllOfSchema`")
.additional_properties()
})
}
fn default_key(&self) -> Option<&'static str> {
for schema in self.list {
let default_key = schema
.any_object()
.expect("non-object-schema in `AllOfSchema`")
.default_key();
if default_key.is_some() {
return default_key;
}
}
None
}
}
#[doc(hidden)]
pub struct SimpleObjectPropertyIterator {
schemas: std::slice::Iter<'static, &'static Schema>,
properties: Option<std::slice::Iter<'static, SchemaPropertyEntry>>,
nested: Option<Box<ObjectPropertyIterator>>,
}
impl Iterator for SimpleObjectPropertyIterator {
type Item = &'static SchemaPropertyEntry;
fn next(&mut self) -> Option<&'static SchemaPropertyEntry> {
loop {
match self.nested.as_mut().and_then(Iterator::next) {
Some(item) => return Some(item),
None => self.nested = None,
}
match self.properties.as_mut().and_then(Iterator::next) {
Some(item) => return Some(item),
None => match self.schemas.next()? {
Schema::AllOf(o) => self.nested = Some(Box::new(o.properties())),
Schema::OneOf(o) => self.nested = Some(Box::new(o.properties())),
Schema::Object(o) => self.properties = Some(o.properties.iter()),
_ => {
self.properties = None;
continue;
}
},
}
}
}
}
impl ObjectSchemaType for OneOfSchema {
fn description(&self) -> &'static str {
self.description
}
fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
OneOfSchema::lookup(self, key)
}
fn properties(&self) -> ObjectPropertyIterator {
ObjectPropertyIterator::OneOf(OneOfPropertyIterator {
type_property_entry: self.type_property_entry,
schemas: self.list.iter(),
done: HashSet::new(),
nested: None,
})
}
fn additional_properties(&self) -> bool {
self.list.iter().any(|(_, schema)| {
schema
.any_object()
.expect("non-object-schema in `OneOfSchema`")
.additional_properties()
})
}
fn default_key(&self) -> Option<&'static str> {
None
}
fn verify_json(&self, data: &Value) -> Result<(), Error> {
let map = match data {
Value::Object(ref map) => map,
Value::Array(_) => bail!("Expected object - got array."),
_ => bail!("Expected object - got scalar value."),
};
// Without the type we also cannot verify anything else...:
let variant = match map.get(self.type_property()) {
None => bail!("Missing '{}' property", self.type_property()),
Some(Value::String(v)) => v,
_ => bail!("Expected string in '{}'", self.type_property()),
};
let schema = self
.lookup_variant(variant)
.ok_or_else(|| format_err!("invalid '{}': {}", self.type_property(), variant))?;
schema.verify_json(data)
}
}
#[doc(hidden)]
pub struct OneOfPropertyIterator {
type_property_entry: &'static SchemaPropertyEntry,
schemas: std::slice::Iter<'static, (&'static str, &'static Schema)>,
done: HashSet<&'static str>,
nested: Option<Box<ObjectPropertyIterator>>,
}
impl Iterator for OneOfPropertyIterator {
type Item = &'static SchemaPropertyEntry;
fn next(&mut self) -> Option<&'static SchemaPropertyEntry> {
if self.done.insert(self.type_property_entry.0) {
return Some(self.type_property_entry);
}
loop {
match self.nested.as_mut().and_then(Iterator::next) {
Some(item) => {
if !self.done.insert(item.0) {
continue;
}
return Some(item);
}
None => self.nested = None,
}
self.nested = Some(Box::new(
self.schemas
.next()?
.1
.any_object()
.expect("non-object-schema in `OneOfSchema`")
.properties(),
));
}
}
}
/// Schemas are used to describe complex data types.
///
/// All schema types implement constant builder methods, and a final
/// `schema()` method to convert them into a `Schema`.
///
/// ```
/// use proxmox_schema::{Schema, BooleanSchema, IntegerSchema, ObjectSchema};
///
/// const SIMPLE_OBJECT: Schema = ObjectSchema::new(
/// "A very simple object with 2 properties",
/// &[ // this arrays needs to be storted by name!
/// (
/// "property_one",
/// false /* required */,
/// &IntegerSchema::new("A required integer property.")
/// .minimum(0)
/// .maximum(100)
/// .schema()
/// ),
/// (
/// "property_two",
/// true /* optional */,
/// &BooleanSchema::new("An optional boolean property.")
/// .default(true)
/// .schema()
/// ),
/// ],
/// ).schema();
/// ```
#[derive(Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub enum Schema {
Null,
Boolean(BooleanSchema),
Integer(IntegerSchema),
Number(NumberSchema),
String(StringSchema),
Object(ObjectSchema),
Array(ArraySchema),
AllOf(AllOfSchema),
OneOf(OneOfSchema),
}
impl Schema {
/// Verify JSON value with `schema`.
pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
match self {
Schema::Null => {
if !data.is_null() {
bail!("Expected Null, but value is not Null.");
}
}
Schema::Object(s) => s.verify_json(data)?,
Schema::Array(s) => s.verify_json(data)?,
Schema::Boolean(s) => s.verify_json(data)?,
Schema::Integer(s) => s.verify_json(data)?,
Schema::Number(s) => s.verify_json(data)?,
Schema::String(s) => s.verify_json(data)?,
Schema::AllOf(s) => s.verify_json(data)?,
Schema::OneOf(s) => s.verify_json(data)?,
}
Ok(())
}
/// Parse a simple value (no arrays and no objects)
pub fn parse_simple_value(&self, value_str: &str) -> Result<Value, Error> {
let value = match self {
Schema::Null => {
bail!("internal error - found Null schema.");
}
Schema::Boolean(_boolean_schema) => {
let res = parse_boolean(value_str)?;
Value::Bool(res)
}
Schema::Integer(integer_schema) => {
let res: isize = value_str.parse()?;
integer_schema.check_constraints(res)?;
Value::Number(res.into())
}
Schema::Number(number_schema) => {
let res: f64 = value_str.parse()?;
number_schema.check_constraints(res)?;
Value::Number(serde_json::Number::from_f64(res).unwrap())
}
Schema::String(string_schema) => {
string_schema.check_constraints(value_str)?;
Value::String(value_str.into())
}
_ => bail!("unable to parse complex (sub) objects."),
};
Ok(value)
}
/// Parse a complex property string (`ApiStringFormat::PropertyString`)
pub fn parse_property_string(&'static self, value_str: &str) -> Result<Value, Error> {
// helper for object/allof schemas:
fn parse_object<T: Into<ParameterSchema>>(
value_str: &str,
schema: T,
default_key: Option<&'static str>,
) -> Result<Value, Error> {
let mut param_list = Vec::new();
for entry in crate::property_string::PropertyIterator::new(value_str) {
let (key, value) = entry?;
match key {
Some(key) => param_list.push((key.to_string(), value.into_owned())),
None => {
if let Some(key) = default_key {
param_list.push((key.to_string(), value.into_owned()));
} else {
bail!("Value without key, but schema does not define a default key.");
}
}
}
}
schema
.into()
.parse_parameter_strings(&param_list, true)
.map_err(Error::from)
}
match self {
Schema::Object(object_schema) => {
parse_object(value_str, object_schema, object_schema.default_key)
}
Schema::AllOf(all_of_schema) => parse_object(value_str, all_of_schema, None),
Schema::Array(array_schema) => {
let mut array: Vec<Value> = vec![];
let list: Vec<&str> = value_str
.split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
.filter(|s| !s.is_empty())
.collect();
for value in list {
match array_schema.items.parse_simple_value(value.trim()) {
Ok(res) => array.push(res),
Err(err) => bail!("unable to parse array element: {}", err),
}
}
array_schema.check_length(array.len())?;
Ok(array.into())
}
_ => bail!("Got unexpected schema type."),
}
}
/// Gets the underlying [`BooleanSchema`], panics on different schemas.
pub const fn unwrap_boolean_schema(&self) -> &BooleanSchema {
match self {
Schema::Boolean(s) => s,
_ => panic!("unwrap_boolean_schema on different schema"),
}
}
/// Gets the underlying [`IntegerSchema`], panics on different schemas.
pub const fn unwrap_integer_schema(&self) -> &IntegerSchema {
match self {
Schema::Integer(s) => s,
_ => panic!("unwrap_integer_schema on different schema"),
}
}
/// Gets the underlying [`NumberSchema`], panics on different schemas.
pub const fn unwrap_number_schema(&self) -> &NumberSchema {
match self {
Schema::Number(s) => s,
_ => panic!("unwrap_number_schema on different schema"),
}
}
/// Gets the underlying [`StringSchema`], panics on different schemas.
pub const fn unwrap_string_schema(&self) -> &StringSchema {
match self {
Schema::String(s) => s,
_ => panic!("unwrap_string_schema on different schema"),
}
}
/// Gets the underlying [`ObjectSchema`], panics on different schemas.
pub const fn unwrap_object_schema(&self) -> &ObjectSchema {
match self {
Schema::Object(s) => s,
_ => panic!("unwrap_object_schema on different schema"),
}
}
/// Gets the underlying [`ArraySchema`], panics on different schemas.
pub const fn unwrap_array_schema(&self) -> &ArraySchema {
match self {
Schema::Array(s) => s,
_ => panic!("unwrap_array_schema on different schema"),
}
}
/// Gets the underlying [`AllOfSchema`], panics on different schemas.
pub const fn unwrap_all_of_schema(&self) -> &AllOfSchema {
match self {
Schema::AllOf(s) => s,
_ => panic!("unwrap_all_of_schema on different schema"),
}
}
/// Gets the underlying [`OneOfSchema`], panics on different schemas.
pub const fn unwrap_one_of_schema(&self) -> &OneOfSchema {
match self {
Schema::OneOf(s) => s,
_ => panic!("unwrap_one_of_schema on different schema"),
}
}
/// Gets the underlying [`BooleanSchema`].
pub const fn boolean(&self) -> Option<&BooleanSchema> {
match self {
Schema::Boolean(s) => Some(s),
_ => None,
}
}
/// Gets the underlying [`IntegerSchema`].
pub const fn integer(&self) -> Option<&IntegerSchema> {
match self {
Schema::Integer(s) => Some(s),
_ => None,
}
}
/// Gets the underlying [`NumberSchema`].
pub const fn number(&self) -> Option<&NumberSchema> {
match self {
Schema::Number(s) => Some(s),
_ => None,
}
}
/// Gets the underlying [`StringSchema`].
pub const fn string(&self) -> Option<&StringSchema> {
match self {
Schema::String(s) => Some(s),
_ => None,
}
}
/// Gets the underlying [`ObjectSchema`].
pub const fn object(&self) -> Option<&ObjectSchema> {
match self {
Schema::Object(s) => Some(s),
_ => None,
}
}
/// Gets the underlying [`ArraySchema`].
pub const fn array(&self) -> Option<&ArraySchema> {
match self {
Schema::Array(s) => Some(s),
_ => None,
}
}
/// Gets the underlying [`AllOfSchema`].
pub const fn all_of(&self) -> Option<&AllOfSchema> {
match self {
Schema::AllOf(s) => Some(s),
_ => None,
}
}
/// Gets the underlying [`AllOfSchema`].
pub const fn one_of(&self) -> Option<&OneOfSchema> {
match self {
Schema::OneOf(s) => Some(s),
_ => None,
}
}
pub fn any_object(&self) -> Option<&dyn ObjectSchemaType> {
match self {
Schema::Object(s) => Some(s),
Schema::AllOf(s) => Some(s),
Schema::OneOf(s) => Some(s),
_ => None,
}
}
}
/// A string enum entry. An enum entry must have a value and a description.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct EnumEntry {
pub value: &'static str,
pub description: &'static str,
}
impl EnumEntry {
/// Convenience method as long as we only have 2 mandatory fields in an `EnumEntry`.
pub const fn new(value: &'static str, description: &'static str) -> Self {
Self { value, description }
}
}
/// String microformat definitions.
///
/// Strings are probably the most flexible data type, and there are
/// several ways to define their content.
///
/// ## Enumerations
///
/// Simple list all possible values.
///
/// ```
/// use proxmox_schema::{ApiStringFormat, EnumEntry};
///
/// const format: ApiStringFormat = ApiStringFormat::Enum(&[
/// EnumEntry::new("vm", "A guest VM run via qemu"),
/// EnumEntry::new("ct", "A guest container run via lxc"),
/// ]);
/// ```
///
/// ## Regular Expressions
///
/// Use a regular expression to describe valid strings.
///
/// ```
/// use proxmox_schema::{const_regex, ApiStringFormat};
///
/// const_regex! {
/// pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$";
/// }
/// const format: ApiStringFormat = ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
/// ```
///
/// ## Property Strings
///
/// Use a schema to describe complex types encoded as string.
///
/// Arrays are parsed as comma separated lists, i.e: `"1,2,3"`. The
/// list may be sparated by comma, semicolon or whitespace.
///
/// Objects are parsed as comma (or semicolon) separated `key=value` pairs, i.e:
/// `"prop1=2,prop2=test"`. Any whitespace is trimmed from key and value.
///
///
/// **Note:** This only works for types which does not allow using the
/// comma, semicolon or whitespace separator inside the value,
/// i.e. this only works for arrays of simple data types, and objects
/// with simple properties (no nesting).
///
/// ```
/// use proxmox_schema::{ApiStringFormat, ArraySchema, IntegerSchema, Schema, StringSchema};
/// use proxmox_schema::{parse_simple_value, parse_property_string};
///
/// const PRODUCT_LIST_SCHEMA: Schema =
/// ArraySchema::new("Product List.", &IntegerSchema::new("Product ID").schema())
/// .min_length(1)
/// .schema();
///
/// const SCHEMA: Schema = StringSchema::new("A list of Product IDs, comma separated.")
/// .format(&ApiStringFormat::PropertyString(&PRODUCT_LIST_SCHEMA))
/// .schema();
///
/// let res = parse_simple_value("", &SCHEMA);
/// assert!(res.is_err());
///
/// let res = parse_simple_value("1,2,3", &SCHEMA); // parse as String
/// assert!(res.is_ok());
///
/// let data = parse_property_string("1,2", &PRODUCT_LIST_SCHEMA); // parse as Array
/// assert!(data.is_ok());
/// ```
pub enum ApiStringFormat {
/// Enumerate all valid strings
Enum(&'static [EnumEntry]),
/// Use a regular expression to describe valid strings.
Pattern(&'static ConstRegexPattern),
/// Use a schema to describe complex types encoded as string.
PropertyString(&'static Schema),
/// Use a verification function.
VerifyFn(ApiStringVerifyFn),
}
/// Type of a verification function for [`StringSchema`]s.
pub type ApiStringVerifyFn = fn(&str) -> Result<(), Error>;
impl ApiStringFormat {
/// Gets the underlying [`&[EnumEntry]`](EnumEntry) list, panics on different formats.
pub const fn unwrap_enum_format(&self) -> &'static [EnumEntry] {
match self {
ApiStringFormat::Enum(v) => v,
_ => panic!("unwrap_enum_format on a different ApiStringFormat"),
}
}
/// Gets the underlying [`&ConstRegexPattern`](ConstRegexPattern), panics on different formats.
pub const fn unwrap_pattern_format(&self) -> &'static ConstRegexPattern {
match self {
ApiStringFormat::Pattern(v) => v,
_ => panic!("unwrap_pattern_format on a different ApiStringFormat"),
}
}
/// Gets the underlying property [`&Schema`](Schema), panics on different formats.
pub const fn unwrap_property_string_format(&self) -> &'static Schema {
match self {
ApiStringFormat::PropertyString(v) => v,
_ => panic!("unwrap_property_string_format on a different ApiStringFormat"),
}
}
}
impl std::fmt::Debug for ApiStringFormat {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ApiStringFormat::VerifyFn(fnptr) => write!(f, "VerifyFn({:p}", fnptr),
ApiStringFormat::Enum(variants) => write!(f, "Enum({:?}", variants),
ApiStringFormat::Pattern(regex) => write!(f, "Pattern({:?}", regex),
ApiStringFormat::PropertyString(schema) => write!(f, "PropertyString({:?}", schema),
}
}
}
#[cfg(feature = "test-harness")]
impl Eq for ApiStringFormat {}
#[cfg(feature = "test-harness")]
impl PartialEq for ApiStringFormat {
fn eq(&self, rhs: &Self) -> bool {
match (self, rhs) {
(ApiStringFormat::Enum(l), ApiStringFormat::Enum(r)) => l == r,
(ApiStringFormat::Pattern(l), ApiStringFormat::Pattern(r)) => l == r,
(ApiStringFormat::PropertyString(l), ApiStringFormat::PropertyString(r)) => l == r,
(ApiStringFormat::VerifyFn(l), ApiStringFormat::VerifyFn(r)) => std::ptr::eq(l, r),
(_, _) => false,
}
}
}
/// Parameters are objects, but we have two types of object schemas, the regular one and the
/// `AllOf` schema.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub enum ParameterSchema {
Object(&'static ObjectSchema),
AllOf(&'static AllOfSchema),
OneOf(&'static OneOfSchema),
}
impl ParameterSchema {
/// Parse key/value pairs and verify with object schema
///
/// - `test_required`: is set, checks if all required properties are
/// present.
pub fn parse_parameter_strings(
self,
data: &[(String, String)],
test_required: bool,
) -> Result<Value, ParameterError> {
do_parse_parameter_strings(self, data, test_required)
}
}
impl ObjectSchemaType for ParameterSchema {
fn description(&self) -> &'static str {
match self {
ParameterSchema::Object(o) => o.description(),
ParameterSchema::AllOf(o) => o.description(),
ParameterSchema::OneOf(o) => o.description(),
}
}
fn lookup(&self, key: &str) -> Option<(bool, &Schema)> {
match self {
ParameterSchema::Object(o) => o.lookup(key),
ParameterSchema::AllOf(o) => o.lookup(key),
ParameterSchema::OneOf(o) => o.lookup(key),
}
}
fn properties(&self) -> ObjectPropertyIterator {
match self {
ParameterSchema::Object(o) => o.properties(),
ParameterSchema::AllOf(o) => o.properties(),
ParameterSchema::OneOf(o) => o.properties(),
}
}
fn additional_properties(&self) -> bool {
match self {
ParameterSchema::Object(o) => o.additional_properties(),
ParameterSchema::AllOf(o) => o.additional_properties(),
ParameterSchema::OneOf(o) => o.additional_properties(),
}
}
fn default_key(&self) -> Option<&'static str> {
match self {
ParameterSchema::Object(o) => o.default_key(),
ParameterSchema::AllOf(o) => o.default_key(),
ParameterSchema::OneOf(o) => o.default_key(),
}
}
}
impl From<&'static ObjectSchema> for ParameterSchema {
fn from(schema: &'static ObjectSchema) -> Self {
ParameterSchema::Object(schema)
}
}
impl From<&'static AllOfSchema> for ParameterSchema {
fn from(schema: &'static AllOfSchema) -> Self {
ParameterSchema::AllOf(schema)
}
}
impl From<&'static OneOfSchema> for ParameterSchema {
fn from(schema: &'static OneOfSchema) -> Self {
ParameterSchema::OneOf(schema)
}
}
/// Helper function to parse boolean values
///
/// - true: `1 | on | yes | true`
/// - false: `0 | off | no | false`
pub fn parse_boolean(value_str: &str) -> Result<bool, Error> {
match value_str.to_lowercase().as_str() {
"1" | "on" | "yes" | "true" => Ok(true),
"0" | "off" | "no" | "false" => Ok(false),
_ => bail!("Unable to parse boolean option."),
}
}
/// Parse a complex property string (`ApiStringFormat::PropertyString`)
#[deprecated(note = "this is now a method of Schema")]
pub fn parse_property_string(value_str: &str, schema: &'static Schema) -> Result<Value, Error> {
schema.parse_property_string(value_str)
}
/// Parse a simple value (no arrays and no objects)
#[deprecated(note = "this is now a method of Schema")]
pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result<Value, Error> {
schema.parse_simple_value(value_str)
}
/// Parse key/value pairs and verify with object schema
///
/// - `test_required`: is set, checks if all required properties are
/// present.
#[deprecated(note = "this is now a method of parameter schema types")]
pub fn parse_parameter_strings<T: Into<ParameterSchema>>(
data: &[(String, String)],
schema: T,
test_required: bool,
) -> Result<Value, ParameterError> {
do_parse_parameter_strings(schema.into(), data, test_required)
}
fn do_parse_parameter_strings(
schema: ParameterSchema,
data: &[(String, String)],
test_required: bool,
) -> Result<Value, ParameterError> {
let mut params = json!({});
let mut errors = ParameterError::new();
let additional_properties = schema.additional_properties();
for (key, value) in data {
if let Some((_optional, prop_schema)) = schema.lookup(key) {
match prop_schema {
Schema::Array(array_schema) => {
if params[key] == Value::Null {
params[key] = json!([]);
}
match params[key] {
Value::Array(ref mut array) => {
match array_schema.items.parse_simple_value(value) {
Ok(res) => array.push(res), // fixme: check_length??
Err(err) => errors.push(key.into(), err),
}
}
_ => {
errors.push(key.into(), format_err!("expected array - type missmatch"))
}
}
}
_ => match prop_schema.parse_simple_value(value) {
Ok(res) => {
if params[key] == Value::Null {
params[key] = res;
} else {
errors.push(key.into(), format_err!("duplicate parameter."));
}
}
Err(err) => errors.push(key.into(), err),
},
}
} else if additional_properties {
match params[key] {
Value::Null => {
params[key] = Value::String(value.to_owned());
}
Value::String(ref old) => {
params[key] = Value::Array(vec![
Value::String(old.to_owned()),
Value::String(value.to_owned()),
]);
}
Value::Array(ref mut array) => {
array.push(Value::String(value.to_string()));
}
_ => errors.push(key.into(), format_err!("expected array - type missmatch")),
}
} else {
errors.push(
key.into(),
format_err!("schema does not allow additional properties."),
);
}
}
if test_required && errors.is_empty() {
for (name, optional, _prop_schema) in schema.properties() {
if !(*optional) && params[name] == Value::Null {
errors.push(
name.to_string(),
format_err!("parameter is missing and it is not optional."),
);
}
}
}
if !errors.is_empty() {
Err(errors)
} else {
Ok(params)
}
}
/// Verify JSON value with `schema`.
#[deprecated(note = "use the method schema.verify_json() instead")]
pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> {
schema.verify_json(data)
}
/// Verify JSON value using a `StringSchema`.
#[deprecated(note = "use the method string_schema.verify_json() instead")]
pub fn verify_json_string(data: &Value, schema: &StringSchema) -> Result<(), Error> {
schema.verify_json(data)
}
/// Verify JSON value using a `BooleanSchema`.
#[deprecated(note = "use the method boolean_schema.verify_json() instead")]
pub fn verify_json_boolean(data: &Value, schema: &BooleanSchema) -> Result<(), Error> {
schema.verify_json(data)
}
/// Verify JSON value using an `IntegerSchema`.
#[deprecated(note = "use the method integer_schema.verify_json() instead")]
pub fn verify_json_integer(data: &Value, schema: &IntegerSchema) -> Result<(), Error> {
schema.verify_json(data)
}
/// Verify JSON value using an `NumberSchema`.
#[deprecated(note = "use the method number_schema.verify_json() instead")]
pub fn verify_json_number(data: &Value, schema: &NumberSchema) -> Result<(), Error> {
schema.verify_json(data)
}
/// Verify JSON value using an `ArraySchema`.
#[deprecated(note = "use the method array_schema.verify_json() instead")]
pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error> {
schema.verify_json(data)
}
/// Verify JSON value using an `ObjectSchema`.
#[deprecated(note = "use the verify_json() method via the ObjectSchemaType trait instead")]
pub fn verify_json_object(data: &Value, schema: &dyn ObjectSchemaType) -> Result<(), Error> {
schema.verify_json(data)
}
/// API types should define an "updater type" via this trait in order to support derived "Updater"
/// structs more easily.
///
/// Most trivial types can simply use an `Option<Self>` as updater. For types which do not use the
/// `#[api]` macro, this will need to be explicitly created (or derived via
/// `#[derive(UpdaterType)]`.
pub trait UpdaterType: Sized {
type Updater: Updater;
}
#[cfg(feature = "api-macro")]
pub use proxmox_api_macro::UpdaterType;
#[cfg(feature = "api-macro")]
#[doc(hidden)]
pub use proxmox_api_macro::Updater;
macro_rules! basic_updater_type {
($($ty:ty)*) => {
$(
impl UpdaterType for $ty {
type Updater = Option<Self>;
}
)*
};
}
basic_updater_type! { bool u8 u16 u32 u64 i8 i16 i32 i64 usize isize f32 f64 String char }
impl<T> UpdaterType for Option<T>
where
T: UpdaterType,
{
type Updater = T::Updater;
}
// this will replace the whole Vec
impl<T> UpdaterType for Vec<T> {
type Updater = Option<Self>;
}
/// Trait signifying that a type contains an API schema.
pub trait ApiType {
const API_SCHEMA: Schema;
}
impl<T: ApiType> ApiType for Option<T> {
const API_SCHEMA: Schema = T::API_SCHEMA;
}
/// A helper type for "Updater" structs. This trait is *not* implemented for an api "base" type
/// when deriving an `Updater` for it, though the generated *updater* type does implement this
/// trait!
///
/// This trait is mostly to figure out if an updater is empty (iow. it should not be applied).
/// In that, it is useful when a type which should have an updater also has optional fields which
/// should not be serialized. Instead of `#[serde(skip_serializing_if = "Option::is_none")]`, this
/// trait's `is_empty` needs to be used via `#[serde(skip_serializing_if = "Updater::is_empty")]`.
pub trait Updater {
/// Check if the updater is "none" or "empty".
fn is_empty(&self) -> bool;
}
impl<T> Updater for Vec<T> {
fn is_empty(&self) -> bool {
self.is_empty()
}
}
impl<T> Updater for Option<T> {
fn is_empty(&self) -> bool {
self.is_none()
}
}
/// Return type schema. Return types may be any schema and additionally be optional.
#[cfg_attr(feature = "test-harness", derive(Eq, PartialEq))]
pub struct ReturnType {
/// A return type may be optional, meaning the method may return null or some fixed data.
///
/// If true, the return type in pseudo openapi terms would be `"oneOf": [ "null", "T" ]`.
pub optional: bool,
/// The method's return type.
pub schema: &'static Schema,
}
impl std::fmt::Debug for ReturnType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.optional {
write!(f, "optional {:?}", self.schema)
} else {
write!(f, "{:?}", self.schema)
}
}
}
impl ReturnType {
pub const fn new(optional: bool, schema: &'static Schema) -> Self {
Self { optional, schema }
}
}