kynos_openapi/model/document.rs
1//! The root OpenAPI Object.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 Map,
9 model::{
10 components::Components,
11 extensions::Extensions,
12 external_docs::ExternalDocumentation,
13 info::Info,
14 paths::{Paths, item::PathItem},
15 schema::dialect::OAS_DIALECT,
16 security::requirement::SecurityRequirement,
17 server::Server,
18 tag::Tag,
19 },
20};
21
22/// The version of the OpenAPI Specification a document targets.
23/// `#[non_exhaustive]` because OpenAPI 3.2 adds to this and the addition is
24/// `#[cfg]`-gated. Cargo unifies features across a dependency graph, so any
25/// crate enabling `openapi32` enables it for every crate in the build -- and
26/// without this attribute that would turn a downstream exhaustive `match` into
27/// a compile error, which is not what "purely additive" is supposed to mean.
28///
29/// The same reasoning marks [`Method`](crate::Method),
30/// [`ParameterIn`](crate::ParameterIn), [`Style`](crate::Style),
31/// [`ExampleValue`](crate::ExampleValue) and
32/// [`SecurityScheme`](crate::SecurityScheme). Matching one takes a wildcard
33/// arm, in either build:
34///
35/// ```
36/// # use kynos_openapi::SpecVersion;
37/// fn label(version: SpecVersion) -> &'static str {
38/// match version {
39/// SpecVersion::V3_1 => "3.1",
40/// _ => "newer",
41/// }
42/// }
43/// # assert_eq!(label(SpecVersion::V3_1), "3.1");
44/// ```
45///
46/// Without one it does not compile, which is the guarantee: this is the error
47/// a downstream crate would otherwise have met the day something else in its
48/// build turned `openapi32` on.
49///
50/// ```compile_fail
51/// # use kynos_openapi::SpecVersion;
52/// fn label(version: SpecVersion) -> &'static str {
53/// match version {
54/// SpecVersion::V3_1 => "3.1",
55/// }
56/// }
57/// ```
58#[non_exhaustive]
59#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
60pub enum SpecVersion {
61 /// OpenAPI 3.1, the baseline.
62 #[default]
63 V3_1,
64 /// OpenAPI 3.2, a strict superset of 3.1.
65 #[cfg(feature = "openapi32")]
66 V3_2,
67}
68
69impl SpecVersion {
70 /// The version string emitted in the `openapi` field.
71 ///
72 /// Kynos implements the 3.1.2 and 3.2.0 texts. Patch releases of the
73 /// specification are clarifying rather than breaking, so a consumer that
74 /// understands 3.1 understands anything emitted here.
75 #[must_use]
76 pub fn as_str(self) -> &'static str {
77 match self {
78 Self::V3_1 => "3.1.2",
79 #[cfg(feature = "openapi32")]
80 Self::V3_2 => "3.2.0",
81 }
82 }
83
84 /// Whether this version is at least 3.2.
85 #[must_use]
86 pub fn supports_3_2(self) -> bool {
87 #[cfg(feature = "openapi32")]
88 {
89 self >= Self::V3_2
90 }
91 #[cfg(not(feature = "openapi32"))]
92 {
93 let _ = self;
94 false
95 }
96 }
97}
98
99impl fmt::Display for SpecVersion {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str(self.as_str())
102 }
103}
104
105/// A complete OpenAPI description.
106#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
107pub struct Document {
108 /// The version of the OpenAPI Specification this document uses.
109 pub openapi: String,
110
111 /// The canonical URI of this document.
112 ///
113 /// Introduced in OpenAPI 3.2. When present it is the base URI that
114 /// references resolve against, which is what makes a `$ref` between two
115 /// separately-served documents interoperable.
116 #[cfg(feature = "openapi32")]
117 #[serde(rename = "$self", default, skip_serializing_if = "Option::is_none")]
118 pub self_uri: Option<String>,
119
120 /// Metadata about the API.
121 pub info: Info,
122
123 /// The default JSON Schema dialect for schemas in this document.
124 ///
125 /// Defaults to [`OAS_DIALECT`] when absent. Note that 3.1 and 3.2 share one
126 /// dialect URI, so this does not vary by specification version.
127 #[serde(
128 rename = "jsonSchemaDialect",
129 default,
130 skip_serializing_if = "Option::is_none"
131 )]
132 pub json_schema_dialect: Option<String>,
133
134 /// The servers providing the API.
135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub servers: Vec<Server>,
137
138 /// The available paths and operations.
139 ///
140 /// Always written, even when empty. Every version Kynos emits requires a
141 /// document to carry at least one of `paths`, `components` or `webhooks`,
142 /// and this is the one of the three that is always true of an API: an
143 /// empty Paths Object says there are no operations to show, which the
144 /// specification's "Security Filtering" section blesses in as many words.
145 /// Skipping it is what let a description of nothing but opaque routes --
146 /// which take no `paths` key by design -- emit as a document declaring
147 /// nothing at all.
148 #[serde(default)]
149 pub paths: Paths,
150
151 /// Webhooks the API delivers, keyed by a name of the API's choosing.
152 ///
153 /// Unlike [`paths`](Document::paths), these are requests the *API* makes,
154 /// initiated outside any single operation.
155 #[serde(default, skip_serializing_if = "Map::is_empty")]
156 pub webhooks: Map<PathItem>,
157
158 /// Reusable objects.
159 #[serde(default, skip_serializing_if = "Components::is_empty")]
160 pub components: Components,
161
162 /// The security requirements applying across the API.
163 ///
164 /// An operation may override this; an operation with an empty override is
165 /// anonymous.
166 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 pub security: Vec<SecurityRequirement>,
168
169 /// Metadata for the tags operations use. Names must be unique.
170 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub tags: Vec<Tag>,
172
173 /// Additional external documentation.
174 #[serde(
175 rename = "externalDocs",
176 default,
177 skip_serializing_if = "Option::is_none"
178 )]
179 pub external_docs: Option<ExternalDocumentation>,
180
181 /// Specification extensions.
182 #[serde(flatten)]
183 pub extensions: Extensions,
184}
185
186impl Document {
187 /// Creates a document targeting `version`.
188 #[must_use]
189 pub fn new(version: SpecVersion, info: Info) -> Self {
190 Self {
191 openapi: version.as_str().to_owned(),
192 info,
193 ..Self::default()
194 }
195 }
196
197 /// The specification version this document declares.
198 ///
199 /// Returns `None` when [`openapi`](Document::openapi) holds a version this
200 /// build does not model — a 3.2 document read by a 3.1-only build, most
201 /// often.
202 #[must_use]
203 pub fn spec_version(&self) -> Option<SpecVersion> {
204 let mut parts = self.openapi.split('.');
205 let major = parts.next()?;
206 let minor = parts.next()?;
207 match (major, minor) {
208 ("3", "1") => Some(SpecVersion::V3_1),
209 #[cfg(feature = "openapi32")]
210 ("3", "2") => Some(SpecVersion::V3_2),
211 _ => None,
212 }
213 }
214
215 /// The dialect schemas in this document default to.
216 #[must_use]
217 pub fn effective_dialect(&self) -> &str {
218 self.json_schema_dialect.as_deref().unwrap_or(OAS_DIALECT)
219 }
220
221 /// Adds a server.
222 #[must_use]
223 pub fn with_server(mut self, server: Server) -> Self {
224 self.servers.push(server);
225 self
226 }
227
228 /// Adds tag metadata.
229 #[must_use]
230 pub fn with_tag(mut self, tag: Tag) -> Self {
231 self.tags.push(tag);
232 self
233 }
234
235 /// Adds a document-wide security requirement.
236 #[must_use]
237 pub fn with_security(mut self, requirement: SecurityRequirement) -> Self {
238 self.security.push(requirement);
239 self
240 }
241}
242
243#[cfg(test)]
244mod tests;