use std::collections::HashMap;
use serde_json::Value;
use thiserror::Error;
use barbacane_compiler::{Parameter, RequestBody};
#[derive(Debug, Error)]
pub enum ValidationError2 {
#[error("missing required parameter '{name}' in {location}")]
MissingRequiredParameter { name: String, location: String },
#[error("invalid parameter '{name}' in {location}: {reason}")]
InvalidParameter {
name: String,
location: String,
reason: String,
},
#[error("missing required request body")]
MissingRequiredBody,
#[error("unsupported content-type: {0}")]
UnsupportedContentType(String),
#[error("invalid request body: {0}")]
InvalidBody(String),
#[error("request body too large: {size} bytes exceeds limit of {limit} bytes")]
BodyTooLarge { size: usize, limit: usize },
#[error("too many headers: {count} exceeds limit of {limit}")]
TooManyHeaders { count: usize, limit: usize },
#[error("URI too long: {length} characters exceeds limit of {limit}")]
UriTooLong { length: usize, limit: usize },
#[error("header '{name}' too large: {size} bytes exceeds limit of {limit} bytes")]
HeaderTooLarge {
name: String,
size: usize,
limit: usize,
},
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ProblemDetails {
#[serde(rename = "type")]
pub error_type: String,
pub title: String,
pub status: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
#[serde(flatten)]
pub extensions: HashMap<String, Value>,
}
impl ProblemDetails {
pub fn validation_error(errors: &[ValidationError2], dev_mode: bool) -> Self {
let mut extensions = HashMap::new();
if dev_mode && !errors.is_empty() {
let error_details: Vec<Value> = errors
.iter()
.map(|e| {
let mut detail = serde_json::Map::new();
match e {
ValidationError2::MissingRequiredParameter { name, location } => {
detail.insert("field".into(), Value::String(name.clone()));
detail.insert("location".into(), Value::String(location.clone()));
detail.insert(
"reason".into(),
Value::String("missing required parameter".into()),
);
}
ValidationError2::InvalidParameter {
name,
location,
reason,
} => {
detail.insert("field".into(), Value::String(name.clone()));
detail.insert("location".into(), Value::String(location.clone()));
detail.insert("reason".into(), Value::String(reason.clone()));
}
ValidationError2::MissingRequiredBody => {
detail.insert("field".into(), Value::String("body".into()));
detail.insert(
"reason".into(),
Value::String("missing required request body".into()),
);
}
ValidationError2::UnsupportedContentType(ct) => {
detail.insert("field".into(), Value::String("content-type".into()));
detail.insert(
"reason".into(),
Value::String(format!("unsupported: {}", ct)),
);
}
ValidationError2::InvalidBody(reason) => {
detail.insert("field".into(), Value::String("body".into()));
detail.insert("reason".into(), Value::String(reason.clone()));
}
ValidationError2::BodyTooLarge { size, limit } => {
detail.insert("field".into(), Value::String("body".into()));
detail.insert(
"reason".into(),
Value::String(format!(
"body too large: {} bytes exceeds {} byte limit",
size, limit
)),
);
}
ValidationError2::TooManyHeaders { count, limit } => {
detail.insert("field".into(), Value::String("headers".into()));
detail.insert(
"reason".into(),
Value::String(format!(
"too many headers: {} exceeds {} limit",
count, limit
)),
);
}
ValidationError2::UriTooLong { length, limit } => {
detail.insert("field".into(), Value::String("uri".into()));
detail.insert(
"reason".into(),
Value::String(format!(
"URI too long: {} chars exceeds {} char limit",
length, limit
)),
);
}
ValidationError2::HeaderTooLarge { name, size, limit } => {
detail
.insert("field".into(), Value::String(format!("header:{}", name)));
detail.insert(
"reason".into(),
Value::String(format!(
"header too large: {} bytes exceeds {} byte limit",
size, limit
)),
);
}
}
Value::Object(detail)
})
.collect();
extensions.insert("errors".into(), Value::Array(error_details));
}
let detail = if errors.len() == 1 {
Some(errors[0].to_string())
} else {
Some(format!("{} validation errors", errors.len()))
};
ProblemDetails {
error_type: "urn:barbacane:error:validation-failed".into(),
title: "Request validation failed".into(),
status: 400,
detail,
instance: None,
extensions,
}
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| {
r#"{"type":"urn:barbacane:error:internal","title":"Serialization error","status":500}"#.into()
})
}
}
#[derive(Debug, Clone)]
pub struct RequestLimits {
pub max_body_size: usize,
pub max_headers: usize,
pub max_header_size: usize,
pub max_uri_length: usize,
}
impl Default for RequestLimits {
fn default() -> Self {
Self {
max_body_size: 1024 * 1024, max_headers: 100,
max_header_size: 8 * 1024, max_uri_length: 8 * 1024, }
}
}
impl RequestLimits {
pub fn validate_uri(&self, uri: &str) -> Result<(), ValidationError2> {
if uri.len() > self.max_uri_length {
return Err(ValidationError2::UriTooLong {
length: uri.len(),
limit: self.max_uri_length,
});
}
Ok(())
}
pub fn validate_headers(
&self,
headers: &HashMap<String, String>,
) -> Result<(), ValidationError2> {
if headers.len() > self.max_headers {
return Err(ValidationError2::TooManyHeaders {
count: headers.len(),
limit: self.max_headers,
});
}
for (name, value) in headers {
let header_size = name.len() + value.len();
if header_size > self.max_header_size {
return Err(ValidationError2::HeaderTooLarge {
name: name.clone(),
size: header_size,
limit: self.max_header_size,
});
}
}
Ok(())
}
pub fn validate_header_count(&self, count: usize) -> Result<(), ValidationError2> {
if count > self.max_headers {
return Err(ValidationError2::TooManyHeaders {
count,
limit: self.max_headers,
});
}
Ok(())
}
pub fn validate_header_size(&self, name: &str, size: usize) -> Result<(), ValidationError2> {
if size > self.max_header_size {
return Err(ValidationError2::HeaderTooLarge {
name: name.to_string(),
size,
limit: self.max_header_size,
});
}
Ok(())
}
pub fn validate_body_size(&self, body_len: usize) -> Result<(), ValidationError2> {
if body_len > self.max_body_size {
return Err(ValidationError2::BodyTooLarge {
size: body_len,
limit: self.max_body_size,
});
}
Ok(())
}
pub fn validate_all(
&self,
uri: &str,
headers: &HashMap<String, String>,
body_len: usize,
) -> Result<(), Vec<ValidationError2>> {
let mut errors = Vec::new();
if let Err(e) = self.validate_uri(uri) {
errors.push(e);
}
if let Err(e) = self.validate_headers(headers) {
errors.push(e);
}
if let Err(e) = self.validate_body_size(body_len) {
errors.push(e);
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
fn compile_schema_with_formats(schema: &Value) -> Option<jsonschema::Validator> {
jsonschema::options()
.should_validate_formats(true)
.build(schema)
.ok()
}
pub struct OperationValidator {
path_params: Vec<CompiledParam>,
query_params: Vec<CompiledParam>,
header_params: Vec<CompiledParam>,
querystring_param: Option<CompiledParam>,
request_body: Option<CompiledRequestBody>,
}
struct CompiledParam {
name: String,
required: bool,
schema: Option<jsonschema::Validator>,
}
struct CompiledRequestBody {
required: bool,
content: HashMap<String, Option<jsonschema::Validator>>,
}
fn validate_params(
params: &[CompiledParam],
lookup: impl Fn(&str) -> Option<String>,
location: &str,
) -> Result<(), Vec<ValidationError2>> {
let mut errors = Vec::new();
for param in params {
match lookup(¶m.name) {
Some(value) => {
if let Some(schema) = ¶m.schema {
let json_value = Value::String(value);
let validation_errors: Vec<_> = schema.iter_errors(&json_value).collect();
if !validation_errors.is_empty() {
let reasons: Vec<String> =
validation_errors.iter().map(|e| e.to_string()).collect();
errors.push(ValidationError2::InvalidParameter {
name: param.name.clone(),
location: location.into(),
reason: reasons.join("; "),
});
}
}
}
None if param.required => {
errors.push(ValidationError2::MissingRequiredParameter {
name: param.name.clone(),
location: location.into(),
});
}
None => {}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
impl OperationValidator {
pub fn new(parameters: &[Parameter], request_body: Option<&RequestBody>) -> Self {
let mut path_params = Vec::new();
let mut query_params = Vec::new();
let mut header_params = Vec::new();
let mut querystring_param = None;
for param in parameters {
let compiled = CompiledParam {
name: param.name.clone(),
required: param.required || param.location == "path", schema: param.schema.as_ref().and_then(compile_schema_with_formats),
};
match param.location.as_str() {
"path" => path_params.push(compiled),
"query" => query_params.push(compiled),
"header" => header_params.push(compiled),
"querystring" => querystring_param = Some(compiled),
_ => {} }
}
let compiled_body = request_body.map(|rb| {
let mut content = HashMap::new();
for (media_type, content_schema) in &rb.content {
let schema = content_schema
.schema
.as_ref()
.and_then(compile_schema_with_formats);
content.insert(media_type.clone(), schema);
}
CompiledRequestBody {
required: rb.required,
content,
}
});
Self {
path_params,
query_params,
header_params,
querystring_param,
request_body: compiled_body,
}
}
pub fn validate_path_params(
&self,
params: &[(String, String)],
) -> Result<(), Vec<ValidationError2>> {
let param_map: HashMap<_, _> = params.iter().cloned().collect();
validate_params(
&self.path_params,
|name| param_map.get(name).cloned(),
"path",
)
}
pub fn validate_query_params(
&self,
query_string: Option<&str>,
) -> Result<(), Vec<ValidationError2>> {
let mut param_map: HashMap<String, String> = HashMap::new();
let mut counts: HashMap<String, usize> = HashMap::new();
for pair in query_string
.unwrap_or("")
.split('&')
.filter(|s| !s.is_empty())
{
let mut parts = pair.splitn(2, '=');
let Some(key) = parts.next() else { continue };
let value = parts.next().unwrap_or("");
let key = percent_decode(key);
*counts.entry(key.clone()).or_insert(0) += 1;
param_map.insert(key, percent_decode(value));
}
let mut errors: Vec<ValidationError2> = self
.query_params
.iter()
.filter(|p| counts.get(&p.name).copied().unwrap_or(0) > 1)
.map(|p| ValidationError2::InvalidParameter {
name: p.name.clone(),
location: "query".into(),
reason: "duplicate query parameter is not allowed".into(),
})
.collect();
if !errors.is_empty() {
errors.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
return Err(errors);
}
validate_params(
&self.query_params,
|name| param_map.get(name).cloned(),
"query",
)
}
pub fn validate_querystring(
&self,
query_string: Option<&str>,
) -> Result<(), Vec<ValidationError2>> {
let Some(param) = &self.querystring_param else {
return Ok(());
};
let qs = query_string.unwrap_or("");
if qs.is_empty() && param.required {
return Err(vec![ValidationError2::MissingRequiredParameter {
name: param.name.clone(),
location: "querystring".into(),
}]);
}
if !qs.is_empty() {
if let Some(schema) = ¶m.schema {
let json_value = Value::String(qs.to_string());
let validation_errors: Vec<_> = schema.iter_errors(&json_value).collect();
if !validation_errors.is_empty() {
let reasons: Vec<String> =
validation_errors.iter().map(|e| e.to_string()).collect();
return Err(vec![ValidationError2::InvalidParameter {
name: param.name.clone(),
location: "querystring".into(),
reason: reasons.join("; "),
}]);
}
}
}
Ok(())
}
pub fn validate_headers(
&self,
headers: &HashMap<String, String>,
) -> Result<(), Vec<ValidationError2>> {
let headers_lower: HashMap<String, String> = headers
.iter()
.map(|(k, v)| (k.to_lowercase(), v.clone()))
.collect();
validate_params(
&self.header_params,
|name| headers_lower.get(&name.to_lowercase()).cloned(),
"header",
)
}
pub fn validate_body(
&self,
content_type: Option<&str>,
body: &[u8],
) -> Result<(), Vec<ValidationError2>> {
let Some(body_spec) = &self.request_body else {
return Ok(());
};
if body_spec.required && body.is_empty() {
return Err(vec![ValidationError2::MissingRequiredBody]);
}
if body.is_empty() {
return Ok(());
}
let ct = content_type.unwrap_or("application/octet-stream");
let base_ct = ct.split(';').next().unwrap_or(ct).trim();
let schema = if let Some(schema) = body_spec.content.get(base_ct) {
schema
} else if let Some(schema) = body_spec.content.get("*/*") {
schema
} else {
return Err(vec![ValidationError2::UnsupportedContentType(
base_ct.to_string(),
)]);
};
if let Some(schema) = schema {
if base_ct.contains("json") {
let json_body: Value = match serde_json::from_slice(body) {
Ok(v) => v,
Err(e) => {
return Err(vec![ValidationError2::InvalidBody(format!(
"invalid JSON: {}",
e
))]);
}
};
let validation_errors: Vec<_> = schema.iter_errors(&json_body).collect();
if !validation_errors.is_empty() {
let reasons: Vec<String> =
validation_errors.iter().map(|e| e.to_string()).collect();
return Err(vec![ValidationError2::InvalidBody(reasons.join("; "))]);
}
}
}
Ok(())
}
pub fn validate_request(
&self,
path_params: &[(String, String)],
query_string: Option<&str>,
headers: &HashMap<String, String>,
content_type: Option<&str>,
body: &[u8],
) -> Result<(), Vec<ValidationError2>> {
self.validate_path_params(path_params)?;
self.validate_query_params(query_string)?;
self.validate_querystring(query_string)?;
self.validate_headers(headers)?;
self.validate_body(content_type, body)?;
Ok(())
}
}
pub fn percent_decode(input: &str) -> String {
let bytes = input.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'%' if i + 2 < bytes.len() => match (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) {
(Some(h), Some(l)) => {
out.push((h << 4) | l);
i += 3;
}
_ => {
out.push(b'%');
i += 1;
}
},
b'+' => {
out.push(b' ');
i += 1;
}
b => {
out.push(b);
i += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::compile_schema_with_formats;
use super::*;
fn make_param(name: &str, location: &str, required: bool, schema: Option<Value>) -> Parameter {
Parameter {
name: name.to_string(),
location: location.to_string(),
required,
schema,
}
}
#[test]
fn validate_required_path_param() {
let params = vec![make_param("id", "path", true, None)];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_path_params(&[]);
assert!(result.is_err());
let result = validator.validate_path_params(&[("id".into(), "123".into())]);
assert!(result.is_ok());
}
#[test]
fn validate_path_param_schema() {
let schema = serde_json::json!({
"type": "string",
"pattern": "^[0-9]+$"
});
let params = vec![make_param("id", "path", true, Some(schema))];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_path_params(&[("id".into(), "123".into())]);
assert!(result.is_ok());
let result = validator.validate_path_params(&[("id".into(), "abc".into())]);
assert!(result.is_err());
}
#[test]
fn validate_required_query_param() {
let params = vec![make_param("page", "query", true, None)];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_query_params(Some(""));
assert!(result.is_err());
let result = validator.validate_query_params(Some("page=1"));
assert!(result.is_ok());
}
#[test]
fn validate_optional_query_param() {
let params = vec![make_param("limit", "query", false, None)];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_query_params(Some(""));
assert!(result.is_ok());
}
#[test]
fn duplicate_declared_query_param_is_rejected() {
let schema = serde_json::json!({"type": "string", "enum": ["user"]});
let params = vec![make_param("role", "query", false, Some(schema))];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_query_params(Some("role=admin&role=user"));
assert!(result.is_err(), "duplicate declared param must be rejected");
assert!(validator.validate_query_params(Some("role=user")).is_ok());
assert!(validator
.validate_query_params(Some("other=1&other=2&role=user"))
.is_ok());
}
#[test]
fn validate_required_body() {
use barbacane_compiler::ContentSchema;
use std::collections::BTreeMap;
let mut content = BTreeMap::new();
content.insert(
"application/json".to_string(),
ContentSchema { schema: None },
);
let request_body = RequestBody {
required: true,
content,
};
let validator = OperationValidator::new(&[], Some(&request_body));
let result = validator.validate_body(Some("application/json"), &[]);
assert!(result.is_err());
let result = validator.validate_body(Some("application/json"), b"{}");
assert!(result.is_ok());
}
#[test]
fn validate_body_schema() {
use barbacane_compiler::ContentSchema;
use std::collections::BTreeMap;
let schema = serde_json::json!({
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" }
}
});
let mut content = BTreeMap::new();
content.insert(
"application/json".to_string(),
ContentSchema {
schema: Some(schema),
},
);
let request_body = RequestBody {
required: true,
content,
};
let validator = OperationValidator::new(&[], Some(&request_body));
let result = validator.validate_body(Some("application/json"), br#"{"name":"test"}"#);
assert!(result.is_ok());
let result = validator.validate_body(Some("application/json"), b"{}");
assert!(result.is_err());
}
#[test]
fn validate_unsupported_content_type() {
use barbacane_compiler::ContentSchema;
use std::collections::BTreeMap;
let mut content = BTreeMap::new();
content.insert(
"application/json".to_string(),
ContentSchema { schema: None },
);
let request_body = RequestBody {
required: true,
content,
};
let validator = OperationValidator::new(&[], Some(&request_body));
let result = validator.validate_body(Some("text/plain"), b"hello");
assert!(result.is_err());
if let Err(errors) = result {
assert!(matches!(
errors[0],
ValidationError2::UnsupportedContentType(_)
));
}
}
#[test]
fn problem_details_format() {
let errors = vec![ValidationError2::MissingRequiredParameter {
name: "id".into(),
location: "path".into(),
}];
let problem = ProblemDetails::validation_error(&errors, false);
assert_eq!(problem.status, 400);
assert_eq!(problem.error_type, "urn:barbacane:error:validation-failed");
let json = problem.to_json();
assert!(json.contains("validation-failed"));
}
#[test]
fn problem_details_dev_mode() {
let errors = vec![ValidationError2::MissingRequiredParameter {
name: "id".into(),
location: "path".into(),
}];
let problem = ProblemDetails::validation_error(&errors, true);
let json = problem.to_json();
assert!(json.contains("errors"));
assert!(json.contains("field"));
}
#[test]
fn validate_uri_length_ok() {
let limits = RequestLimits::default();
let uri = "/api/users/123";
assert!(limits.validate_uri(uri).is_ok());
}
#[test]
fn validate_uri_length_too_long() {
let limits = RequestLimits {
max_uri_length: 10,
..Default::default()
};
let uri = "/api/users/123456789";
let result = limits.validate_uri(uri);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ValidationError2::UriTooLong { .. }
));
}
#[test]
fn validate_header_count_ok() {
let limits = RequestLimits::default();
let headers: HashMap<String, String> = (0..10)
.map(|i| (format!("Header-{}", i), "value".to_string()))
.collect();
assert!(limits.validate_headers(&headers).is_ok());
}
#[test]
fn validate_header_count_too_many() {
let limits = RequestLimits {
max_headers: 5,
..Default::default()
};
let headers: HashMap<String, String> = (0..10)
.map(|i| (format!("Header-{}", i), "value".to_string()))
.collect();
let result = limits.validate_headers(&headers);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ValidationError2::TooManyHeaders { .. }
));
}
#[test]
fn validate_header_size_ok() {
let limits = RequestLimits::default();
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
assert!(limits.validate_headers(&headers).is_ok());
}
#[test]
fn validate_header_size_too_large() {
let limits = RequestLimits {
max_header_size: 20,
..Default::default()
};
let mut headers = HashMap::new();
headers.insert("X-Very-Long-Header".to_string(), "a".repeat(100));
let result = limits.validate_headers(&headers);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ValidationError2::HeaderTooLarge { .. }
));
}
#[test]
fn validate_body_size_ok() {
let limits = RequestLimits::default();
assert!(limits.validate_body_size(1000).is_ok());
}
#[test]
fn validate_body_size_too_large() {
let limits = RequestLimits {
max_body_size: 100,
..Default::default()
};
let result = limits.validate_body_size(1000);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ValidationError2::BodyTooLarge { .. }
));
}
#[test]
fn format_validation_email() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" }
}
});
let validator = compile_schema_with_formats(&schema).unwrap();
let valid = serde_json::json!({"email": "user@example.com"});
assert!(validator.is_valid(&valid));
let invalid = serde_json::json!({"email": "not-an-email"});
assert!(!validator.is_valid(&invalid));
}
#[test]
fn format_validation_uuid() {
let schema = serde_json::json!({
"type": "string",
"format": "uuid"
});
let validator = compile_schema_with_formats(&schema).unwrap();
let valid = serde_json::json!("550e8400-e29b-41d4-a716-446655440000");
assert!(validator.is_valid(&valid));
let invalid = serde_json::json!("not-a-uuid");
assert!(!validator.is_valid(&invalid));
}
#[test]
fn format_validation_date_time() {
let schema = serde_json::json!({
"type": "string",
"format": "date-time"
});
let validator = compile_schema_with_formats(&schema).unwrap();
let valid = serde_json::json!("2024-01-29T12:30:00Z");
assert!(validator.is_valid(&valid));
let invalid = serde_json::json!("not-a-date");
assert!(!validator.is_valid(&invalid));
}
#[test]
fn format_validation_uri() {
let schema = serde_json::json!({
"type": "string",
"format": "uri"
});
let validator = compile_schema_with_formats(&schema).unwrap();
let valid = serde_json::json!("https://example.com/path?query=1");
assert!(validator.is_valid(&valid));
let invalid = serde_json::json!("not a uri");
assert!(!validator.is_valid(&invalid));
}
#[test]
fn format_validation_ipv4() {
let schema = serde_json::json!({
"type": "string",
"format": "ipv4"
});
let validator = compile_schema_with_formats(&schema).unwrap();
let valid = serde_json::json!("192.168.1.1");
assert!(validator.is_valid(&valid));
let invalid = serde_json::json!("999.999.999.999");
assert!(!validator.is_valid(&invalid));
}
#[test]
fn format_validation_ipv6() {
let schema = serde_json::json!({
"type": "string",
"format": "ipv6"
});
let validator = compile_schema_with_formats(&schema).unwrap();
let valid = serde_json::json!("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
assert!(validator.is_valid(&valid));
let invalid = serde_json::json!("not-ipv6");
assert!(!validator.is_valid(&invalid));
}
#[test]
fn validate_all_limits() {
let limits = RequestLimits {
max_uri_length: 10,
max_headers: 2,
max_body_size: 50,
..Default::default()
};
let mut headers = HashMap::new();
headers.insert("A".to_string(), "1".to_string());
assert!(limits.validate_all("/short", &headers, 10).is_ok());
let result = limits.validate_all("/this/is/a/very/long/uri", &headers, 10);
assert!(result.is_err());
assert_eq!(result.unwrap_err().len(), 1);
let many_headers: HashMap<String, String> = (0..5)
.map(|i| (format!("H{}", i), "v".to_string()))
.collect();
let result = limits.validate_all("/this/is/a/very/long/uri", &many_headers, 100);
assert!(result.is_err());
assert_eq!(result.unwrap_err().len(), 3); }
#[test]
fn validate_asyncapi_message_payload() {
use barbacane_compiler::ContentSchema;
use std::collections::BTreeMap;
let message_schema = serde_json::json!({
"type": "object",
"required": ["eventId", "userId", "timestamp"],
"properties": {
"eventId": { "type": "string", "format": "uuid" },
"userId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"metadata": {
"type": "object",
"additionalProperties": true
}
}
});
let mut content = BTreeMap::new();
content.insert(
"application/json".to_string(),
ContentSchema {
schema: Some(message_schema),
},
);
let request_body = RequestBody {
required: true,
content,
};
let validator = OperationValidator::new(&[], Some(&request_body));
let valid_payload = br#"{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"userId": "user-123",
"timestamp": "2024-01-29T12:30:00Z"
}"#;
let result = validator.validate_body(Some("application/json"), valid_payload);
assert!(result.is_ok(), "Valid message should pass: {:?}", result);
let missing_field = br#"{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"userId": "user-123"
}"#;
let result = validator.validate_body(Some("application/json"), missing_field);
assert!(result.is_err(), "Missing timestamp should fail");
let wrong_format = br#"{
"eventId": "not-a-uuid",
"userId": "user-123",
"timestamp": "2024-01-29T12:30:00Z"
}"#;
let result = validator.validate_body(Some("application/json"), wrong_format);
assert!(result.is_err(), "Invalid UUID format should fail");
}
#[test]
fn validate_asyncapi_message_with_avro_content_type() {
use barbacane_compiler::ContentSchema;
use std::collections::BTreeMap;
let message_schema = serde_json::json!({
"type": "object",
"required": ["key"],
"properties": {
"key": { "type": "string" }
}
});
let mut content = BTreeMap::new();
content.insert(
"application/vnd.apache.avro+json".to_string(),
ContentSchema {
schema: Some(message_schema),
},
);
let request_body = RequestBody {
required: true,
content,
};
let validator = OperationValidator::new(&[], Some(&request_body));
let result = validator.validate_body(
Some("application/vnd.apache.avro+json"),
br#"{"key": "value"}"#,
);
assert!(result.is_ok());
let result =
validator.validate_body(Some("application/octet-stream"), br#"{"key": "value"}"#);
assert!(result.is_err());
}
#[test]
fn validate_required_header_param() {
let params = vec![make_param("X-Request-Id", "header", true, None)];
let validator = OperationValidator::new(¶ms, None);
let headers = HashMap::new();
let result = validator.validate_headers(&headers);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(matches!(
&errors[0],
ValidationError2::MissingRequiredParameter { name, location }
if name == "X-Request-Id" && location == "header"
));
let mut headers = HashMap::new();
headers.insert("X-Request-Id".to_string(), "abc-123".to_string());
let result = validator.validate_headers(&headers);
assert!(result.is_ok());
}
#[test]
fn validate_optional_header_param() {
let params = vec![make_param("X-Trace-Id", "header", false, None)];
let validator = OperationValidator::new(¶ms, None);
let headers = HashMap::new();
let result = validator.validate_headers(&headers);
assert!(result.is_ok());
}
#[test]
fn validate_header_param_schema() {
let schema = serde_json::json!({
"type": "string",
"pattern": "^Bearer .+$"
});
let params = vec![make_param("Authorization", "header", true, Some(schema))];
let validator = OperationValidator::new(¶ms, None);
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer token123".to_string());
let result = validator.validate_headers(&headers);
assert!(result.is_ok());
headers.insert(
"Authorization".to_string(),
"Basic dXNlcjpwYXNz".to_string(),
);
let result = validator.validate_headers(&headers);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(matches!(
&errors[0],
ValidationError2::InvalidParameter { name, location, .. }
if name == "Authorization" && location == "header"
));
}
#[test]
fn validate_header_param_case_insensitive() {
let params = vec![make_param("X-Request-Id", "header", true, None)];
let validator = OperationValidator::new(¶ms, None);
let mut headers = HashMap::new();
headers.insert("x-request-id".to_string(), "abc-123".to_string());
let result = validator.validate_headers(&headers);
assert!(result.is_ok());
let mut headers = HashMap::new();
headers.insert("X-REQUEST-ID".to_string(), "abc-123".to_string());
let result = validator.validate_headers(&headers);
assert!(result.is_ok());
}
#[test]
fn validate_asyncapi_channel_parameter() {
let schema = serde_json::json!({
"type": "string",
"pattern": "^user-[a-z0-9]+$"
});
let params = vec![make_param("userId", "path", true, Some(schema))];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_path_params(&[("userId".into(), "user-abc123".into())]);
assert!(result.is_ok());
let result = validator.validate_path_params(&[("userId".into(), "invalid".into())]);
assert!(result.is_err());
}
#[test]
fn validate_querystring_param_present() {
let schema = serde_json::json!({
"type": "string",
"minLength": 1
});
let params = vec![make_param("q", "querystring", true, Some(schema))];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_querystring(Some("filter=active&sort=name"));
assert!(result.is_ok());
}
#[test]
fn validate_querystring_param_missing_required() {
let params = vec![make_param("q", "querystring", true, None)];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_querystring(Some(""));
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(matches!(
&errors[0],
ValidationError2::MissingRequiredParameter { location, .. }
if location == "querystring"
));
}
#[test]
fn validate_querystring_param_schema() {
let schema = serde_json::json!({
"type": "string",
"pattern": "^[a-z]+=\\w+$"
});
let params = vec![make_param("q", "querystring", false, Some(schema))];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_querystring(Some("key=value"));
assert!(result.is_ok());
let result = validator.validate_querystring(Some("KEY=value&other=123"));
assert!(result.is_err());
}
#[test]
fn validate_querystring_not_set() {
let params = vec![make_param("name", "query", false, None)];
let validator = OperationValidator::new(¶ms, None);
let result = validator.validate_querystring(Some("anything"));
assert!(result.is_ok());
}
#[test]
fn percent_decode_handles_multibyte_utf8() {
assert_eq!(percent_decode("caf%C3%A9"), "café");
assert_eq!(percent_decode("%E2%82%AC"), "€");
assert_eq!(percent_decode("a%20b+c"), "a b c");
assert_eq!(percent_decode("100%"), "100%");
assert_eq!(percent_decode("%zz"), "%zz");
}
#[test]
fn header_count_counts_duplicates() {
let limits = RequestLimits {
max_headers: 2,
..Default::default()
};
assert!(limits.validate_header_count(2).is_ok());
assert!(limits.validate_header_count(3).is_err());
}
#[test]
fn header_size_is_checked_per_entry() {
let limits = RequestLimits {
max_header_size: 10,
..Default::default()
};
assert!(limits.validate_header_size("x", 10).is_ok());
assert!(limits.validate_header_size("x-big", 11).is_err());
}
}