1use std::collections::HashSet;
2
3use anyhow::{Context, anyhow, bail};
4use http::Method;
5use indexmap::IndexMap;
6use okapi::openapi3::{
7 Contact, ExternalDocs, License, OpenApi, SecurityRequirement, SecurityScheme, Server, Tag,
8};
9
10use crate::{OperationGenerator, components::Components};
11
12#[derive(Clone)]
13pub struct BuilderOptions {
14 pub infer_operation_id: bool,
15}
16
17#[allow(clippy::derivable_impls)]
18impl Default for BuilderOptions {
19 fn default() -> Self {
20 Self {
21 infer_operation_id: false,
22 }
23 }
24}
25
26impl BuilderOptions {
27 pub fn infer_operation_id(&self) -> bool {
28 self.infer_operation_id
29 }
30}
31
32#[derive(Clone)]
34pub struct OpenApiBuilder {
35 spec: OpenApi,
36 components: Components,
37 operations: IndexMap<(String, Method), OperationGenerator>,
38 known_operation_ids: HashSet<String>, builder_options: BuilderOptions,
40}
41
42impl Default for OpenApiBuilder {
43 fn default() -> Self {
44 let spec = OpenApi {
45 openapi: OpenApi::default_version(),
46 ..Default::default()
47 };
48 Self {
49 spec,
50 components: Components::new(Default::default()),
51 operations: IndexMap::new(),
52 known_operation_ids: Default::default(),
53 builder_options: Default::default(),
54 }
55 }
56}
57
58impl OpenApiBuilder {
59 pub fn new(title: &str, version: &str) -> Self {
61 let mut this = Self::default();
62 this.title(title);
63 this.version(version);
64 this
65 }
66
67 pub fn set_components(&mut self, new_components: Components) -> &mut Self {
74 self.components = new_components;
75 self
76 }
77
78 pub fn try_operation<T>(
82 &mut self,
83 path: T,
84 method: Method,
85 generator: OperationGenerator,
86 ) -> Result<&mut Self, anyhow::Error>
87 where
88 T: Into<String>,
89 {
90 let path = path.into();
91 if self
92 .operations
93 .insert((path.clone(), method.clone()), generator)
94 .is_some()
95 {
96 bail!("{method} {path} is already present in specification");
97 };
98 Ok(self)
99 }
100
101 pub fn try_operations<I, S>(&mut self, operations: I) -> Result<&mut Self, anyhow::Error>
105 where
106 I: Iterator<Item = (S, Method, OperationGenerator)>,
107 S: Into<String>,
108 {
109 for (path, method, f) in operations {
110 self.try_operation(path, method, f)?;
111 }
112 Ok(self)
113 }
114
115 pub fn operation<T>(
119 &mut self,
120 path: T,
121 method: Method,
122 generator: OperationGenerator,
123 ) -> &mut Self
124 where
125 T: Into<String>,
126 {
127 let _ = self.try_operation(path, method, generator);
128 self
129 }
130
131 pub fn operations<I, S>(&mut self, operations: I) -> &mut Self
135 where
136 I: Iterator<Item = (S, Method, OperationGenerator)>,
137 S: Into<String>,
138 {
139 for (path, method, f) in operations {
140 self.operation(path, method, f);
141 }
142 self
143 }
144
145 pub fn spec_mut(&mut self) -> &mut OpenApi {
154 &mut self.spec
155 }
156
157 pub fn apply_global_security<N, S>(&mut self, name: N, scopes: S) -> &mut Self
159 where
160 N: Into<String>,
161 S: IntoIterator<Item = String>,
162 {
163 let mut sec = SecurityRequirement::new();
164 sec.insert(name.into(), scopes.into_iter().collect());
165 self.spec.security.push(sec);
166 self
167 }
168
169 pub fn set_infer_operation_id(&mut self, value: bool) -> &mut Self {
173 self.builder_options.infer_operation_id = value;
174 self
175 }
176
177 pub fn add_operation(
179 &mut self,
180 path: &str,
181 method: Method,
182 generator: OperationGenerator,
183 ) -> Result<&mut Self, anyhow::Error> {
184 let operation_schema = generator(&mut self.components, &self.builder_options)?;
185
186 if let Some(operation_id) = operation_schema.operation_id.as_ref() {
188 if self.known_operation_ids.contains(operation_id) {
189 return Err(anyhow!("Found duplicate operation_id {operation_id}."));
190 }
191 self.known_operation_ids.insert(operation_id.clone());
192 }
193
194 let path = self.spec.paths.entry(path.into()).or_default();
195 if method == Method::DELETE {
196 path.delete = Some(operation_schema);
197 } else if method == Method::GET {
198 path.get = Some(operation_schema);
199 } else if method == Method::HEAD {
200 path.head = Some(operation_schema);
201 } else if method == Method::OPTIONS {
202 path.options = Some(operation_schema);
203 } else if method == Method::PATCH {
204 path.patch = Some(operation_schema);
205 } else if method == Method::POST {
206 path.post = Some(operation_schema);
207 } else if method == Method::PUT {
208 path.put = Some(operation_schema);
209 } else if method == Method::TRACE {
210 path.trace = Some(operation_schema);
211 } else {
212 return Err(anyhow::anyhow!("Unsupported method {}", method));
213 }
214 Ok(self)
215 }
216
217 pub fn add_operations(
219 &mut self,
220 operations: impl Iterator<Item = (String, Method, OperationGenerator)>,
221 ) -> Result<&mut Self, anyhow::Error> {
222 for (path, method, f) in operations {
223 self.add_operation(&path, method, f)?;
224 }
225 Ok(self)
226 }
227
228 pub fn build(&mut self) -> Result<OpenApi, anyhow::Error> {
232 let mut spec = self.spec.clone();
233
234 self.operations.sort_by(|lkey, _, rkey, _| {
235 let lkey_str = (&lkey.0, lkey.1.as_str());
236 let rkey_str = (&rkey.0, rkey.1.as_str());
237 lkey_str.cmp(&rkey_str)
238 });
239
240 for ((path, method), generator) in &self.operations {
241 try_add_path(
242 &mut spec,
243 &mut self.components,
244 &self.builder_options,
245 path,
246 method.clone(),
247 *generator,
248 )
249 .with_context(|| format!("Failed to add {method} {path}"))?;
250 }
251
252 spec.components = Some(self.components.okapi_components()?);
253
254 Ok(spec)
255 }
256
257 pub fn title(&mut self, title: impl Into<String>) -> &mut Self {
263 self.spec.info.title = title.into();
264 self
265 }
266
267 pub fn version(&mut self, version: impl Into<String>) -> &mut Self {
271 self.spec.info.version = version.into();
272 self
273 }
274
275 pub fn description(&mut self, description: impl Into<String>) -> &mut Self {
277 self.spec.info.description = Some(description.into());
278 self
279 }
280
281 pub fn contact(&mut self, contact: Contact) -> &mut Self {
283 self.spec.info.contact = Some(contact);
284 self
285 }
286
287 pub fn license(&mut self, license: License) -> &mut Self {
289 self.spec.info.license = Some(license);
290 self
291 }
292
293 pub fn terms_of_service(&mut self, terms_of_service: impl Into<String>) -> &mut Self {
295 self.spec.info.terms_of_service = Some(terms_of_service.into());
296 self
297 }
298
299 pub fn server(&mut self, server: Server) -> &mut Self {
301 self.spec.servers.push(server);
302 self
303 }
304
305 pub fn tag(&mut self, tag: Tag) -> &mut Self {
307 self.spec.tags.push(tag);
308 self
309 }
310
311 pub fn external_docs(&mut self, docs: ExternalDocs) -> &mut Self {
313 let _ = self.spec.external_docs.insert(docs);
314 self
315 }
316
317 pub fn security_scheme<N>(&mut self, name: N, sec: SecurityScheme) -> &mut Self
319 where
320 N: Into<String>,
321 {
322 self.components.add_security_scheme(name, sec);
323 self
324 }
325}
326
327fn try_add_path(
328 spec: &mut OpenApi,
329 components: &mut Components,
330 builder_options: &BuilderOptions,
331 path: &str,
332 method: Method,
333 generator: OperationGenerator,
334) -> Result<(), anyhow::Error> {
335 let operation_schema = generator(components, builder_options)?;
336 let path_str = path;
337 let path = spec.paths.entry(path.into()).or_default();
338 if method == Method::DELETE {
339 path.delete = Some(operation_schema);
340 } else if method == Method::GET {
341 path.get = Some(operation_schema);
342 } else if method == Method::HEAD {
343 path.head = Some(operation_schema);
344 } else if method == Method::OPTIONS {
345 path.options = Some(operation_schema);
346 } else if method == Method::PATCH {
347 path.patch = Some(operation_schema);
348 } else if method == Method::POST {
349 path.post = Some(operation_schema);
350 } else if method == Method::PUT {
351 path.put = Some(operation_schema);
352 } else if method == Method::TRACE {
353 path.trace = Some(operation_schema);
354 } else {
355 return Err(anyhow::anyhow!(
356 "Unsupported method {method} (at {path_str})"
357 ));
358 }
359 Ok(())
360}
361
362#[test]
365fn ensure_builder_deterministic() {
366 use okapi::openapi3::Operation;
367
368 let mut built_specs = Vec::new();
369
370 for _ in 0..100 {
372 let mut builder = OpenApiBuilder::new("title", "version");
373 for i in 0..2 {
374 builder.operation(format!("/path/{}", i), Method::GET, |_, _| {
375 Ok(Operation::default())
376 });
377 }
378
379 let spec = builder
380 .build()
381 .map(|x| format!("{:?}", x))
382 .expect("Failed to build spec");
383 built_specs.push(spec);
384 }
385
386 for i in 1..built_specs.len() {
388 assert_eq!(built_specs[i - 1], built_specs[i]);
389 }
390}