1use anyhow::{Context, bail};
2use http::Method;
3use indexmap::IndexMap;
4use okapi::openapi3::{
5 Contact, ExternalDocs, License, OpenApi, SecurityRequirement, SecurityScheme, Server, Tag,
6};
7
8use crate::{OperationGenerator, components::Components};
9
10#[derive(Clone)]
12pub struct OpenApiBuilder {
13 spec: OpenApi,
14 components: Components,
15 operations: IndexMap<(String, Method), OperationGenerator>,
16}
17
18impl Default for OpenApiBuilder {
19 fn default() -> Self {
20 let spec = OpenApi {
21 openapi: OpenApi::default_version(),
22 ..Default::default()
23 };
24 Self {
25 spec,
26 components: Components::new(Default::default()),
27 operations: IndexMap::new(),
28 }
29 }
30}
31
32impl OpenApiBuilder {
33 pub fn new(title: &str, version: &str) -> Self {
35 let mut this = Self::default();
36 this.title(title);
37 this.version(version);
38 this
39 }
40
41 pub fn set_components(&mut self, new_components: Components) -> &mut Self {
48 self.components = new_components;
49 self
50 }
51
52 pub fn try_operation<T>(
56 &mut self,
57 path: T,
58 method: Method,
59 generator: OperationGenerator,
60 ) -> Result<&mut Self, anyhow::Error>
61 where
62 T: Into<String>,
63 {
64 let path = path.into();
65 if self
66 .operations
67 .insert((path.clone(), method.clone()), generator)
68 .is_some()
69 {
70 bail!("{method} {path} is already present in specification");
71 };
72 Ok(self)
73 }
74
75 pub fn try_operations<I, S>(&mut self, operations: I) -> Result<&mut Self, anyhow::Error>
79 where
80 I: Iterator<Item = (S, Method, OperationGenerator)>,
81 S: Into<String>,
82 {
83 for (path, method, f) in operations {
84 self.try_operation(path, method, f)?;
85 }
86 Ok(self)
87 }
88
89 pub fn operation<T>(
93 &mut self,
94 path: T,
95 method: Method,
96 generator: OperationGenerator,
97 ) -> &mut Self
98 where
99 T: Into<String>,
100 {
101 let _ = self.try_operation(path, method, generator);
102 self
103 }
104
105 pub fn operations<I, S>(&mut self, operations: I) -> &mut Self
109 where
110 I: Iterator<Item = (S, Method, OperationGenerator)>,
111 S: Into<String>,
112 {
113 for (path, method, f) in operations {
114 self.operation(path, method, f);
115 }
116 self
117 }
118
119 pub fn spec_mut(&mut self) -> &mut OpenApi {
128 &mut self.spec
129 }
130
131 pub fn apply_global_security<N, S>(&mut self, name: N, scopes: S) -> &mut Self
133 where
134 N: Into<String>,
135 S: IntoIterator<Item = String>,
136 {
137 let mut sec = SecurityRequirement::new();
138 sec.insert(name.into(), scopes.into_iter().collect());
139 self.spec.security.push(sec);
140 self
141 }
142
143 pub fn build(&mut self) -> Result<OpenApi, anyhow::Error> {
147 let mut spec = self.spec.clone();
148
149 self.operations.sort_by(|lkey, _, rkey, _| {
150 let lkey_str = (&lkey.0, lkey.1.as_str());
151 let rkey_str = (&rkey.0, rkey.1.as_str());
152 lkey_str.cmp(&rkey_str)
153 });
154
155 for ((path, method), generator) in &self.operations {
156 try_add_path(
157 &mut spec,
158 &mut self.components,
159 path,
160 method.clone(),
161 *generator,
162 )
163 .with_context(|| format!("Failed to add {method} {path}"))?;
164 }
165
166 spec.components = Some(self.components.okapi_components()?);
167
168 Ok(spec)
169 }
170
171 pub fn title(&mut self, title: impl Into<String>) -> &mut Self {
177 self.spec.info.title = title.into();
178 self
179 }
180
181 pub fn version(&mut self, version: impl Into<String>) -> &mut Self {
185 self.spec.info.version = version.into();
186 self
187 }
188
189 pub fn description(&mut self, description: impl Into<String>) -> &mut Self {
191 self.spec.info.description = Some(description.into());
192 self
193 }
194
195 pub fn contact(&mut self, contact: Contact) -> &mut Self {
197 self.spec.info.contact = Some(contact);
198 self
199 }
200
201 pub fn license(&mut self, license: License) -> &mut Self {
203 self.spec.info.license = Some(license);
204 self
205 }
206
207 pub fn terms_of_service(&mut self, terms_of_service: impl Into<String>) -> &mut Self {
209 self.spec.info.terms_of_service = Some(terms_of_service.into());
210 self
211 }
212
213 pub fn server(&mut self, server: Server) -> &mut Self {
215 self.spec.servers.push(server);
216 self
217 }
218
219 pub fn tag(&mut self, tag: Tag) -> &mut Self {
221 self.spec.tags.push(tag);
222 self
223 }
224
225 pub fn external_docs(&mut self, docs: ExternalDocs) -> &mut Self {
227 let _ = self.spec.external_docs.insert(docs);
228 self
229 }
230
231 pub fn security_scheme<N>(&mut self, name: N, sec: SecurityScheme) -> &mut Self
233 where
234 N: Into<String>,
235 {
236 self.components.add_security_scheme(name, sec);
237 self
238 }
239}
240
241fn try_add_path(
242 spec: &mut OpenApi,
243 components: &mut Components,
244 path: &str,
245 method: Method,
246 generator: OperationGenerator,
247) -> Result<(), anyhow::Error> {
248 let operation_schema = generator(components)?;
249 let path_str = path;
250 let path = spec.paths.entry(path.into()).or_default();
251 if method == Method::DELETE {
252 path.delete = Some(operation_schema);
253 } else if method == Method::GET {
254 path.get = Some(operation_schema);
255 } else if method == Method::HEAD {
256 path.head = Some(operation_schema);
257 } else if method == Method::OPTIONS {
258 path.options = Some(operation_schema);
259 } else if method == Method::PATCH {
260 path.patch = Some(operation_schema);
261 } else if method == Method::POST {
262 path.post = Some(operation_schema);
263 } else if method == Method::PUT {
264 path.put = Some(operation_schema);
265 } else if method == Method::TRACE {
266 path.trace = Some(operation_schema);
267 } else {
268 return Err(anyhow::anyhow!(
269 "Unsupported method {method} (at {path_str})"
270 ));
271 }
272 Ok(())
273}
274
275#[test]
278fn ensure_builder_deterministic() {
279 use okapi::openapi3::Operation;
280
281 let mut built_specs = Vec::new();
282
283 for _ in 0..100 {
285 let mut builder = OpenApiBuilder::new("title", "version");
286 for i in 0..2 {
287 builder.operation(format!("/path/{}", i), Method::GET, |_| {
288 Ok(Operation::default())
289 });
290 }
291
292 let spec = builder
293 .build()
294 .map(|x| format!("{:?}", x))
295 .expect("Failed to build spec");
296 built_specs.push(spec);
297 }
298
299 for i in 1..built_specs.len() {
301 assert_eq!(built_specs[i - 1], built_specs[i]);
302 }
303}