use crate::common::helpers::{Context, PushError, ValidateWithContext, validate_required_string};
use crate::common::reference::RefOr;
use crate::v3_1::callback::Callback;
use crate::v3_1::external_documentation::ExternalDocumentation;
use crate::v3_1::parameter::Parameter;
use crate::v3_1::request_body::RequestBody;
use crate::v3_1::response::Responses;
use crate::v3_1::server::Server;
use crate::v3_1::spec::Spec;
use crate::v3_1::tag::Tag;
use crate::validation::Options;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Operation {
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[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<Vec<RefOr<Parameter>>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "requestBody")]
pub request_body: Option<RefOr<RequestBody>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub responses: Option<Responses>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callbacks: Option<BTreeMap<String, RefOr<Callback>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub servers: Option<Vec<Server>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-codeSamples", alias = "x-code-samples")]
pub x_code_samples: Option<Vec<CodeSample>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-tags")]
pub x_tags: Option<Vec<String>>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct CodeSample {
pub lang: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub source: String,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext<Spec> for Operation {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(tags) = &self.tags {
for (i, tag) in tags.iter().enumerate() {
let path = format!("{path}.tags[{i}]");
validate_required_string(tag, ctx, path.clone());
if tag.is_empty() {
continue;
}
let reference = format!("#/tags/{tag}");
if let Ok(spec_tag) = RefOr::<Tag>::new_ref(reference.clone()).get_item(ctx.spec) {
if ctx.visit(reference.clone()) {
spec_tag.validate_with_context(ctx, reference);
}
} else if !ctx.is_option(Options::IgnoreMissingTags) {
ctx.error(path, format_args!(".tags[{i}]: `{tag}` not found in spec"));
}
}
}
if let Some(parameters) = &self.parameters {
for (i, parameter) in parameters.clone().iter().enumerate() {
parameter.validate_with_context(ctx, format!("{path}.parameters[{i}]"));
}
}
if let Some(request_body) = &self.request_body {
request_body.validate_with_context(ctx, format!("{path}.requestBody"));
}
if let Some(servers) = &self.servers {
for (i, server) in servers.iter().enumerate() {
server.validate_with_context(ctx, format!("{path}.servers[{i}]"));
}
}
if let Some(samples) = &self.x_code_samples {
for (i, sample) in samples.iter().enumerate() {
sample.validate_with_context(ctx, format!("{path}.x-codeSamples[{i}]"));
}
}
if let Some(tags) = &self.x_tags {
for (i, tag) in tags.iter().enumerate() {
validate_required_string(tag, ctx, format!("{path}.x-tags[{i}]"));
}
}
if let Some(callbacks) = &self.callbacks {
for (k, v) in callbacks {
v.validate_with_context(ctx, format!("{path}.callbacks[{k}]"));
}
}
match &self.responses {
Some(r) => r.validate_with_context(ctx, format!("{path}.responses")),
None => ctx.error(path.clone(), ".responses: required field is missing"),
}
if let Some(external_doc) = &self.external_docs {
external_doc.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(sec) = &self.security {
crate::v3_1::validation::validate_security_requirements(
ctx,
&format!("{path}.security"),
sec,
);
}
}
}
impl ValidateWithContext<Spec> for CodeSample {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.lang, ctx, format!("{path}.lang"));
validate_required_string(&self.source, ctx, format!("{path}.source"));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::helpers::Context;
use crate::v3_1::response::{Response, Responses};
use crate::v3_1::tag::Tag;
fn ok_responses() -> Responses {
Responses {
responses: Some(BTreeMap::from([(
"200".to_owned(),
RefOr::new_item(Response {
description: "ok".into(),
..Default::default()
}),
)])),
..Default::default()
}
}
#[test]
fn missing_responses_required_field_reported() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Operation::default().validate_with_context(&mut ctx, "op".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("op.responses: required field is missing")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn validate_walks_tags_servers_external_docs() {
let spec = Spec {
tags: Some(vec![Tag {
name: "pets".into(),
..Default::default()
}]),
..Default::default()
};
let op = Operation {
tags: Some(vec!["pets".into(), "".into(), "missing".into()]),
servers: Some(vec![Server {
url: "".into(),
..Default::default()
}]),
external_docs: Some(ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
}),
responses: Some(ok_responses()),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
op.validate_with_context(&mut ctx, "op".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("op.tags[1]") && e.contains("must not be empty")),
"empty tag: {:?}",
ctx.errors
);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("`missing` not found in spec")),
"missing tag: {:?}",
ctx.errors
);
assert!(
ctx.errors.iter().any(|e| e.contains("op.servers[0].url")),
"server.url: {:?}",
ctx.errors
);
assert!(
ctx.errors.iter().any(|e| e.contains("op.externalDocs.url")),
"externalDocs.url: {:?}",
ctx.errors
);
let mut ctx = Context::new(&spec, Options::IgnoreMissingTags.only());
op.validate_with_context(&mut ctx, "op".into());
assert!(
ctx.errors.iter().all(|e| !e.contains("not found in spec")),
"missing-tags should be silenced: {:?}",
ctx.errors
);
}
#[test]
fn op_level_security_runs_through_helper() {
let spec = Spec::default();
let op = Operation {
responses: Some(ok_responses()),
security: Some(vec![{
let mut req = BTreeMap::new();
req.insert("missing-scheme".to_owned(), vec![]);
req
}]),
..Default::default()
};
let mut ctx = Context::new(&spec, Options::new());
op.validate_with_context(&mut ctx, "op".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("op.security") && e.contains("missing-scheme")),
"expected op-level security error: {:?}",
ctx.errors
);
}
#[test]
fn documentation_extensions_round_trip_and_validate() {
let value = serde_json::json!({
"responses": {
"200": {
"description": "OK"
}
},
"x-codeSamples": [
{
"lang": "curl",
"label": "cURL",
"source": "curl https://example.com/pets"
}
],
"x-tags": ["sdk", "docs"]
});
let operation: Operation = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(&operation).unwrap(), value);
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
operation.validate_with_context(&mut ctx, "operation".to_owned());
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let alias_value = serde_json::json!({
"responses": {
"200": {
"description": "OK"
}
},
"x-code-samples": [
{
"lang": "rust",
"source": "println!(\"ok\");"
}
]
});
let operation: Operation = serde_json::from_value(alias_value).unwrap();
assert_eq!(operation.x_code_samples.as_ref().unwrap()[0].lang, "rust");
let mut ctx = Context::new(&spec, Options::new());
CodeSample::default().validate_with_context(&mut ctx, "sample".to_owned());
assert_eq!(ctx.errors.len(), 2, "expected lang/source errors");
}
}