use crate::common::runtime_expression;
use crate::v2_6::schema::SubSchema;
use crate::validation::{Context, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Parameter {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<SubSchema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext for Parameter {
fn validate_with_context(&self, ctx: &mut Context) {
if let Some(location) = &self.location {
if location.is_empty() {
ctx.error_field("location", "must not be empty");
} else if let Err(err) = runtime_expression::parse(location) {
ctx.error_field(
"location",
format!(
"`{location}` is not a valid runtime expression: {}",
err.message()
),
);
}
}
if let Some(schema) = &self.schema {
ctx.in_field("schema", |ctx| schema.validate_with_context(ctx));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use enumset::EnumSet;
use serde_json::json;
#[test]
fn round_trips_a_parameter_with_a_schema() {
let value = json!({
"description": "Street id",
"schema": { "type": "string", "pattern": "^[0-9]+$" },
"location": "$message.payload#/streetId"
});
let parameter: Parameter = serde_json::from_value(value.clone()).unwrap();
assert!(parameter.schema.is_some());
assert_eq!(serde_json::to_value(¶meter).unwrap(), value);
let mut ctx = Context::with_path(EnumSet::empty(), "#.parameters.streetId");
parameter.validate_with_context(&mut ctx);
assert!(ctx.errors.is_empty(), "got: {:?}", ctx.errors);
}
#[test]
fn a_parameter_schema_may_be_any_json_schema() {
let parameter: Parameter = serde_json::from_value(json!({
"schema": { "type": "integer", "minimum": 1.0 }
}))
.unwrap();
let mut ctx = Context::with_path(EnumSet::empty(), "#.parameters.p");
parameter.validate_with_context(&mut ctx);
assert!(ctx.errors.is_empty());
}
#[test]
fn a_boolean_schema_is_accepted() {
for value in [json!({ "schema": true }), json!({ "schema": false })] {
let parameter: Parameter = serde_json::from_value(value.clone()).unwrap();
assert!(matches!(parameter.schema, Some(SubSchema::Bool(_))));
assert_eq!(serde_json::to_value(¶meter).unwrap(), value);
}
}
#[test]
fn empty_parameter_is_valid() {
let mut ctx = Context::with_path(EnumSet::empty(), "#.parameters.p");
Parameter::default().validate_with_context(&mut ctx);
assert!(ctx.errors.is_empty());
}
#[test]
fn rejects_malformed_and_empty_location() {
for (location, needle) in [
("payload#/id", "is not a valid runtime expression"),
("$message.payload", "is not a valid runtime expression"),
("", "must not be empty"),
] {
let parameter = Parameter {
location: Some(location.to_owned()),
..Default::default()
};
let mut ctx = Context::with_path(EnumSet::empty(), "#.parameters.p");
parameter.validate_with_context(&mut ctx);
assert!(
ctx.errors.iter().any(|e| e.contains(needle)),
"{location:?}: {:?}",
ctx.errors
);
}
}
#[test]
fn nested_schema_errors_carry_their_path() {
let parameter: Parameter =
serde_json::from_value(json!({ "schema": { "minItems": 2, "maxItems": 1 } })).unwrap();
let mut ctx = Context::with_path(EnumSet::empty(), "#.parameters.p");
parameter.validate_with_context(&mut ctx);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("#.parameters.p.schema.minItems")),
"got: {:?}",
ctx.errors
);
}
}