Skip to main content

salvo_oapi/openapi/
tag.rs

1//! Implements [OpenAPI Tag Object][tag] types.
2//!
3//! [tag]: https://spec.openapis.org/oas/latest.html#tag-object
4use std::cmp::Ordering;
5
6use serde::{Deserialize, Serialize};
7
8use super::external_docs::ExternalDocs;
9use crate::PropMap;
10
11/// Implements [OpenAPI Tag Object][tag].
12///
13/// Tag can be used to provide additional metadata for tags used by path operations.
14///
15/// [tag]: https://spec.openapis.org/oas/latest.html#tag-object
16#[non_exhaustive]
17#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
18#[serde(rename_all = "camelCase")]
19pub struct Tag {
20    /// Name of the tag. Should match to tag of **operation**.
21    pub name: String,
22
23    /// Short summary of the tag, used for display purposes. Added in OpenAPI 3.2 as the
24    /// standardized replacement for the `x-displayName` extension.
25    ///
26    /// See <https://spec.openapis.org/oas/v3.2.0.html#tag-object>.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub summary: Option<String>,
29
30    /// Additional description for the tag shown in the document.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub description: Option<String>,
33
34    /// Additional external documentation for the tag.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub external_docs: Option<ExternalDocs>,
37
38    /// [`Tag::name`] of the tag this tag is nested under. Added in OpenAPI 3.2.
39    ///
40    /// The named tag must exist in the document and circular parent/child references are not
41    /// allowed; neither condition is validated here.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub parent: Option<String>,
44
45    /// Machine-readable string categorizing what sort of tag this is. Added in OpenAPI 3.2.
46    ///
47    /// Any string is allowed; commonly used values are `nav`, `badge` and `audience`. See the
48    /// [registry](https://spec.openapis.org/registry/tag-kind/) for the well-known values.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub kind: Option<String>,
51
52    /// Optional extensions "x-something"
53    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
54    pub extensions: PropMap<String, serde_json::Value>,
55}
56impl Ord for Tag {
57    fn cmp(&self, other: &Self) -> Ordering {
58        self.name.cmp(&other.name)
59    }
60}
61impl PartialOrd for Tag {
62    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
63        Some(self.cmp(other))
64    }
65}
66impl From<String> for Tag {
67    fn from(name: String) -> Self {
68        Self::new(name)
69    }
70}
71impl From<&String> for Tag {
72    fn from(name: &String) -> Self {
73        Self::new(name)
74    }
75}
76impl<'a> From<&'a str> for Tag {
77    fn from(name: &'a str) -> Self {
78        Self::new(name.to_owned())
79    }
80}
81
82impl Tag {
83    /// Construct a new [`Tag`] with given name.
84    #[must_use]
85    pub fn new(name: impl Into<String>) -> Self {
86        Self {
87            name: name.into(),
88            ..Default::default()
89        }
90    }
91    /// Add name of the tag.
92    #[must_use]
93    pub fn name(mut self, name: impl Into<String>) -> Self {
94        self.name = name.into();
95        self
96    }
97
98    /// Add a short summary used for display purposes. Requires OpenAPI 3.2.
99    #[must_use]
100    pub fn summary(mut self, summary: impl Into<String>) -> Self {
101        self.summary = Some(summary.into());
102        self
103    }
104
105    /// Add additional description for the tag.
106    #[must_use]
107    pub fn description(mut self, description: impl Into<String>) -> Self {
108        self.description = Some(description.into());
109        self
110    }
111
112    /// Nest this tag under the tag with the given name. Requires OpenAPI 3.2.
113    #[must_use]
114    pub fn parent(mut self, parent: impl Into<String>) -> Self {
115        self.parent = Some(parent.into());
116        self
117    }
118
119    /// Categorize the tag, e.g. `nav`, `badge` or `audience`. Requires OpenAPI 3.2.
120    #[must_use]
121    pub fn kind(mut self, kind: impl Into<String>) -> Self {
122        self.kind = Some(kind.into());
123        self
124    }
125
126    /// Add additional external documentation for the tag.
127    #[must_use]
128    pub fn external_docs(mut self, external_docs: ExternalDocs) -> Self {
129        self.external_docs = Some(external_docs);
130        self
131    }
132
133    /// Add openapi extension (`x-something`) for [`Tag`].
134    #[must_use]
135    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
136        self.extensions.insert(key.into(), value);
137        self
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::{ExternalDocs, Tag};
144
145    #[test]
146    fn tag_new() {
147        let tag = Tag::new("tag name");
148        assert_eq!(tag.name, "tag name");
149        assert!(tag.description.is_none());
150        assert!(tag.external_docs.is_none());
151        assert!(tag.extensions.is_empty());
152
153        let tag = tag.name("new tag name");
154        assert_eq!(tag.name, "new tag name");
155
156        let tag = tag.description("description");
157        assert!(tag.description.is_some());
158
159        let tag = tag.external_docs(ExternalDocs::new(""));
160        assert!(tag.external_docs.is_some());
161    }
162
163    #[test]
164    fn tag_openapi_3_2_fields_round_trip() {
165        let tag = Tag::new("partner")
166            .summary("Partner")
167            .description("Operations available to the partners network")
168            .parent("external")
169            .kind("audience");
170
171        let value = serde_json::to_value(&tag).expect("serialize");
172        assert_eq!(
173            value,
174            serde_json::json!({
175                "name": "partner",
176                "summary": "Partner",
177                "description": "Operations available to the partners network",
178                "parent": "external",
179                "kind": "audience"
180            })
181        );
182
183        let parsed: Tag = serde_json::from_value(value).expect("deserialize");
184        assert_eq!(parsed, tag);
185    }
186
187    #[test]
188    fn tag_3_1_output_is_unchanged() {
189        let tag = Tag::new("pets").description("pet operations");
190        assert_eq!(
191            serde_json::to_value(&tag).expect("serialize"),
192            serde_json::json!({ "name": "pets", "description": "pet operations" })
193        );
194    }
195
196    #[test]
197    fn from_string() {
198        let name = "tag name".to_owned();
199        let tag = Tag::from(name);
200        assert_eq!(tag.name, "tag name".to_owned());
201    }
202
203    #[test]
204    fn from_string_ref() {
205        let name = "tag name".to_owned();
206        let tag = Tag::from(&name);
207        assert_eq!(tag.name, "tag name".to_owned());
208    }
209
210    #[test]
211    fn from_str() {
212        let name = "tag name";
213        let tag = Tag::from(name);
214        assert_eq!(tag.name, "tag name");
215    }
216
217    #[test]
218    fn cmp() {
219        let tag1 = Tag::new("a");
220        let tag2 = Tag::new("b");
221
222        assert!(tag1 < tag2);
223    }
224}