Skip to main content

kynos_openapi/model/
info.rs

1//! The Info, Contact and License Objects.
2
3use serde::{Deserialize, Serialize};
4
5use crate::model::extensions::Extensions;
6
7/// Metadata about the API.
8#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Info {
10    /// The title of the API.
11    pub title: String,
12
13    /// A short summary of the API.
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub summary: Option<String>,
16
17    /// A description of the API. [CommonMark] syntax may be used.
18    ///
19    /// [CommonMark]: https://spec.commonmark.org/
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub description: Option<String>,
22
23    /// A URI for the Terms of Service for the API.
24    #[serde(
25        rename = "termsOfService",
26        default,
27        skip_serializing_if = "Option::is_none"
28    )]
29    pub terms_of_service: Option<String>,
30
31    /// Contact information for the exposed API.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub contact: Option<Contact>,
34
35    /// License information for the exposed API.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub license: Option<License>,
38
39    /// The version of *this API document*.
40    ///
41    /// This is not the OpenAPI specification version — that lives on
42    /// [`Document::openapi`](crate::Document::openapi) — nor is it required to
43    /// be the implementation's version.
44    pub version: String,
45
46    /// Specification extensions.
47    #[serde(flatten)]
48    pub extensions: Extensions,
49}
50
51impl Info {
52    /// Creates the required fields of an Info Object.
53    pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
54        Self {
55            title: title.into(),
56            version: version.into(),
57            ..Self::default()
58        }
59    }
60
61    /// Sets the short summary.
62    #[must_use]
63    pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
64        self.summary = Some(summary.into());
65        self
66    }
67
68    /// Sets the description.
69    #[must_use]
70    pub fn with_description(mut self, description: impl Into<String>) -> Self {
71        self.description = Some(description.into());
72        self
73    }
74
75    /// Sets the contact information.
76    #[must_use]
77    pub fn with_contact(mut self, contact: Contact) -> Self {
78        self.contact = Some(contact);
79        self
80    }
81
82    /// Sets the license.
83    #[must_use]
84    pub fn with_license(mut self, license: License) -> Self {
85        self.license = Some(license);
86        self
87    }
88}
89
90/// Contact information for the exposed API.
91#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
92pub struct Contact {
93    /// The identifying name of the contact person or organization.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub name: Option<String>,
96
97    /// A URI for the contact information.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub url: Option<String>,
100
101    /// The email address of the contact person or organization.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub email: Option<String>,
104
105    /// Specification extensions.
106    #[serde(flatten)]
107    pub extensions: Extensions,
108}
109
110/// License information for the exposed API.
111///
112/// Every version of the specification makes `identifier` and `url` mutually
113/// exclusive, and makes both optional beside a required `name`. This type holds
114/// at most one of the two, so the three states the specification allows are the
115/// three a program can build — and a document setting both cannot be
116/// constructed, nor parsed, nor emitted.
117///
118/// Reach for [`spdx`](License::spdx) in preference to
119/// [`with_url`](License::with_url): an SPDX expression is machine-readable and a
120/// URL is not. Both are permitted, which is why both are here.
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(try_from = "RawLicense", into = "RawLicense")]
123pub struct License {
124    name: String,
125    link: Option<LicenseLink>,
126
127    /// Specification extensions.
128    pub extensions: Extensions,
129}
130
131/// How a license points at its terms, when it does.
132///
133/// An enum rather than two `Option` fields, for the reason
134/// [`SecurityScheme`](crate::model::security::SecurityScheme) is one: an
135/// unusable combination that cannot be spelled needs no rule to reject it.
136#[derive(Clone, Debug, PartialEq, Eq)]
137enum LicenseLink {
138    /// An [SPDX] license expression.
139    ///
140    /// [SPDX]: https://spdx.org/licenses/
141    Spdx(String),
142
143    /// A URI for the license text.
144    Url(String),
145}
146
147impl License {
148    /// Creates a license identified by name alone.
149    ///
150    /// Valid, and the weakest of the three: a consumer gets something to show a
151    /// human and nothing to resolve.
152    pub fn named(name: impl Into<String>) -> Self {
153        Self {
154            name: name.into(),
155            link: None,
156            extensions: Extensions::default(),
157        }
158    }
159
160    /// Creates a license identified by an SPDX expression.
161    ///
162    /// This is preferred over [`License::with_url`]: an SPDX identifier is
163    /// machine-readable, a URL is not.
164    pub fn spdx(name: impl Into<String>, identifier: impl Into<String>) -> Self {
165        Self {
166            link: Some(LicenseLink::Spdx(identifier.into())),
167            ..Self::named(name)
168        }
169    }
170
171    /// Creates a license identified by a URL.
172    pub fn with_url(name: impl Into<String>, url: impl Into<String>) -> Self {
173        Self {
174            link: Some(LicenseLink::Url(url.into())),
175            ..Self::named(name)
176        }
177    }
178
179    /// The license name used for the API.
180    #[must_use]
181    pub fn name(&self) -> &str {
182        &self.name
183    }
184
185    /// The [SPDX] license expression, when this license carries one.
186    ///
187    /// [SPDX]: https://spdx.org/licenses/
188    #[must_use]
189    pub fn identifier(&self) -> Option<&str> {
190        match &self.link {
191            Some(LicenseLink::Spdx(identifier)) => Some(identifier),
192            _ => None,
193        }
194    }
195
196    /// The URI for the license, when this license carries one.
197    #[must_use]
198    pub fn url(&self) -> Option<&str> {
199        match &self.link {
200            Some(LicenseLink::Url(url)) => Some(url),
201            _ => None,
202        }
203    }
204}
205
206/// The wire shape: two flat optional fields, as the specification writes them.
207///
208/// The exclusion is enforced crossing this boundary rather than inside
209/// [`License`], so the invariant holds for a parsed document as well as a built
210/// one.
211#[derive(Serialize, Deserialize)]
212struct RawLicense {
213    name: String,
214
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    identifier: Option<String>,
217
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    url: Option<String>,
220
221    #[serde(flatten)]
222    extensions: Extensions,
223}
224
225/// A License Object that set both `identifier` and `url`.
226#[derive(Debug)]
227struct LicenseConflict;
228
229impl std::fmt::Display for LicenseConflict {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        f.write_str("`identifier` and `url` are mutually exclusive on a License Object")
232    }
233}
234
235impl TryFrom<RawLicense> for License {
236    type Error = LicenseConflict;
237
238    fn try_from(raw: RawLicense) -> Result<Self, Self::Error> {
239        let link = match (raw.identifier, raw.url) {
240            (Some(_), Some(_)) => return Err(LicenseConflict),
241            (Some(identifier), None) => Some(LicenseLink::Spdx(identifier)),
242            (None, Some(url)) => Some(LicenseLink::Url(url)),
243            (None, None) => None,
244        };
245
246        Ok(Self {
247            name: raw.name,
248            link,
249            extensions: raw.extensions,
250        })
251    }
252}
253
254impl From<License> for RawLicense {
255    fn from(license: License) -> Self {
256        let (identifier, url) = match license.link {
257            Some(LicenseLink::Spdx(identifier)) => (Some(identifier), None),
258            Some(LicenseLink::Url(url)) => (None, Some(url)),
259            None => (None, None),
260        };
261
262        Self {
263            name: license.name,
264            identifier,
265            url,
266            extensions: license.extensions,
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests;