Skip to main content

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;