use crate::common::helpers::validate_required_string;
use crate::v3_2::external_documentation::ExternalDocumentation;
use crate::v3_2::spec::Spec;
use crate::validation::{Context, PushError, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct Tag {
pub name: 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")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext<Spec> for Tag {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}.name"));
if let Some(doc) = &self.external_docs {
doc.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(parent) = &self.parent {
if parent.is_empty() {
ctx.error(format!("{path}.parent"), "must not be empty");
} else if parent == &self.name {
ctx.error(
format!("{path}.parent"),
"tag must not name itself as its parent",
);
} else if !ctx
.spec
.tags
.as_ref()
.is_some_and(|tags| tags.iter().any(|t| t.name == *parent))
{
ctx.error(
format!("{path}.parent"),
format_args!("`{parent}` is not declared in `#/tags`"),
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize() {
assert_eq!(
serde_json::from_value::<Tag>(serde_json::json!({
"name": "pet",
"description": "Pets operations",
"externalDocs": {
"description": "Find more info here",
"url": "https://example.com/about"
},
}))
.unwrap(),
Tag {
name: String::from("pet"),
description: Some(String::from("Pets operations")),
external_docs: Some(ExternalDocumentation {
description: Some(String::from("Find more info here")),
url: String::from("https://example.com/about"),
..Default::default()
}),
..Default::default()
},
"deserialize name, description and externalDocs"
);
assert_eq!(
serde_json::from_value::<Tag>(serde_json::json!({
"name": "pet",
"description": "Pets operations",
}))
.unwrap(),
Tag {
name: String::from("pet"),
description: Some(String::from("Pets operations")),
..Default::default()
},
"deserialize name and description"
);
assert_eq!(
serde_json::from_value::<Tag>(serde_json::json!({
"name": "pet",
}))
.unwrap(),
Tag {
name: String::from("pet"),
..Default::default()
},
"deserialize name only"
);
}
#[test]
fn serialize() {
assert_eq!(
serde_json::to_value(Tag {
name: String::from("pet"),
description: Some(String::from("Pets operations")),
external_docs: Some(ExternalDocumentation {
description: Some(String::from("Find more info here")),
url: String::from("https://example.com/about"),
..Default::default()
}),
..Default::default()
})
.unwrap(),
serde_json::json!({
"name": "pet",
"description":"Pets operations",
"externalDocs": {
"description": "Find more info here",
"url": "https://example.com/about"
},
}),
"serialize name, description and externalDocs",
);
assert_eq!(
serde_json::to_value(Tag {
name: String::from("pet"),
description: Some(String::from("Pets operations")),
..Default::default()
})
.unwrap(),
serde_json::json!({
"name": "pet",
"description":"Pets operations",
}),
"serialize name and description",
);
assert_eq!(
serde_json::to_value(Tag {
name: String::from("pet"),
..Default::default()
})
.unwrap(),
serde_json::json!({
"name": "pet",
}),
"serialize name only",
);
}
#[test]
fn validate() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Default::default());
Tag {
name: String::from("pet"),
description: Some(String::from("Pets operations")),
external_docs: Some(ExternalDocumentation {
description: Some(String::from("Find more info here")),
url: String::from("https://example.com/about"),
..Default::default()
}),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("tag"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
Tag {
name: String::from("pet"),
description: Some(String::from("Pets operations")),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("tag"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
Tag {
name: String::from("pet"),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("tag"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
Tag {
..Default::default()
}
.validate_with_context(&mut ctx, String::from("tag"));
assert_eq!(
ctx.errors,
vec!["tag.name: must not be empty"],
"name error: {:?}",
ctx.errors
);
}
#[test]
fn summary_kind_parent_round_trip() {
let v = serde_json::json!({
"name": "international",
"summary": "International",
"description": "Cross-border flights",
"kind": "nav",
"parent": "flights"
});
let tag: Tag = serde_json::from_value(v.clone()).unwrap();
assert_eq!(tag.summary.as_deref(), Some("International"));
assert_eq!(tag.kind.as_deref(), Some("nav"));
assert_eq!(tag.parent.as_deref(), Some("flights"));
assert_eq!(serde_json::to_value(&tag).unwrap(), v);
}
#[test]
fn parent_must_exist_in_spec_tags() {
let spec = Spec {
tags: Some(vec![Tag {
name: "flights".into(),
..Default::default()
}]),
..Default::default()
};
let mut ctx = Context::new(&spec, crate::validation::Options::new());
Tag {
name: "international".into(),
parent: Some("flights".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "t".into());
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let mut ctx = Context::new(&spec, crate::validation::Options::new());
Tag {
name: "x".into(),
parent: Some("missing".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "t".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("parent") && e.contains("missing")),
"errors: {:?}",
ctx.errors
);
let mut ctx = Context::new(&spec, crate::validation::Options::new());
Tag {
name: "loop".into(),
parent: Some("loop".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "t".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("must not name itself")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn x_display_name_round_trip_via_generic_extensions() {
let tag: Tag = serde_json::from_value(serde_json::json!({
"name": "pet",
"x-displayName": "Pets"
}))
.unwrap();
assert_eq!(
tag.extensions.as_ref().and_then(|m| m.get("x-displayName")),
Some(&serde_json::Value::String("Pets".to_owned()))
);
assert_eq!(
serde_json::to_value(tag).unwrap(),
serde_json::json!({
"name": "pet",
"x-displayName": "Pets"
})
);
}
#[test]
fn parent_empty_string_errors() {
use crate::v3_2::spec::Spec;
use crate::validation::Context;
let spec = Spec::default();
let mut ctx = Context::new(&spec, crate::validation::Options::new());
Tag {
name: "pets".into(),
parent: Some("".into()),
..Default::default()
}
.validate_with_context(&mut ctx, "t".into());
assert!(
ctx.errors.iter().any(|e| e.contains("must not be empty")),
"expected empty-parent error: {:?}",
ctx.errors
);
}
}