use lazy_regex::regex;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use crate::common::reference::{RefOr, ResolveReference};
use crate::v3_2::parameter::{InCookie, InHeader, InPath, InQuery, Parameter};
use crate::v3_2::path_item::PathItem;
use crate::v3_2::security_scheme::SecurityScheme;
use crate::v3_2::spec::Spec;
use crate::v3_2::tag::Tag;
use crate::validation::Options;
use crate::validation::{Context, PushError};
enum ResolvedParam<'a> {
Item(&'a Parameter),
UnresolvedInternal,
UnresolvedExternal,
}
fn resolve_parameter<'a>(spec: &'a Spec, p: &'a RefOr<Parameter>) -> ResolvedParam<'a> {
match p {
RefOr::Item(p) => ResolvedParam::Item(p),
RefOr::Ref(r) => {
if r.reference.starts_with("#/") {
match <Spec as ResolveReference<Parameter>>::resolve_reference(spec, &r.reference) {
Some(p) => ResolvedParam::Item(p),
None => ResolvedParam::UnresolvedInternal,
}
} else {
ResolvedParam::UnresolvedExternal
}
}
}
}
fn parameter_identity(p: &Parameter) -> (&str, &'static str) {
match p {
Parameter::Path(p) => (in_path_name(p), "path"),
Parameter::Query(q) => (in_query_name(q), "query"),
Parameter::Querystring(q) => (q.name.as_str(), "querystring"),
Parameter::Header(h) => (in_header_name(h), "header"),
Parameter::Cookie(c) => (in_cookie_name(c), "cookie"),
}
}
fn in_path_name(p: &InPath) -> &str {
&p.name
}
fn in_query_name(q: &InQuery) -> &str {
&q.name
}
fn in_header_name(h: &InHeader) -> &str {
&h.name
}
fn in_cookie_name(c: &InCookie) -> &str {
&c.name
}
fn path_template_variables(template: &str) -> BTreeSet<String> {
let re = regex!(r"\{([^}]+)\}");
re.captures_iter(template)
.map(|c| c.get(1).unwrap().as_str().to_owned())
.collect()
}
fn canonical_path(template: &str) -> String {
regex!(r"\{[^}]+\}")
.replace_all(template, "{}")
.into_owned()
}
pub fn validate_operation_parameters(
ctx: &mut Context<Spec>,
op_path: &str,
template: &str,
path_item_params: Option<&[RefOr<Parameter>]>,
op_params: Option<&[RefOr<Parameter>]>,
) {
let template_vars = path_template_variables(template);
let ignore_external = ctx.is_option(Options::IgnoreExternalReferences);
let mut has_unresolved_external = false;
fn dup_pass(
ctx: &mut Context<Spec>,
op_path: &str,
params: &[RefOr<Parameter>],
origin: &str,
ignore_external: bool,
has_unresolved_external: &mut bool,
) {
let mut seen: HashMap<(String, &'static str), usize> = HashMap::new();
for (i, raw) in params.iter().enumerate() {
let r = resolve_parameter(ctx.spec, raw);
match r {
ResolvedParam::Item(p) => {
let (name, loc) = parameter_identity(p);
let key = (name.to_owned(), loc);
*seen.entry(key.clone()).or_insert(0) += 1;
if seen[&key] == 2 {
ctx.error(
op_path.to_owned(),
format_args!(
".parameters: duplicate parameter `{name}` in `{loc}` ({origin}[{i}])"
),
);
}
}
ResolvedParam::UnresolvedExternal if ignore_external => {
*has_unresolved_external = true;
}
_ => {}
}
}
}
if let Some(p) = path_item_params {
dup_pass(
ctx,
op_path,
p,
"path-item",
ignore_external,
&mut has_unresolved_external,
);
}
if let Some(p) = op_params {
dup_pass(
ctx,
op_path,
p,
"operation",
ignore_external,
&mut has_unresolved_external,
);
}
#[derive(Clone, Copy)]
enum Kind {
Path,
Other,
}
fn kind_of(p: &Parameter) -> Kind {
match p {
Parameter::Path(_) => Kind::Path,
_ => Kind::Other,
}
}
let mut merged: HashMap<(String, &'static str), Kind> = HashMap::new();
for params in [path_item_params, op_params].into_iter().flatten() {
for raw in params {
if let ResolvedParam::Item(p) = resolve_parameter(ctx.spec, raw) {
let (name, loc) = parameter_identity(p);
merged.insert((name.to_owned(), loc), kind_of(p));
}
}
}
let mut declared_path_params: BTreeSet<String> = BTreeSet::new();
for ((name, _loc), kind) in &merged {
if matches!(kind, Kind::Path) {
declared_path_params.insert(name.clone());
}
}
let skip_template_var_missing = has_unresolved_external;
if !skip_template_var_missing {
for var in &template_vars {
if !declared_path_params.contains(var) {
ctx.error(
op_path.to_owned(),
format_args!(
".parameters: path template variable `{{{var}}}` has no matching `in: path` parameter"
),
);
}
}
}
for declared in &declared_path_params {
if !template_vars.contains(declared) {
ctx.error(
op_path.to_owned(),
format_args!(
".parameters: path parameter `{declared}` does not match any `{{name}}` in the path template"
),
);
}
}
let mut querystring_count = 0usize;
let mut has_query = false;
for params in [path_item_params, op_params].into_iter().flatten() {
for raw in params {
if let ResolvedParam::Item(p) = resolve_parameter(ctx.spec, raw) {
match p {
Parameter::Querystring(_) => querystring_count += 1,
Parameter::Query(_) => has_query = true,
_ => {}
}
}
}
}
if querystring_count > 1 {
ctx.error(
op_path.to_owned(),
format_args!(
".parameters: at most one `in: querystring` parameter is allowed, found {querystring_count}"
),
);
}
if querystring_count > 0 && has_query {
ctx.error(
op_path.to_owned(),
".parameters: `in: querystring` is mutually exclusive with `in: query` parameters",
);
}
}
pub fn validate_security_requirements(
ctx: &mut Context<Spec>,
path: &str,
requirements: &[BTreeMap<String, Vec<String>>],
) {
let schemes_map = ctx
.spec
.components
.as_ref()
.and_then(|c| c.security_schemes.as_ref());
for (i, req) in requirements.iter().enumerate() {
for (name, scopes) in req {
let scheme_ref = format!("#/components/securitySchemes/{name}");
ctx.visit(scheme_ref.clone());
let Some(map) = schemes_map else {
ctx.error(
path.to_owned(),
format_args!(
"[{i}].`{name}`: no `components.securitySchemes` on the spec to resolve against"
),
);
continue;
};
let Some(scheme_or) = map.get(name) else {
ctx.error(
path.to_owned(),
format_args!("[{i}].`{name}`: not declared in `components.securitySchemes`"),
);
continue;
};
let Ok(scheme) = scheme_or.get_item(ctx.spec) else {
continue;
};
match scheme {
SecurityScheme::OAuth2(o) => {
for scope in scopes {
let scope_ref = format!("{scheme_ref}/{scope}");
ctx.visit(scope_ref);
let in_any = [
o.flows
.implicit
.as_ref()
.map(|f| f.scopes.contains_key(scope)),
o.flows
.password
.as_ref()
.map(|f| f.scopes.contains_key(scope)),
o.flows
.client_credentials
.as_ref()
.map(|f| f.scopes.contains_key(scope)),
o.flows
.authorization_code
.as_ref()
.map(|f| f.scopes.contains_key(scope)),
o.flows
.device_authorization
.as_ref()
.map(|f| f.scopes.contains_key(scope)),
]
.into_iter()
.flatten()
.any(|x| x);
if !in_any {
ctx.error(
path.to_owned(),
format_args!(
"[{i}].`{name}`: scope `{scope}` not declared in any flow's scopes"
),
);
}
}
}
SecurityScheme::OpenIdConnect(_) => {
}
SecurityScheme::ApiKey(_)
| SecurityScheme::HTTP(_)
| SecurityScheme::MutualTLS(_) => {
}
}
}
}
}
pub fn validate_tag_uniqueness(ctx: &mut Context<Spec>, tags: &[Tag]) {
let mut counts: HashMap<&str, usize> = HashMap::new();
for t in tags {
*counts.entry(t.name.as_str()).or_insert(0) += 1;
}
for (name, n) in counts {
if n > 1 {
ctx.error(
"#.tags".to_owned(),
format_args!("duplicate tag name `{name}` (found {n} times)"),
);
}
}
}
pub fn validate_path_template_uniqueness<V>(
ctx: &mut Context<Spec>,
section: &str,
paths: &BTreeMap<String, V>,
) {
let mut canonicals: HashMap<String, Vec<&str>> = HashMap::new();
for key in paths.keys() {
if key.starts_with("x-") {
continue;
}
canonicals
.entry(canonical_path(key))
.or_default()
.push(key.as_str());
}
for (canonical, keys) in canonicals {
if keys.len() > 1 {
ctx.error(
section.to_owned(),
format_args!(
"templated paths {keys:?} collapse to the same shape `{canonical}`; OAS forbids equivalent templates"
),
);
}
}
}
pub fn validate_path_item(ctx: &mut Context<Spec>, template: &str, path: &str, item: &PathItem) {
let effective = if item.parameters.is_none() && item.operations.is_none() {
resolve_path_item_chain(ctx.spec, item)
} else {
item
};
let pi_params = effective.parameters.as_deref();
if let Some(ops) = &effective.operations {
for (method, op) in ops {
let op_path = format!("{path}.{method}");
validate_operation_parameters(
ctx,
&op_path,
template,
pi_params,
op.parameters.as_deref(),
);
}
}
}
fn resolve_path_item_chain<'a>(spec: &'a Spec, item: &'a PathItem) -> &'a PathItem {
let mut current = item;
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
while let Some(r) = current.reference.as_deref() {
if r.is_empty() || !seen.insert(r.to_owned()) {
return current;
}
let Some(next) = find_path_item_by_ref(spec, r) else {
return current;
};
current = next;
}
current
}
fn find_path_item_by_ref<'a>(spec: &'a Spec, reference: &str) -> Option<&'a PathItem> {
let unescape = |s: &str| s.replace("~1", "/").replace("~0", "~");
if let Some(after) = reference.strip_prefix("#/paths/") {
if after.contains('/') {
return None;
}
spec.paths.as_ref()?.paths.get(&unescape(after))
} else if let Some(after) = reference.strip_prefix("#/webhooks/") {
if after.contains('/') {
return None;
}
spec.webhooks.as_ref()?.paths.get(&unescape(after))
} else if let Some(after) = reference.strip_prefix("#/components/pathItems/") {
if after.contains('/') {
return None;
}
spec.components
.as_ref()?
.path_items
.as_ref()?
.get(&unescape(after))
} else if let Some(after) = reference.strip_prefix("#/components/callbacks/") {
let mut split = after.splitn(2, '/');
let (Some(cb_token), Some(expr_token)) = (split.next(), split.next()) else {
return None;
};
if expr_token.contains('/') {
return None;
}
let cb_name = unescape(cb_token);
let expr = unescape(expr_token);
let cb_ref = spec
.components
.as_ref()?
.callbacks
.as_ref()?
.get(&cb_name)?;
let cb = cb_ref.get_item(spec).ok()?;
cb.paths.get(&expr)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::v3_2::components::Components;
use crate::v3_2::parameter::{InCookie, InHeader, InPath, InQuery};
use crate::v3_2::security_scheme::{
AuthorizationCodeOAuth2Flow, ImplicitOAuth2Flow, OAuth2Flows, OAuth2SecurityScheme,
OpenIdConnectSecurityScheme, SecurityScheme,
};
use crate::v3_2::spec::Spec;
use crate::validation::Context;
use crate::validation::ValidationErrorsExt;
fn path_param(name: &str) -> RefOr<Parameter> {
RefOr::new_item(Parameter::Path(Box::new(InPath {
name: name.into(),
description: None,
required: true,
deprecated: None,
style: None,
explode: None,
schema: None,
example: None,
examples: None,
content: None,
extensions: None,
})))
}
fn query_param(name: &str) -> RefOr<Parameter> {
RefOr::new_item(Parameter::Query(Box::new(InQuery {
name: name.into(),
description: None,
required: None,
deprecated: None,
allow_empty_value: None,
style: None,
explode: None,
allow_reserved: None,
schema: None,
example: None,
examples: None,
content: None,
extensions: None,
})))
}
fn header_param(name: &str) -> RefOr<Parameter> {
RefOr::new_item(Parameter::Header(Box::new(InHeader {
name: name.into(),
description: None,
required: None,
deprecated: None,
style: None,
explode: None,
schema: None,
example: None,
examples: None,
content: None,
extensions: None,
})))
}
fn cookie_param(name: &str) -> RefOr<Parameter> {
RefOr::new_item(Parameter::Cookie(Box::new(InCookie {
name: name.into(),
description: None,
required: None,
deprecated: None,
style: None,
explode: None,
schema: None,
example: None,
examples: None,
content: None,
extensions: None,
})))
}
fn spec_with_components(c: Components) -> Spec {
Spec {
components: Some(c),
..Default::default()
}
}
#[test]
fn duplicate_param_within_level_flagged() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let params = vec![query_param("q"), query_param("q")];
validate_operation_parameters(&mut ctx, "op", "/p", None, Some(¶ms));
assert!(
ctx.errors
.iter()
.any(|e| e.contains("duplicate parameter `q`")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn cookie_and_header_locations_distinct() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let params = vec![header_param("session"), cookie_param("session")];
validate_operation_parameters(&mut ctx, "op", "/p", None, Some(¶ms));
assert!(
ctx.errors.is_empty(),
"different locations should not duplicate: {:?}",
ctx.errors
);
}
#[test]
fn template_var_missing_path_param() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
validate_operation_parameters(&mut ctx, "op", "/users/{id}", None, None);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("template variable `{id}`")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn path_param_without_template_var() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let params = vec![path_param("id")];
validate_operation_parameters(&mut ctx, "op", "/no-vars", None, Some(¶ms));
assert!(
ctx.errors
.mentions("path parameter `id` does not match any `{name}` in the path template"),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn equivalent_templates_flagged() {
let mut paths: BTreeMap<String, PathItem> = BTreeMap::new();
paths.insert("/pets/{id}".into(), PathItem::default());
paths.insert("/pets/{name}".into(), PathItem::default());
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
validate_path_template_uniqueness(&mut ctx, "#.paths", &paths);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("collapse to the same shape")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn duplicate_tag_names_flagged() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let tags = vec![
Tag {
name: "pets".into(),
..Default::default()
},
Tag {
name: "pets".into(),
..Default::default()
},
];
validate_tag_uniqueness(&mut ctx, &tags);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("duplicate tag name `pets`")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn security_oauth2_undefined_scope() {
let flows = OAuth2Flows {
implicit: Some(ImplicitOAuth2Flow {
authorization_url: "https://x.example/auth".into(),
refresh_url: None,
scopes: BTreeMap::from([("read".to_owned(), "Read".to_owned())]),
extensions: None,
}),
..Default::default()
};
let mut schemes = BTreeMap::new();
schemes.insert(
"o".to_owned(),
RefOr::new_item(SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
deprecated: None,
flows,
description: None,
oauth2_metadata_url: None,
extensions: None,
}))),
);
let comp = Components {
security_schemes: Some(schemes),
..Default::default()
};
let spec: &'static Spec = Box::leak(Box::new(spec_with_components(comp)));
let mut ctx = Context::new(spec, Options::new());
let mut req: BTreeMap<String, Vec<String>> = BTreeMap::new();
req.insert("o".to_owned(), vec!["write".to_owned()]);
validate_security_requirements(&mut ctx, "#.security", &[req]);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("scope `write` not declared")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn security_apikey_with_role_array_accepted_in_3_2() {
let mut schemes = BTreeMap::new();
schemes.insert(
"ak".to_owned(),
RefOr::new_item(SecurityScheme::ApiKey(Box::default())),
);
let comp = Components {
security_schemes: Some(schemes),
..Default::default()
};
let spec: &'static Spec = Box::leak(Box::new(spec_with_components(comp)));
let mut ctx = Context::new(spec, Options::new());
let mut req: BTreeMap<String, Vec<String>> = BTreeMap::new();
req.insert("ak".to_owned(), vec!["admin".to_owned()]);
validate_security_requirements(&mut ctx, "#.security", &[req]);
assert!(
ctx.errors.is_empty(),
"non-empty role names should be allowed in 3.1: {:?}",
ctx.errors
);
}
#[test]
fn security_mutual_tls_with_role_array_accepted() {
let mut schemes = BTreeMap::new();
schemes.insert(
"mtls".to_owned(),
RefOr::new_item(SecurityScheme::MutualTLS(Box::default())),
);
let comp = Components {
security_schemes: Some(schemes),
..Default::default()
};
let spec: &'static Spec = Box::leak(Box::new(spec_with_components(comp)));
let mut ctx = Context::new(spec, Options::new());
let mut req: BTreeMap<String, Vec<String>> = BTreeMap::new();
req.insert("mtls".to_owned(), vec!["operator".to_owned()]);
validate_security_requirements(&mut ctx, "#.security", &[req]);
assert!(ctx.errors.is_empty(), "errors: {:?}", ctx.errors);
}
#[test]
fn security_openidconnect_accepts_any_scopes() {
let mut schemes = BTreeMap::new();
schemes.insert(
"oid".to_owned(),
RefOr::new_item(SecurityScheme::OpenIdConnect(Box::new(
OpenIdConnectSecurityScheme {
deprecated: None,
open_id_connect_url: "https://x.example/.well-known".into(),
description: None,
extensions: None,
},
))),
);
let comp = Components {
security_schemes: Some(schemes),
..Default::default()
};
let spec: &'static Spec = Box::leak(Box::new(spec_with_components(comp)));
let mut ctx = Context::new(spec, Options::new());
let mut req: BTreeMap<String, Vec<String>> = BTreeMap::new();
req.insert("oid".to_owned(), vec!["openid".to_owned()]);
validate_security_requirements(&mut ctx, "#.security", &[req]);
assert!(ctx.errors.is_empty(), "errors: {:?}", ctx.errors);
}
#[test]
fn security_missing_scheme_reported() {
let comp = Components {
security_schemes: Some(BTreeMap::new()),
..Default::default()
};
let spec: &'static Spec = Box::leak(Box::new(spec_with_components(comp)));
let mut ctx = Context::new(spec, Options::new());
let mut req: BTreeMap<String, Vec<String>> = BTreeMap::new();
req.insert("missing".to_owned(), vec![]);
validate_security_requirements(&mut ctx, "#.security", &[req]);
assert!(
ctx.errors.mentions("not declared"),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn external_ref_suppresses_template_correspondence_under_ignore_external() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let params: Vec<RefOr<Parameter>> = vec![RefOr::new_ref(
"https://other.example/spec.yaml#/components/parameters/PetId".to_owned(),
)];
let mut ctx = Context::new(spec, Options::new());
validate_operation_parameters(&mut ctx, "op", "/users/{id}", None, Some(¶ms));
assert!(
ctx.errors
.iter()
.any(|e| e.contains("template variable `{id}`")),
"errors: {:?}",
ctx.errors
);
let mut ctx = Context::new(spec, Options::IgnoreExternalReferences.only());
validate_operation_parameters(&mut ctx, "op", "/users/{id}", None, Some(¶ms));
assert!(
!ctx.errors.mentions("template variable"),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn validate_path_item_follows_internal_ref_chain() {
use crate::v3_2::operation::Operation;
use crate::v3_2::parameter::{InPath, Parameter};
use crate::v3_2::response::{Response, Responses};
let target = PathItem {
operations: Some(BTreeMap::from([(
"get".to_owned(),
Operation {
parameters: Some(vec![RefOr::new_item(Parameter::Path(Box::new(InPath {
name: "wrong".into(),
description: None,
required: true,
deprecated: None,
style: None,
explode: None,
schema: None,
example: None,
examples: None,
content: Some(BTreeMap::from([(
"application/json".to_owned(),
RefOr::new_item(crate::v3_2::media_type::MediaType::default()),
)])),
extensions: None,
})))]),
responses: Some(Responses {
responses: Some(BTreeMap::from([(
"200".to_owned(),
RefOr::new_item(Response {
description: Some("ok".into()),
..Default::default()
}),
)])),
..Default::default()
}),
..Default::default()
},
)])),
..Default::default()
};
let mut cp = BTreeMap::new();
cp.insert("Reusable".to_owned(), target);
let comp = Components {
path_items: Some(cp),
..Default::default()
};
let spec: &'static Spec = Box::leak(Box::new(Spec {
components: Some(comp),
..Default::default()
}));
let item = PathItem {
reference: Some("#/components/pathItems/Reusable".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/users/{id}", "#.paths[/users/{id}]", &item);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("template variable `{id}`") || e.contains("parameter `wrong`")),
"expected param-mismatch report after chain follow: {:?}",
ctx.errors
);
}
#[test]
fn authorization_code_flow_is_smoke_compiled() {
let _ = AuthorizationCodeOAuth2Flow {
authorization_url: "x".into(),
token_url: "y".into(),
refresh_url: None,
scopes: BTreeMap::new(),
extensions: None,
};
}
#[test]
fn querystring_xor_query_errors() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let qs_param = RefOr::new_item(Parameter::Querystring(Box::new(
crate::v3_2::parameter::InQuerystring {
name: "q".into(),
content: BTreeMap::new(), ..Default::default()
},
)));
let q_param = query_param("filter");
validate_operation_parameters(&mut ctx, "op", "/p", None, Some(&[qs_param, q_param]));
assert!(
ctx.errors
.iter()
.any(|e| e.contains("`in: querystring` is mutually exclusive")),
"querystring + query must error: {:?}",
ctx.errors
);
}
#[test]
fn multiple_querystring_params_errors() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let qs1 = RefOr::new_item(Parameter::Querystring(Box::new(
crate::v3_2::parameter::InQuerystring {
name: "q1".into(),
content: BTreeMap::new(),
..Default::default()
},
)));
let qs2 = RefOr::new_item(Parameter::Querystring(Box::new(
crate::v3_2::parameter::InQuerystring {
name: "q2".into(),
content: BTreeMap::new(),
..Default::default()
},
)));
validate_operation_parameters(&mut ctx, "op", "/p", None, Some(&[qs1, qs2]));
assert!(
ctx.errors
.iter()
.any(|e| e.contains("at most one `in: querystring` parameter")),
"two querystring params must error: {:?}",
ctx.errors
);
}
#[test]
fn parameter_identity_querystring_variant() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let qs1 = RefOr::new_item(Parameter::Querystring(Box::new(
crate::v3_2::parameter::InQuerystring {
name: "q".into(),
content: BTreeMap::new(),
..Default::default()
},
)));
let qs2 = RefOr::new_item(Parameter::Querystring(Box::new(
crate::v3_2::parameter::InQuerystring {
name: "q".into(),
content: BTreeMap::new(),
..Default::default()
},
)));
validate_operation_parameters(&mut ctx, "op", "/p", None, Some(&[qs1, qs2]));
assert!(
ctx.errors
.iter()
.any(|e| e.contains("duplicate parameter `q`")),
"duplicate querystring must error: {:?}",
ctx.errors
);
}
#[test]
fn security_no_components_reports_no_schemes() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let mut req: BTreeMap<String, Vec<String>> = BTreeMap::new();
req.insert("myScheme".to_owned(), vec![]);
validate_security_requirements(&mut ctx, "#.security", &[req]);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("no `components.securitySchemes`")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn path_template_uniqueness_skips_extension_keys() {
let mut paths: BTreeMap<String, PathItem> = BTreeMap::new();
paths.insert("/pets/{id}".into(), PathItem::default());
paths.insert("x-custom".into(), PathItem::default()); let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
validate_path_template_uniqueness(&mut ctx, "#.paths", &paths);
assert!(
ctx.errors.is_empty(),
"x- keys must be skipped: {:?}",
ctx.errors
);
}
#[test]
fn validate_path_item_no_ops_skips_param_check() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let item = PathItem::default();
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/pets", "#.paths[/pets]", &item);
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
}
#[test]
fn internal_unresolved_ref_in_dup_pass() {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let mut ctx = Context::new(spec, Options::new());
let dangling = RefOr::new_ref("#/components/parameters/Missing".to_owned());
validate_operation_parameters(&mut ctx, "op", "/p", None, Some(&[dangling]));
}
#[test]
fn find_path_item_by_ref_malformed_tokens_return_none() {
use crate::v3_2::path_item::{PathItem, Paths};
let mut paths = Paths::default();
paths.paths.insert("/pets".to_owned(), PathItem::default());
let spec: &'static Spec = Box::leak(Box::new(Spec {
paths: Some(paths),
..Default::default()
}));
let item = PathItem {
reference: Some("#/paths/~1pets/extra".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/pets", "#.paths[/pets]", &item);
assert!(
ctx.errors.is_empty(),
"no validation errors: {:?}",
ctx.errors
);
}
#[test]
fn security_scheme_ref_that_fails_to_resolve_is_silently_skipped() {
use crate::common::reference::RefOr;
let mut schemes = BTreeMap::new();
schemes.insert(
"dangling".to_owned(),
RefOr::new_ref("#/components/securitySchemes/DoesNotExist".to_owned()),
);
let comp = Components {
security_schemes: Some(schemes),
..Default::default()
};
let spec: &'static Spec = Box::leak(Box::new(spec_with_components(comp)));
let mut ctx = Context::new(spec, Options::new());
let mut req: BTreeMap<String, Vec<String>> = BTreeMap::new();
req.insert("dangling".to_owned(), vec![]);
validate_security_requirements(&mut ctx, "#.security", &[req]);
}
#[test]
fn resolve_path_item_chain_empty_ref_stops_chain() {
use crate::v3_2::path_item::PathItem;
let target = PathItem {
reference: Some("".into()), ..Default::default()
};
let mut paths = crate::v3_2::path_item::Paths::default();
paths.paths.insert("/stop".to_owned(), target);
let spec: &'static Spec = Box::leak(Box::new(Spec {
paths: Some(paths),
..Default::default()
}));
let item = PathItem {
reference: Some("#/paths/~1stop".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/stop", "#.paths[/stop]", &item);
}
#[test]
fn resolve_path_item_chain_cycle_stops_chain() {
use crate::v3_2::path_item::PathItem;
let mut paths = crate::v3_2::path_item::Paths::default();
paths.paths.insert(
"/a".to_owned(),
PathItem {
reference: Some("#/paths/~1b".into()),
..Default::default()
},
);
paths.paths.insert(
"/b".to_owned(),
PathItem {
reference: Some("#/paths/~1a".into()),
..Default::default()
},
);
let spec: &'static Spec = Box::leak(Box::new(Spec {
paths: Some(paths),
..Default::default()
}));
let item = PathItem {
reference: Some("#/paths/~1a".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/a", "#.paths[/a]", &item);
}
#[test]
fn find_path_item_by_ref_webhooks_malformed_token_returns_none() {
use crate::v3_2::path_item::{PathItem, Paths};
let mut webhooks = Paths::default();
webhooks
.paths
.insert("event".to_owned(), PathItem::default());
let spec: &'static Spec = Box::leak(Box::new(Spec {
webhooks: Some(webhooks),
..Default::default()
}));
let item = PathItem {
reference: Some("#/webhooks/event/extra".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/p", "#.paths[/p]", &item);
assert!(
ctx.errors.is_empty(),
"no validation errors for malformed webhook ref: {:?}",
ctx.errors
);
}
#[test]
fn find_path_item_by_ref_webhooks_valid_resolves() {
use crate::common::reference::RefOr;
use crate::v3_2::operation::Operation;
use crate::v3_2::path_item::{PathItem, Paths};
use crate::v3_2::response::{Response, Responses};
let mut ops = BTreeMap::new();
ops.insert(
"get".to_owned(),
Operation {
parameters: Some(vec![path_param("id")]),
responses: Some(Responses {
responses: Some(BTreeMap::from([(
"200".to_owned(),
RefOr::new_item(Response {
description: Some("ok".into()),
..Default::default()
}),
)])),
..Default::default()
}),
..Default::default()
},
);
let target = PathItem {
operations: Some(ops),
..Default::default()
};
let mut webhooks = Paths::default();
webhooks.paths.insert("myEvent".to_owned(), target);
let spec: &'static Spec = Box::leak(Box::new(Spec {
webhooks: Some(webhooks),
..Default::default()
}));
let item = PathItem {
reference: Some("#/webhooks/myEvent".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/{id}", "#.webhooks[myEvent]", &item);
assert!(
ctx.errors.is_empty(),
"valid webhook ref chain should resolve cleanly: {:?}",
ctx.errors
);
}
#[test]
fn find_path_item_by_ref_components_path_items_malformed_token() {
use crate::v3_2::path_item::PathItem;
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let item = PathItem {
reference: Some("#/components/pathItems/a/b".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/p", "#.paths[/p]", &item);
assert!(
ctx.errors.is_empty(),
"malformed pathItems token should stop chain: {:?}",
ctx.errors
);
}
#[test]
fn find_path_item_by_ref_callbacks_branch_covered() {
use crate::v3_2::callback::Callback;
use crate::v3_2::path_item::PathItem;
let mut cb_paths = BTreeMap::new();
cb_paths.insert("e".to_owned(), PathItem::default());
let cb = Callback {
paths: cb_paths,
..Default::default()
};
let mut cbs = BTreeMap::new();
cbs.insert(
"CB".to_owned(),
crate::common::reference::RefOr::new_item(cb),
);
let comp = Components {
callbacks: Some(cbs),
..Default::default()
};
let spec: &'static Spec = Box::leak(Box::new(Spec {
components: Some(comp),
..Default::default()
}));
let item = PathItem {
reference: Some("#/components/callbacks/CB/e".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/p", "#.paths[/p]", &item);
assert!(
ctx.errors.is_empty(),
"callbacks ref chain should resolve: {:?}",
ctx.errors
);
}
#[test]
fn find_path_item_by_ref_callbacks_no_slash_returns_none() {
use crate::v3_2::path_item::PathItem;
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let item = PathItem {
reference: Some("#/components/callbacks/OnlyOnePart".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/p", "#.paths[/p]", &item);
assert!(
ctx.errors.is_empty(),
"callback ref with missing expr should stop chain: {:?}",
ctx.errors
);
}
#[test]
fn find_path_item_by_ref_callbacks_expr_token_with_slash_returns_none() {
use crate::v3_2::path_item::PathItem;
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let item = PathItem {
reference: Some("#/components/callbacks/CB/expr/extra".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/p", "#.paths[/p]", &item);
assert!(
ctx.errors.is_empty(),
"callback expr with extra slash should stop chain: {:?}",
ctx.errors
);
}
#[test]
fn find_path_item_by_ref_unknown_prefix_else_branch() {
use crate::v3_2::path_item::PathItem;
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
let item = PathItem {
reference: Some("#/completely/unknown/prefix".into()),
..Default::default()
};
let mut ctx = Context::new(spec, Options::new());
validate_path_item(&mut ctx, "/p", "#.paths[/p]", &item);
assert!(
ctx.errors.is_empty(),
"unknown prefix ref should stop chain without error: {:?}",
ctx.errors
);
}
}