Skip to main content

okapi_operation/
builder.rs

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/// OpenAPI specificatrion builder.
33#[derive(Clone)]
34pub struct OpenApiBuilder {
35    spec: OpenApi,
36    components: Components,
37    operations: IndexMap<(String, Method), OperationGenerator>,
38    known_operation_ids: HashSet<String>, // Used to validate operation ids
39    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    /// Create new builder with specified title and version
60    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    /// Alter default [`Components`].
68    ///
69    /// ## NOTE
70    ///
71    /// This will override existing components in builder. Use this before adding anything to
72    /// the builder.
73    pub fn set_components(&mut self, new_components: Components) -> &mut Self {
74        self.components = new_components;
75        self
76    }
77
78    /// Add single operation.
79    ///
80    /// Throws an error if (path, method) pair is already present.
81    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    /// Add multiple operations.
102    ///
103    /// Throws an error if any (path, method) pair is already present.
104    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    /// Add single operation.
116    ///
117    /// Replaces operation if (path, method) pair is already present.
118    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    /// Add multiple operations.
132    ///
133    /// Replaces operation if (path, method) pair is already present.
134    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    /// Access inner [`okapi::openapi3::OpenApi`].
146    ///
147    /// **Warning!** This allows raw access to underlying `OpenApi` object,
148    /// which might break generated specification.
149    ///
150    /// # NOTE
151    ///
152    /// Components are overwritten on building specification.
153    pub fn spec_mut(&mut self) -> &mut OpenApi {
154        &mut self.spec
155    }
156
157    /// Apply security scheme globally.
158    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    /// Infer the operation id for every operation based on the function name.
170    ///
171    /// If the operation_id is specified in the macro, it will replace the inferred name.
172    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    /// Add single operation.
178    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        // Check operation id doesn't exists
187        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    /// Add multiple operations.
218    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    /// Generate [`okapi::openapi3::OpenApi`] specification.
229    ///
230    /// This method can be called repeatedly on the same object.
231    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    // Helpers to set OpenApi info/servers/tags/... as is
258
259    /// Set specification title.
260    ///
261    /// Empty string by default.
262    pub fn title(&mut self, title: impl Into<String>) -> &mut Self {
263        self.spec.info.title = title.into();
264        self
265    }
266
267    /// Set specification version.
268    ///
269    /// Empty string by default.
270    pub fn version(&mut self, version: impl Into<String>) -> &mut Self {
271        self.spec.info.version = version.into();
272        self
273    }
274
275    /// Add description to specification.
276    pub fn description(&mut self, description: impl Into<String>) -> &mut Self {
277        self.spec.info.description = Some(description.into());
278        self
279    }
280
281    /// Add contact to specification.
282    pub fn contact(&mut self, contact: Contact) -> &mut Self {
283        self.spec.info.contact = Some(contact);
284        self
285    }
286
287    /// Add license to specification.
288    pub fn license(&mut self, license: License) -> &mut Self {
289        self.spec.info.license = Some(license);
290        self
291    }
292
293    /// Add terms_of_service to specification.
294    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    /// Add server to specification.
300    pub fn server(&mut self, server: Server) -> &mut Self {
301        self.spec.servers.push(server);
302        self
303    }
304
305    /// Add tag to specification.
306    pub fn tag(&mut self, tag: Tag) -> &mut Self {
307        self.spec.tags.push(tag);
308        self
309    }
310
311    /// Set external documentation for specification.
312    pub fn external_docs(&mut self, docs: ExternalDocs) -> &mut Self {
313        let _ = self.spec.external_docs.insert(docs);
314        self
315    }
316
317    /// Add security scheme definition to specification.
318    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/// Ensures that a builder always generates the same file every time, by not relying on
363/// internal data structures that may contain random ordering, e.g. [`std::collections::HashMap`].
364#[test]
365fn ensure_builder_deterministic() {
366    use okapi::openapi3::Operation;
367
368    let mut built_specs = Vec::new();
369
370    // generate 100 specs
371    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    // ensure all specs are the same
387    for i in 1..built_specs.len() {
388        assert_eq!(built_specs[i - 1], built_specs[i]);
389    }
390}