Skip to main content

siderite_openapi/
model.rs

1//! Typed OpenAPI 3.1 structures (the subset siderite generates).
2
3use serde::Serialize;
4use serde_json::Value;
5use siderite_validation::SchemaObject;
6use std::collections::BTreeMap;
7
8/// OpenAPI version emitted by this crate.
9pub const OPENAPI_VERSION: &str = "3.1.0";
10
11/// Root document.
12#[derive(Debug, Clone, PartialEq, Serialize)]
13pub struct OpenApi {
14    /// Always [`OPENAPI_VERSION`].
15    pub openapi: &'static str,
16    /// API metadata.
17    pub info: Info,
18    /// Operations grouped by path template.
19    pub paths: BTreeMap<String, PathItem>,
20    /// Reusable components.
21    #[serde(skip_serializing_if = "Components::is_empty")]
22    pub components: Components,
23}
24
25impl OpenApi {
26    /// Serialize to a JSON value.
27    pub fn to_value(&self) -> Value {
28        // Serialization of these plain structs cannot fail.
29        serde_json::to_value(self).unwrap_or(Value::Null)
30    }
31}
32
33/// `info` object.
34#[derive(Debug, Clone, PartialEq, Serialize)]
35pub struct Info {
36    /// API title.
37    pub title: String,
38    /// API version (not the OpenAPI version).
39    pub version: String,
40    /// Longer description (CommonMark).
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub description: Option<String>,
43}
44
45/// `components` object.
46#[derive(Debug, Clone, Default, PartialEq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct Components {
49    /// Named schemas.
50    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
51    pub schemas: BTreeMap<String, SchemaObject>,
52    /// Security schemes registered by security extractors.
53    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
54    pub security_schemes: BTreeMap<String, Value>,
55}
56
57impl Components {
58    /// Whether nothing would be emitted.
59    pub fn is_empty(&self) -> bool {
60        self.schemas.is_empty() && self.security_schemes.is_empty()
61    }
62}
63
64/// HTTP methods representable in a path item.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
66#[allow(missing_docs)]
67pub enum HttpMethod {
68    Get,
69    Put,
70    Post,
71    Delete,
72    Options,
73    Head,
74    Patch,
75    Trace,
76}
77
78impl HttpMethod {
79    /// Lower-case name used as the path item key.
80    pub const fn as_str(self) -> &'static str {
81        match self {
82            Self::Get => "get",
83            Self::Put => "put",
84            Self::Post => "post",
85            Self::Delete => "delete",
86            Self::Options => "options",
87            Self::Head => "head",
88            Self::Patch => "patch",
89            Self::Trace => "trace",
90        }
91    }
92}
93
94/// Operations available on one path.
95#[derive(Debug, Clone, Default, PartialEq, Serialize)]
96pub struct PathItem(pub BTreeMap<&'static str, Operation>);
97
98/// A single API operation.
99#[derive(Debug, Clone, Default, PartialEq, Serialize)]
100#[serde(rename_all = "camelCase")]
101pub struct Operation {
102    /// Grouping tags.
103    #[serde(skip_serializing_if = "Vec::is_empty")]
104    pub tags: Vec<String>,
105    /// One-line summary.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub summary: Option<String>,
108    /// Longer description.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub description: Option<String>,
111    /// Unique operation id.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub operation_id: Option<String>,
114    /// Parameters (path, query, header, cookie).
115    #[serde(skip_serializing_if = "Vec::is_empty")]
116    pub parameters: Vec<Parameter>,
117    /// Request body.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub request_body: Option<RequestBody>,
120    /// Responses keyed by status code (`"200"`) or `"default"`.
121    pub responses: BTreeMap<String, Response>,
122    /// Deprecated flag.
123    #[serde(skip_serializing_if = "std::ops::Not::not")]
124    pub deprecated: bool,
125    /// Security requirements.
126    #[serde(skip_serializing_if = "Vec::is_empty")]
127    pub security: Vec<BTreeMap<String, Vec<String>>>,
128}
129
130impl Operation {
131    /// Add a parameter unless one with the same name and location exists.
132    ///
133    /// Path parameters may be pushed with an empty `name`; the builder names
134    /// them positionally from the path template.
135    pub fn add_parameter(&mut self, parameter: Parameter) {
136        let duplicate = !parameter.name.is_empty()
137            && self
138                .parameters
139                .iter()
140                .any(|p| p.name == parameter.name && p.location == parameter.location);
141        if !duplicate {
142            self.parameters.push(parameter);
143        }
144    }
145
146    /// Set the request body to `schema` with the given media type.
147    pub fn set_request_body(&mut self, media_type: &str, schema: SchemaObject, required: bool) {
148        self.request_body = Some(RequestBody {
149            description: None,
150            content: BTreeMap::from([(
151                media_type.to_owned(),
152                MediaType {
153                    schema: Some(schema),
154                },
155            )]),
156            required,
157        });
158    }
159
160    /// Add (or replace) a response.
161    pub fn add_response(
162        &mut self,
163        status: impl Into<String>,
164        description: impl Into<String>,
165        content: Option<(&str, SchemaObject)>,
166    ) {
167        let content = content
168            .map(|(mime, schema)| {
169                BTreeMap::from([(
170                    mime.to_owned(),
171                    MediaType {
172                        schema: Some(schema),
173                    },
174                )])
175            })
176            .unwrap_or_default();
177        self.responses.insert(
178            status.into(),
179            Response {
180                description: description.into(),
181                content,
182            },
183        );
184    }
185
186    /// Move the response documented under `from` to `to` (status override).
187    pub fn remap_response(&mut self, from: &str, to: &str) {
188        if let Some(r) = self.responses.remove(from) {
189            self.responses.insert(to.to_owned(), r);
190        }
191    }
192}
193
194/// Parameter location.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
196#[serde(rename_all = "lowercase")]
197#[allow(missing_docs)]
198pub enum ParameterLocation {
199    Path,
200    Query,
201    Header,
202    Cookie,
203}
204
205/// Operation parameter.
206#[derive(Debug, Clone, PartialEq, Serialize)]
207pub struct Parameter {
208    /// Parameter name.
209    pub name: String,
210    /// Where the parameter is read from.
211    #[serde(rename = "in")]
212    pub location: ParameterLocation,
213    /// Required flag (always `true` for path parameters).
214    pub required: bool,
215    /// Parameter schema.
216    pub schema: SchemaObject,
217    /// Description.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub description: Option<String>,
220}
221
222impl Parameter {
223    /// New parameter.
224    pub fn new(
225        name: impl Into<String>,
226        location: ParameterLocation,
227        required: bool,
228        schema: SchemaObject,
229    ) -> Self {
230        Self {
231            name: name.into(),
232            location,
233            required,
234            schema,
235            description: None,
236        }
237    }
238}
239
240/// Request body.
241#[derive(Debug, Clone, PartialEq, Serialize)]
242pub struct RequestBody {
243    /// Description.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub description: Option<String>,
246    /// Content by media type.
247    pub content: BTreeMap<String, MediaType>,
248    /// Required flag.
249    pub required: bool,
250}
251
252/// Media type entry.
253#[derive(Debug, Clone, PartialEq, Serialize)]
254pub struct MediaType {
255    /// Payload schema.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub schema: Option<SchemaObject>,
258}
259
260/// Response.
261#[derive(Debug, Clone, PartialEq, Serialize)]
262pub struct Response {
263    /// Required description.
264    pub description: String,
265    /// Content by media type.
266    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
267    pub content: BTreeMap<String, MediaType>,
268}