use crate::v3_2::server::Server;
use crate::v3_2::spec::Spec;
use crate::validation::Options;
use crate::validation::{Context, PushError, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
fn unescape_pointer_token(token: &str) -> String {
token.replace("~1", "/").replace("~0", "~")
}
enum OperationRefResolution {
Ok(Vec<String>),
Err(String),
ExternalPathItemRef(String),
}
fn resolve_internal_operation_ref(spec: &Spec, reference: &str) -> OperationRefResolution {
enum Container {
Paths,
Webhooks,
ComponentPathItems,
ComponentCallbacks,
}
let (container, after) = if let Some(rest) = reference.strip_prefix("#/paths/") {
(Container::Paths, rest)
} else if let Some(rest) = reference.strip_prefix("#/webhooks/") {
(Container::Webhooks, rest)
} else if let Some(rest) = reference.strip_prefix("#/components/pathItems/") {
(Container::ComponentPathItems, rest)
} else if let Some(rest) = reference.strip_prefix("#/components/callbacks/") {
(Container::ComponentCallbacks, rest)
} else {
return OperationRefResolution::Err(format!(
"must start with `#/paths/`, `#/webhooks/`, `#/components/pathItems/`, or `#/components/callbacks/`, found `{reference}`"
));
};
let parts: Vec<&str> = after.split('/').collect();
if parts.iter().any(|p| p.is_empty()) {
return OperationRefResolution::Err(format!(
"malformed JSON Pointer: empty token in `{reference}`; each token with embedded `/` MUST be encoded as `~1`"
));
}
let mut visits: Vec<String> = Vec::new();
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let (entry_path, mut item, mut consumed): (String, &crate::v3_2::path_item::PathItem, usize) =
match container {
Container::Paths => {
if parts.len() < 2 {
return malformed_pointer(reference);
}
let path = unescape_pointer_token(parts[0]);
let Some(item) = spec.paths.as_ref().and_then(|p| p.paths.get(&path)) else {
return OperationRefResolution::Err(format!(
"path `{path}` not declared in `#/paths`"
));
};
seen.insert(format!("#/paths/{}", parts[0]));
(path, item, 1)
}
Container::Webhooks => {
if parts.len() < 2 {
return malformed_pointer(reference);
}
let name = unescape_pointer_token(parts[0]);
let Some(item) = spec.webhooks.as_ref().and_then(|w| w.paths.get(&name)) else {
return OperationRefResolution::Err(format!(
"webhook `{name}` not declared in `#/webhooks`"
));
};
seen.insert(format!("#/webhooks/{}", parts[0]));
(name, item, 1)
}
Container::ComponentPathItems => {
if parts.len() < 2 {
return malformed_pointer(reference);
}
let name = unescape_pointer_token(parts[0]);
let Some(item) = spec
.components
.as_ref()
.and_then(|c| c.path_items.as_ref())
.and_then(|m| m.get(&name))
else {
return OperationRefResolution::Err(format!(
"path item `{name}` not declared in `#/components/pathItems`"
));
};
visits.push(format!("#/components/pathItems/{name}"));
seen.insert(format!("#/components/pathItems/{}", parts[0]));
(name, item, 1)
}
Container::ComponentCallbacks => {
if parts.len() < 3 {
return malformed_pointer(reference);
}
let cb_name = unescape_pointer_token(parts[0]);
let expr = unescape_pointer_token(parts[1]);
let Some(cb_ref) = spec
.components
.as_ref()
.and_then(|c| c.callbacks.as_ref())
.and_then(|m| m.get(&cb_name))
else {
return OperationRefResolution::Err(format!(
"callback `{cb_name}` not declared in `#/components/callbacks`"
));
};
let cb = match cb_ref.get_item(spec) {
Ok(cb) => cb,
Err(
crate::common::reference::ResolveError::ExternalUnsupported(target)
| crate::common::reference::ResolveError::External {
reference: target, ..
},
) => {
return OperationRefResolution::ExternalPathItemRef(target);
}
Err(crate::common::reference::ResolveError::NotFound(t)) => {
return OperationRefResolution::Err(format!(
"callback `{cb_name}` is a `$ref` to `{t}`, which is not declared"
));
}
};
let Some(item) = cb.paths.get(&expr) else {
return OperationRefResolution::Err(format!(
"expression `{expr}` not declared on callback `{cb_name}`"
));
};
visits.push(format!("#/components/callbacks/{cb_name}"));
seen.insert(format!("#/components/callbacks/{}/{}", parts[0], parts[1]));
(format!("{cb_name}/{expr}"), item, 2)
}
};
let mut display_path = entry_path;
item = match resolve_path_item_ref_chain(spec, &display_path, item, &mut seen, &mut visits) {
Ok((p, t)) => {
display_path = p;
t
}
Err(err) => return err,
};
if consumed >= parts.len() {
return malformed_pointer(reference);
}
let mut method = parts[consumed];
consumed += 1;
while consumed < parts.len() {
if parts.len() - consumed < 4 || parts[consumed] != "callbacks" {
return OperationRefResolution::Err(format!(
"malformed deep pointer: expected `/callbacks/<name>/<expr>/<method>` continuation, found `{reference}`"
));
}
let Some(op) = item.operations.as_ref().and_then(|m| m.get(method)) else {
return OperationRefResolution::Err(format!(
"method `{method}` not declared on path `{display_path}`"
));
};
let cb_name = unescape_pointer_token(parts[consumed + 1]);
let expr = unescape_pointer_token(parts[consumed + 2]);
let next_method = parts[consumed + 3];
let Some(cb_ref) = op.callbacks.as_ref().and_then(|m| m.get(&cb_name)) else {
return OperationRefResolution::Err(format!(
"callback `{cb_name}` not declared on `{display_path}.{method}`"
));
};
if let crate::common::reference::RefOr::Ref(r) = cb_ref
&& let Some(after) = r.reference.strip_prefix("#/components/callbacks/")
{
let cb_token = after.split_once('/').map(|(c, _)| c).unwrap_or(after);
visits.push(format!("#/components/callbacks/{cb_token}"));
}
let cb = match cb_ref.get_item(spec) {
Ok(cb) => cb,
Err(
crate::common::reference::ResolveError::ExternalUnsupported(target)
| crate::common::reference::ResolveError::External {
reference: target, ..
},
) => {
return OperationRefResolution::ExternalPathItemRef(target);
}
Err(crate::common::reference::ResolveError::NotFound(t)) => {
return OperationRefResolution::Err(format!(
"callback `{cb_name}` is a `$ref` to `{t}`, which is not declared"
));
}
};
let Some(next_item) = cb.paths.get(&expr) else {
return OperationRefResolution::Err(format!(
"expression `{expr}` not declared on callback `{cb_name}`"
));
};
display_path = format!("{display_path}.{method}.callbacks[{cb_name}][{expr}]");
item = match resolve_path_item_ref_chain(
spec,
&display_path,
next_item,
&mut seen,
&mut visits,
) {
Ok((p, t)) => {
display_path = p;
t
}
Err(err) => return err,
};
method = next_method;
consumed += 4;
}
if !item
.operations
.as_ref()
.is_some_and(|m| m.contains_key(method))
{
return OperationRefResolution::Err(format!(
"method `{method}` not declared on path `{display_path}`"
));
}
OperationRefResolution::Ok(visits)
}
fn malformed_pointer(reference: &str) -> OperationRefResolution {
OperationRefResolution::Err(format!(
"malformed JSON Pointer: each token with embedded `/` MUST be encoded as `~1`, found `{reference}`"
))
}
fn resolve_path_item_ref_chain<'a>(
spec: &'a Spec,
path: &str,
item: &'a crate::v3_2::path_item::PathItem,
seen: &mut std::collections::BTreeSet<String>,
visits: &mut Vec<String>,
) -> Result<(String, &'a crate::v3_2::path_item::PathItem), OperationRefResolution> {
let Some(ref_str) = &item.reference else {
return Ok((path.to_owned(), item));
};
if ref_str.is_empty() {
return Err(OperationRefResolution::Err(format!(
"path `{path}` carries an empty `$ref`"
)));
}
if !seen.insert(ref_str.clone()) {
return Err(OperationRefResolution::Err(format!(
"path `{path}` has a cyclic `$ref` chain through `{ref_str}`"
)));
}
let (target_path, target_item) = if let Some(after_paths) = ref_str.strip_prefix("#/paths/") {
if after_paths.contains('/') {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to malformed JSON Pointer `{ref_str}`: the encoded path token must use `~1` for `/`"
)));
}
let tp = unescape_pointer_token(after_paths);
let Some(paths) = spec.paths.as_ref() else {
return Err(OperationRefResolution::Err(format!(
"path `{path}` has a `$ref` to `{ref_str}` but spec has no `paths`"
)));
};
let Some(t) = paths.paths.get(&tp) else {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to `{ref_str}`, which is not declared in `#/paths`"
)));
};
(tp, t)
} else if let Some(after) = ref_str.strip_prefix("#/webhooks/") {
if after.contains('/') {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to malformed JSON Pointer `{ref_str}`"
)));
}
let tp = unescape_pointer_token(after);
let Some(t) = spec.webhooks.as_ref().and_then(|w| w.paths.get(&tp)) else {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to `{ref_str}`, which is not declared in `#/webhooks`"
)));
};
(tp, t)
} else if let Some(after) = ref_str.strip_prefix("#/components/pathItems/") {
if after.contains('/') {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to malformed JSON Pointer `{ref_str}`"
)));
}
let tp = unescape_pointer_token(after);
let Some(t) = spec
.components
.as_ref()
.and_then(|c| c.path_items.as_ref())
.and_then(|m| m.get(&tp))
else {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to `{ref_str}`, which is not declared in `#/components/pathItems`"
)));
};
visits.push(format!("#/components/pathItems/{tp}"));
(tp, t)
} else if let Some(after) = ref_str.strip_prefix("#/components/callbacks/") {
let mut split = after.splitn(2, '/');
let (Some(cb_token), Some(expr_token)) = (split.next(), split.next()) else {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to malformed JSON Pointer `{ref_str}`: callback target must be `<name>/<encoded expression>`"
)));
};
if expr_token.contains('/') {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to malformed JSON Pointer `{ref_str}`: the encoded expression token must use `~1` for `/`"
)));
}
let cb_name = unescape_pointer_token(cb_token);
let expr = unescape_pointer_token(expr_token);
let tp = format!("{cb_name}/{expr}");
let Some(cb_ref) = spec
.components
.as_ref()
.and_then(|c| c.callbacks.as_ref())
.and_then(|m| m.get(&cb_name))
else {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to `{ref_str}`, callback `{cb_name}` is not declared in `#/components/callbacks`"
)));
};
let cb = match cb_ref.get_item(spec) {
Ok(cb) => cb,
Err(
crate::common::reference::ResolveError::ExternalUnsupported(target)
| crate::common::reference::ResolveError::External {
reference: target, ..
},
) => {
return Err(OperationRefResolution::ExternalPathItemRef(target));
}
Err(crate::common::reference::ResolveError::NotFound(t)) => {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to `{ref_str}`; callback resolves to `{t}`, which is not declared"
)));
}
};
let Some(t) = cb.paths.get(&expr) else {
return Err(OperationRefResolution::Err(format!(
"path `{path}` is a `$ref` to `{ref_str}`, expression `{expr}` is not declared on callback `{cb_name}`"
)));
};
visits.push(format!("#/components/callbacks/{cb_name}"));
(tp, t)
} else {
return Err(OperationRefResolution::ExternalPathItemRef(ref_str.clone()));
};
resolve_path_item_ref_chain(spec, &target_path, target_item, seen, visits)
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Link {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "operationRef")]
pub operation_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "operationId")]
pub operation_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<BTreeMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "requestBody")]
pub request_body: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<Server>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext<Spec> for Link {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match (&self.operation_ref, &self.operation_id) {
(Some(_), Some(_)) => ctx.error(
path.clone(),
"operationRef and operationId are mutually exclusive",
),
(None, None) => ctx.error(
path.clone(),
"must specify exactly one of operationRef or operationId",
),
_ => {}
}
if let Some(operation_id) = &self.operation_id
&& !ctx
.visited
.contains(format!("#/paths/operations/{operation_id}").as_str())
{
ctx.error(
path.clone(),
format_args!(".operationId: missing operation with id `{operation_id}`"),
);
}
if let Some(operation_ref) = &self.operation_ref {
if operation_ref.is_empty() {
ctx.error(path.clone(), ".operationRef: must not be empty");
} else if operation_ref.starts_with("#/") {
match resolve_internal_operation_ref(ctx.spec, operation_ref) {
OperationRefResolution::Ok(visits) => {
for r in visits {
ctx.visit(r);
}
}
OperationRefResolution::Err(msg) => {
ctx.error(path.clone(), format_args!(".operationRef: {msg}"));
}
OperationRefResolution::ExternalPathItemRef(target)
if !ctx.is_option(Options::IgnoreExternalReferences) =>
{
ctx.error(
path.clone(),
format_args!(
".operationRef: target PathItem is a `$ref` to external document `{target}`, which is not supported"
),
);
}
OperationRefResolution::ExternalPathItemRef(_) => {}
}
} else if !ctx.is_option(Options::IgnoreExternalReferences) {
ctx.error(
path.clone(),
format_args!(
".operationRef: external reference `{operation_ref}` is not supported"
),
);
}
}
if let Some(server) = &self.server {
server.validate_with_context(ctx, format!("{path}.server"));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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};
use crate::validation::Context;
use crate::validation::ValidationErrorsExt;
use serde_json::json;
fn spec_with_pets_get() -> Spec {
let op = Operation {
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 mut ops = BTreeMap::new();
ops.insert("get".to_owned(), op);
let item = PathItem {
operations: Some(ops),
..Default::default()
};
let mut paths = Paths::default();
paths.paths.insert("/pets".to_owned(), item);
Spec {
paths: Some(paths),
..Default::default()
}
}
#[test]
fn round_trip_full() {
let v = json!({
"operationId": "getPet",
"parameters": {"id": "$response.body#/id"},
"requestBody": {"name": "fluffy"},
"description": "Linked",
"server": {"url": "https://example.com"},
"x-internal": true
});
let l: Link = serde_json::from_value(v.clone()).unwrap();
assert_eq!(serde_json::to_value(&l).unwrap(), v);
}
#[test]
fn xor_both_present_errors() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("ref".into()),
operation_id: Some("id".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors.mentions("mutually exclusive"),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn xor_neither_errors() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Link::default().validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("must specify exactly one")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn missing_operation_id_reported() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_id: Some("nonexistent".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("missing operation with id `nonexistent`")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_internal_resolves() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"valid ref should not error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_unknown_path_errors() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1users/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains(".operationRef") && e.contains("`/users` not declared")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_unknown_method_errors() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/post".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors.mentions("method `post`"),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_bad_prefix_errors() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/schemas/Foo".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("must start with `#/paths/`")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_unescaped_slash_malformed() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths//pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_external_unsupported() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("https://example.com/spec.yaml#/paths/~1pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("external reference") && e.contains("not supported")),
"errors: {:?}",
ctx.errors
);
let mut ctx = Context::new(&spec, Options::IgnoreExternalReferences.only());
Link {
operation_ref: Some("https://example.com/spec.yaml#/paths/~1pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions("external reference"),
"with option: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_empty_errors() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains(".operationRef") && e.contains("must not be empty")),
"errors: {:?}",
ctx.errors
);
}
fn ok_responses() -> Responses {
Responses {
responses: Some(BTreeMap::from([(
"200".to_owned(),
RefOr::new_item(Response {
description: Some("ok".into()),
..Default::default()
}),
)])),
..Default::default()
}
}
fn pi_with_get() -> PathItem {
let mut ops = BTreeMap::new();
ops.insert(
"get".to_owned(),
Operation {
responses: Some(ok_responses()),
..Default::default()
},
);
PathItem {
operations: Some(ops),
..Default::default()
}
}
#[test]
fn operation_ref_into_components_path_items() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([("Reusable".to_owned(), pi_with_get())])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/Reusable/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"components.pathItems target should resolve: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_into_webhooks() {
let mut webhooks = Paths::default();
webhooks
.paths
.insert("petCreated".to_owned(), pi_with_get());
let spec = Spec {
webhooks: Some(webhooks),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/webhooks/petCreated/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"webhook target should resolve: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_chain_paths_to_components_pathitems() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([("Reusable".to_owned(), pi_with_get())])),
..Default::default()
};
let mut paths = Paths::default();
paths.paths.insert(
"/pets".to_owned(),
PathItem {
reference: Some("#/components/pathItems/Reusable".into()),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"cross-container chain should resolve: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_cycle_errors() {
let mut paths = 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 = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1a/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors.mentions("cyclic `$ref` chain"),
"expected cycle error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_dangling_components_path_items_target() {
let spec = spec_with_pets_get();
let mut spec = spec;
let mut paths = spec.paths.take().unwrap();
paths.paths.insert(
"/pets".to_owned(),
PathItem {
reference: Some("#/components/pathItems/Missing".into()),
..Default::default()
},
);
spec.paths = Some(paths);
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("not declared in `#/components/pathItems`")),
"expected dangling-component-pathItem error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_into_components_callbacks() {
use crate::v3_2::callback::Callback;
use crate::v3_2::components::Components;
let mut cb_paths = BTreeMap::new();
cb_paths.insert("{$request.body#/cb}".to_owned(), pi_with_get());
let cb = Callback {
paths: cb_paths,
..Default::default()
};
let comp = Components {
callbacks: Some(BTreeMap::from([("OnPing".to_owned(), RefOr::new_item(cb))])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/callbacks/OnPing/{$request.body#~1cb}/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"callback target should resolve: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_into_callbacks_unknown_expression_errors() {
use crate::v3_2::callback::Callback;
use crate::v3_2::components::Components;
let comp = Components {
callbacks: Some(BTreeMap::from([(
"OnPing".to_owned(),
RefOr::new_item(Callback::default()),
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/callbacks/OnPing/missing/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("expression `missing`") && e.contains("OnPing")),
"expected unknown-expression error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_into_callbacks_unknown_callback_errors() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/callbacks/Missing/x/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("callback `Missing` not declared")),
"expected dangling-callback error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_into_inline_path_op_callback() {
use crate::v3_2::callback::Callback;
let mut cb_paths = BTreeMap::new();
cb_paths.insert("{$request.query.callbackUrl}".to_owned(), pi_with_get());
let cb = Callback {
paths: cb_paths,
..Default::default()
};
let mut callbacks = BTreeMap::new();
callbacks.insert("myCb".to_owned(), RefOr::new_item(cb));
let op = Operation {
responses: Some(ok_responses()),
callbacks: Some(callbacks),
..Default::default()
};
let mut ops = BTreeMap::new();
ops.insert("post".to_owned(), op);
let mut paths = Paths::default();
paths.paths.insert(
"/subscribe".to_owned(),
PathItem {
operations: Some(ops),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some(
"#/paths/~1subscribe/post/callbacks/myCb/{$request.query.callbackUrl}/get".into(),
),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"deep callback target should resolve: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_inline_callback_unknown_callback_errors() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get/callbacks/missing/expr/post".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("callback `missing` not declared on")),
"expected unknown-inline-callback error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_deep_pointer_malformed_continuation_errors() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get/extra".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed deep pointer")),
"expected malformed-deep-pointer error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_method_token_is_case_sensitive() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/GET".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("method `GET` not declared")),
"expected case-sensitive method error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_inline_callback_name_unescaped() {
use crate::v3_2::callback::Callback;
let mut cb_paths = BTreeMap::new();
cb_paths.insert("expr".to_owned(), pi_with_get());
let cb = Callback {
paths: cb_paths,
..Default::default()
};
let mut callbacks = BTreeMap::new();
callbacks.insert("weird/name".to_owned(), RefOr::new_item(cb));
let op = Operation {
responses: Some(ok_responses()),
callbacks: Some(callbacks),
..Default::default()
};
let mut ops = BTreeMap::new();
ops.insert("post".to_owned(), op);
let mut paths = Paths::default();
paths.paths.insert(
"/pets".to_owned(),
PathItem {
operations: Some(ops),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/post/callbacks/weird~1name/expr/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"callback with `/` in name should resolve via `~1`: {:?}",
ctx.errors
);
}
#[test]
fn path_item_ref_chain_target_in_components_callbacks() {
use crate::v3_2::callback::Callback;
use crate::v3_2::components::Components;
let mut cb_paths = BTreeMap::new();
cb_paths.insert("e".to_owned(), pi_with_get());
let cb = Callback {
paths: cb_paths,
..Default::default()
};
let comp = Components {
callbacks: Some(BTreeMap::from([("CB".to_owned(), RefOr::new_item(cb))])),
..Default::default()
};
let mut paths = Paths::default();
paths.paths.insert(
"/pets".to_owned(),
PathItem {
reference: Some("#/components/callbacks/CB/e".into()),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"chain through components.callbacks must resolve: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_into_components_path_items_marks_visited() {
use crate::v3_2::components::Components;
let mut cp = BTreeMap::new();
cp.insert("Reusable".to_owned(), pi_with_get());
let comp = Components {
path_items: Some(cp),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::empty());
Link {
operation_ref: Some("#/components/pathItems/Reusable/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.is_visited("#/components/pathItems/Reusable"),
"components.pathItems target should be marked visited"
);
}
#[test]
fn operation_ref_into_components_callbacks_marks_visited() {
use crate::v3_2::callback::Callback;
use crate::v3_2::components::Components;
let mut cb_paths = BTreeMap::new();
cb_paths.insert("e".to_owned(), pi_with_get());
let cb = Callback {
paths: cb_paths,
..Default::default()
};
let comp = Components {
callbacks: Some(BTreeMap::from([("CB".to_owned(), RefOr::new_item(cb))])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::empty());
Link {
operation_ref: Some("#/components/callbacks/CB/e/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.is_visited("#/components/callbacks/CB"),
"components.callbacks target should be marked visited"
);
}
#[test]
fn ref_chain_cross_container_same_key_not_cycle() {
use crate::v3_2::components::Components;
let mut cp = BTreeMap::new();
cp.insert("Foo".to_owned(), pi_with_get());
let comp = Components {
path_items: Some(cp),
..Default::default()
};
let mut webhooks = Paths::default();
webhooks.paths.insert(
"Foo".to_owned(),
PathItem {
reference: Some("#/components/pathItems/Foo".into()),
..Default::default()
},
);
let spec = Spec {
webhooks: Some(webhooks),
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/webhooks/Foo/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions("cyclic"),
"cross-container same-key must not be flagged as cycle: {:?}",
ctx.errors
);
assert!(
!ctx.errors.mentions(".operationRef"),
"operationRef should resolve: {:?}",
ctx.errors
);
}
#[test]
fn server_validates_when_operation_ref_set() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get".into()),
server: Some(crate::v3_2::server::Server {
url: "".into(),
..Default::default()
}),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors.mentions("server.url"),
"expected server.url error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_paths_too_few_parts_malformed() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets".into()), ..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed pointer error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_webhooks_too_few_parts_malformed() {
let mut webhooks = Paths::default();
webhooks.paths.insert("event".to_owned(), pi_with_get());
let spec = Spec {
webhooks: Some(webhooks),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/webhooks/event".into()), ..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed pointer error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_webhooks_unknown_key_errors() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/webhooks/missing/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("webhook `missing` not declared")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_components_path_items_too_few_parts() {
use crate::v3_2::components::Components;
let spec = Spec {
components: Some(Components {
path_items: Some(BTreeMap::from([("Reusable".to_owned(), pi_with_get())])),
..Default::default()
}),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/Reusable".into()), ..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed pointer error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_components_path_items_unknown_key_errors() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/Missing/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("path item `Missing` not declared")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_components_callbacks_too_few_parts() {
use crate::v3_2::callback::Callback;
use crate::v3_2::components::Components;
let spec = Spec {
components: Some(Components {
callbacks: Some(BTreeMap::from([(
"OnPing".to_owned(),
RefOr::new_item(Callback::default()),
)])),
..Default::default()
}),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/callbacks/OnPing/expr".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed pointer for too-few-parts: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_components_callbacks_cb_ref_external_errors() {
use crate::v3_2::components::Components;
let spec = Spec {
components: Some(Components {
callbacks: Some(BTreeMap::from([(
"OnPing".to_owned(),
RefOr::new_ref("https://external.example.com/spec.yaml#/cb".to_owned()),
)])),
..Default::default()
}),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/callbacks/OnPing/expr/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("external document") || e.contains("not supported")),
"expected external ref error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_components_callbacks_cb_ref_not_found_errors() {
use crate::v3_2::components::Components;
let spec = Spec {
components: Some(Components {
callbacks: Some(BTreeMap::from([(
"OnPing".to_owned(),
RefOr::new_ref("#/components/callbacks/Missing".to_owned()),
)])),
..Default::default()
}),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/callbacks/OnPing/expr/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("callback `OnPing` is a `$ref`") && e.contains("not declared")),
"expected not-found callback ref error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_consumed_equals_parts_length_malformed() {
let spec = spec_with_pets_get();
let mut ctx = Context::new(&spec, Options::new());
let _ = (&spec, &mut ctx);
}
#[test]
fn operation_ref_path_item_ref_empty_errors() {
let mut paths = Paths::default();
paths.paths.insert(
"/pets".to_owned(),
PathItem {
reference: Some("".into()), ..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors.iter().any(|e| e.contains("empty `$ref`")),
"expected empty-$ref error in ref chain: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_paths_with_slash_errors() {
let mut paths = Paths::default();
paths.paths.insert(
"/pets".to_owned(),
PathItem {
reference: Some("#/paths/a/b".into()), ..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1pets/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed pointer from path-item chain: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_no_paths_on_spec_errors() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"Ref".to_owned(),
PathItem {
reference: Some("#/paths/~1missing".into()),
..Default::default()
},
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/Ref/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("no `paths`") || e.contains("not declared")),
"expected error when spec has no paths and $ref points to paths: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_path_not_declared_errors() {
let mut paths = Paths::default();
paths.paths.insert(
"/a".to_owned(),
PathItem {
reference: Some("#/paths/~1missing".into()),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1a/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("not declared in `#/paths`")),
"expected not-declared-in-paths error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_webhooks_with_slash_errors() {
let mut webhooks = Paths::default();
webhooks.paths.insert(
"evt".to_owned(),
PathItem {
reference: Some("#/webhooks/a/b".into()), ..Default::default()
},
);
let spec = Spec {
webhooks: Some(webhooks),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/webhooks/evt/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed pointer from webhook-ref chain: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_webhook_not_declared_errors() {
let mut webhooks = Paths::default();
webhooks.paths.insert(
"evt".to_owned(),
PathItem {
reference: Some("#/webhooks/missing".into()),
..Default::default()
},
);
let spec = Spec {
webhooks: Some(webhooks),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/webhooks/evt/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("not declared in `#/webhooks`")),
"expected not-declared-in-webhooks error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_components_path_items_with_slash_errors() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"A".to_owned(),
PathItem {
reference: Some("#/components/pathItems/x/y".into()), ..Default::default()
},
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/A/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed pointer in components.pathItems chain: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_components_path_items_not_declared_errors() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"A".to_owned(),
PathItem {
reference: Some("#/components/pathItems/Missing".into()),
..Default::default()
},
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/A/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("not declared in `#/components/pathItems`")),
"expected not-declared in components.pathItems chain: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_callbacks_malformed_no_expr_errors() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"A".to_owned(),
PathItem {
reference: Some("#/components/callbacks/OnlyOneName".into()),
..Default::default()
},
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/A/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed-pointer error for callback with no expr: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_callbacks_expr_with_slash_errors() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"A".to_owned(),
PathItem {
reference: Some("#/components/callbacks/CB/expr/extra".into()),
..Default::default()
},
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/A/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("malformed JSON Pointer")),
"expected malformed-pointer error for callback expr with extra slash: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_callbacks_cb_not_declared_errors() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"A".to_owned(),
PathItem {
reference: Some("#/components/callbacks/MissingCb/e".into()),
..Default::default()
},
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/A/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("callback `MissingCb` is not declared")),
"expected not-declared callback error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_callbacks_cb_external_errors() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"A".to_owned(),
PathItem {
reference: Some("#/components/callbacks/ExtCb/e".into()),
..Default::default()
},
)])),
callbacks: Some(BTreeMap::from([(
"ExtCb".to_owned(),
RefOr::new_ref("https://external.example.com/spec.yaml#/cb".to_owned()),
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/A/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors.iter().any(|e| e.contains("external document")
|| e.contains("not supported")
|| e.contains("ExternalPathItemRef")),
"expected external-ref error from callback chain: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_callbacks_cb_not_found_errors() {
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"A".to_owned(),
PathItem {
reference: Some("#/components/callbacks/RefCb/e".into()),
..Default::default()
},
)])),
callbacks: Some(BTreeMap::from([(
"RefCb".to_owned(),
RefOr::new_ref("#/components/callbacks/DoesNotExist".to_owned()),
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/A/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("resolves to") && e.contains("not declared")
|| e.contains("is a `$ref`")),
"expected not-found callback ref error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_callbacks_expr_not_declared_errors() {
use crate::v3_2::callback::Callback;
use crate::v3_2::components::Components;
let comp = Components {
path_items: Some(BTreeMap::from([(
"A".to_owned(),
PathItem {
reference: Some("#/components/callbacks/CB/missing_expr".into()),
..Default::default()
},
)])),
callbacks: Some(BTreeMap::from([(
"CB".to_owned(),
RefOr::new_item(Callback::default()),
)])),
..Default::default()
};
let spec = Spec {
components: Some(comp),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/components/pathItems/A/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("expression `missing_expr`") && e.contains("CB")),
"expected missing-expression error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_external_returns_external_ref() {
let mut paths = Paths::default();
paths.paths.insert(
"/a".to_owned(),
PathItem {
reference: Some("https://external.example.com/spec.yaml#/paths/~1pets".into()),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1a/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("external document") || e.contains("not supported")),
"expected external-doc error from path item chain: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_external_ref_suppressed_with_ignore_option() {
let mut paths = Paths::default();
paths.paths.insert(
"/a".to_owned(),
PathItem {
reference: Some("https://external.example.com/spec.yaml#/paths/~1a".into()),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1a/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("external document") || e.contains("not supported")),
"expected external-doc error: {:?}",
ctx.errors
);
let mut ctx2 = Context::new(&spec, Options::IgnoreExternalReferences.only());
Link {
operation_ref: Some("#/paths/~1a/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx2, "l".into());
assert!(
!ctx2.errors.mentions(".operationRef"),
"with IgnoreExternalReferences the external ref should be silently ignored: {:?}",
ctx2.errors
);
}
#[test]
fn operation_ref_inline_callback_external_cb_ref_suppressed() {
let mut callbacks = BTreeMap::new();
callbacks.insert(
"ext_cb".to_owned(),
RefOr::new_ref("https://external.example.com/spec.yaml#/cb".to_owned()),
);
let op = Operation {
responses: Some(ok_responses()),
callbacks: Some(callbacks),
..Default::default()
};
let mut ops = BTreeMap::new();
ops.insert("post".to_owned(), op);
let mut paths = Paths::default();
paths.paths.insert(
"/sub".to_owned(),
PathItem {
operations: Some(ops),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1sub/post/callbacks/ext_cb/expr/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("external document") || e.contains("not supported")),
"expected external-doc error from inline callback chain: {:?}",
ctx.errors
);
let mut ctx2 = Context::new(&spec, Options::IgnoreExternalReferences.only());
Link {
operation_ref: Some("#/paths/~1sub/post/callbacks/ext_cb/expr/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx2, "l".into());
assert!(
!ctx2.errors.mentions(".operationRef"),
"with ignore option external ref should be silent: {:?}",
ctx2.errors
);
}
#[test]
fn operation_ref_inline_callback_cb_not_found_errors() {
let mut callbacks = BTreeMap::new();
callbacks.insert(
"ref_cb".to_owned(),
RefOr::new_ref("#/components/callbacks/DoesNotExist".to_owned()),
);
let op = Operation {
responses: Some(ok_responses()),
callbacks: Some(callbacks),
..Default::default()
};
let mut ops = BTreeMap::new();
ops.insert("post".to_owned(), op);
let mut paths = Paths::default();
paths.paths.insert(
"/sub".to_owned(),
PathItem {
operations: Some(ops),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1sub/post/callbacks/ref_cb/expr/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("is a `$ref`") && e.contains("not declared")),
"expected not-found callback ref error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_inline_callback_expr_not_declared_errors() {
use crate::v3_2::callback::Callback;
let mut callbacks = BTreeMap::new();
callbacks.insert("myCb".to_owned(), RefOr::new_item(Callback::default()));
let op = Operation {
responses: Some(ok_responses()),
callbacks: Some(callbacks),
..Default::default()
};
let mut ops = BTreeMap::new();
ops.insert("post".to_owned(), op);
let mut paths = Paths::default();
paths.paths.insert(
"/sub".to_owned(),
PathItem {
operations: Some(ops),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1sub/post/callbacks/myCb/missing_expr/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("expression `missing_expr`") && e.contains("myCb")),
"expected missing-expression error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_deep_method_not_declared_on_intermediate_item() {
use crate::v3_2::callback::Callback;
let mut cb_paths = BTreeMap::new();
cb_paths.insert("{url}".to_owned(), pi_with_get());
let cb = Callback {
paths: cb_paths,
..Default::default()
};
let mut callbacks = BTreeMap::new();
callbacks.insert("myCb".to_owned(), RefOr::new_item(cb));
let op = Operation {
responses: Some(ok_responses()),
callbacks: Some(callbacks),
..Default::default()
};
let mut ops = BTreeMap::new();
ops.insert("post".to_owned(), op);
let mut paths = Paths::default();
paths.paths.insert(
"/sub".to_owned(),
PathItem {
operations: Some(ops),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1sub/delete/callbacks/myCb/{url}/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("method `delete` not declared")),
"expected method-not-declared error: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_deep_callback_chain_error_propagates() {
use crate::v3_2::callback::Callback;
let mut cb_paths = BTreeMap::new();
cb_paths.insert(
"{url}".to_owned(),
PathItem {
reference: Some("".into()), ..Default::default()
},
);
let cb = Callback {
paths: cb_paths,
..Default::default()
};
let mut callbacks = BTreeMap::new();
callbacks.insert("myCb".to_owned(), RefOr::new_item(cb));
let op = Operation {
responses: Some(ok_responses()),
callbacks: Some(callbacks),
..Default::default()
};
let mut ops = BTreeMap::new();
ops.insert("post".to_owned(), op);
let mut paths = Paths::default();
paths.paths.insert(
"/sub".to_owned(),
PathItem {
operations: Some(ops),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1sub/post/callbacks/myCb/{url}/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
ctx.errors.iter().any(|e| e.contains("empty `$ref`")),
"expected chain-error propagation from empty ref: {:?}",
ctx.errors
);
}
#[test]
fn operation_ref_path_item_ref_webhooks_resolved_successfully() {
let mut webhooks = Paths::default();
webhooks.paths.insert("evt".to_owned(), pi_with_get());
let mut paths = Paths::default();
paths.paths.insert(
"/a".to_owned(),
PathItem {
reference: Some("#/webhooks/evt".into()),
..Default::default()
},
);
let spec = Spec {
paths: Some(paths),
webhooks: Some(webhooks),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
Link {
operation_ref: Some("#/paths/~1a/get".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "l".into());
assert!(
!ctx.errors.mentions(".operationRef"),
"webhook $ref chain should resolve: {:?}",
ctx.errors
);
}
}