diff --git a/proxmox-api-macro/src/api.rs b/proxmox-api-macro/src/api.rs index 85ecc51f..ad8bf550 100644 --- a/proxmox-api-macro/src/api.rs +++ b/proxmox-api-macro/src/api.rs @@ -17,6 +17,9 @@ mod structs; pub const INTNAMES: &[&str] = &[ "Integer", "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64", "usize", ]; +pub const NUMBERNAMES: &[&str] = &[ + "Number", "f32", "f64", +]; /// The main `Schema` type. /// @@ -151,6 +154,7 @@ enum SchemaItem { Null, Boolean, Integer, + Number, String, Object(SchemaObject), Array(SchemaArray), @@ -202,6 +206,8 @@ impl SchemaItem { Ok(SchemaItem::Boolean) } else if INTNAMES.iter().any(|n| name == n) { Ok(SchemaItem::Integer) + } else if NUMBERNAMES.iter().any(|n| name == n) { + Ok(SchemaItem::Number) } else if name == "String" { Ok(SchemaItem::String) } else if name == "Object" { @@ -235,6 +241,10 @@ impl SchemaItem { let description = description?; ts.extend(quote! { ::proxmox::api::schema::IntegerSchema::new(#description) }); } + SchemaItem::Number => { + let description = description?; + ts.extend(quote! { ::proxmox::api::schema::NumberSchema::new(#description) }); + } SchemaItem::String => { let description = description?; ts.extend(quote! { ::proxmox::api::schema::StringSchema::new(#description) }); diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs index ecfd5486..12373107 100644 --- a/proxmox-api-macro/src/api/method.rs +++ b/proxmox-api-macro/src/api/method.rs @@ -204,6 +204,9 @@ fn handle_function_signature( } else if super::INTNAMES.iter().any(|n| path.path.is_ident(n)) { schema.item = SchemaItem::Integer; continue; + } else if super::NUMBERNAMES.iter().any(|n| path.path.is_ident(n)) { + schema.item = SchemaItem::Number; + continue; } } _ => (), diff --git a/proxmox-api/src/format.rs b/proxmox-api/src/format.rs index 94d1f256..d012e408 100644 --- a/proxmox-api/src/format.rs +++ b/proxmox-api/src/format.rs @@ -87,6 +87,12 @@ pub fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> S (None, Some(max)) => format!(" (-N - {})", max), _ => String::from(""), }, + Schema::Number(number_schema) => match (number_schema.minimum, number_schema.maximum) { + (Some(min), Some(max)) => format!(" ({} - {})", min, max), + (Some(min), None) => format!(" ({} - N)", min), + (None, Some(max)) => format!(" (-N - {})", max), + _ => String::from(""), + }, Schema::Object(_) => String::from(""), Schema::Array(_) => String::from(""), } @@ -106,6 +112,7 @@ pub fn get_property_description( Schema::String(ref schema) => (schema.description, schema.default.map(|v| v.to_owned())), Schema::Boolean(ref schema) => (schema.description, schema.default.map(|v| v.to_string())), Schema::Integer(ref schema) => (schema.description, schema.default.map(|v| v.to_string())), + Schema::Number(ref schema) => (schema.description, schema.default.map(|v| v.to_string())), Schema::Object(ref schema) => (schema.description, None), Schema::Array(ref schema) => (schema.description, None), }; @@ -211,6 +218,10 @@ fn dump_api_return_schema(schema: &Schema) -> String { let description = wrap_text("", "", schema.description, 80); res.push_str(&description); } + Schema::Number(schema) => { + let description = wrap_text("", "", schema.description, 80); + res.push_str(&description); + } Schema::String(schema) => { let description = wrap_text("", "", schema.description, 80); res.push_str(&description); diff --git a/proxmox-api/src/schema.rs b/proxmox-api/src/schema.rs index 5ad20fe3..de64fad6 100644 --- a/proxmox-api/src/schema.rs +++ b/proxmox-api/src/schema.rs @@ -155,6 +155,73 @@ impl IntegerSchema { } } + +/// Data type to describe (JSON like) number value +#[derive(Debug)] +pub struct NumberSchema { + pub description: &'static str, + /// Optional minimum. + pub minimum: Option, + /// Optional maximum. + pub maximum: Option, + /// Optional default. + pub default: Option, +} + +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) + } + + 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(()) + } +} + /// Data type to describe string values. #[derive(Debug)] pub struct StringSchema { @@ -402,6 +469,7 @@ pub enum Schema { Null, Boolean(BooleanSchema), Integer(IntegerSchema), + Number(NumberSchema), String(StringSchema), Object(ObjectSchema), Array(ArraySchema), @@ -550,6 +618,11 @@ pub fn parse_simple_value(value_str: &str, schema: &Schema) -> Result { + 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()) @@ -683,6 +756,7 @@ pub fn verify_json(data: &Value, schema: &Schema) -> Result<(), Error> { } Schema::Boolean(boolean_schema) => verify_json_boolean(data, &boolean_schema)?, Schema::Integer(integer_schema) => verify_json_integer(data, &integer_schema)?, + Schema::Number(number_schema) => verify_json_number(data, &number_schema)?, Schema::String(string_schema) => verify_json_string(data, &string_schema)?, } Ok(()) @@ -714,6 +788,15 @@ pub fn verify_json_integer(data: &Value, schema: &IntegerSchema) -> Result<(), E } } +/// Verify JSON value using an `NumberSchema`. +pub fn verify_json_number(data: &Value, schema: &NumberSchema) -> Result<(), Error> { + if let Some(value) = data.as_f64() { + schema.check_constraints(value) + } else { + bail!("Expected number value."); + } +} + /// Verify JSON value using an `ArraySchema`. pub fn verify_json_array(data: &Value, schema: &ArraySchema) -> Result<(), Error> { let list = match data {