├── .github └── workflows │ ├── rust.yml │ └── typos.yml ├── .gitignore ├── .typos.toml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-Apache ├── LICENSE-MIT ├── README.md ├── crates ├── aide-macros │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── aide │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── cliff.toml │ ├── res │ ├── redoc │ │ └── redoc.standalone.js │ ├── scalar │ │ ├── rust-theme.css │ │ └── scalar.standalone.min.js │ └── swagger │ │ ├── swagger-ui-bundle.js │ │ ├── swagger-ui.css │ │ └── update.sh │ └── src │ ├── axum │ ├── inputs.rs │ ├── mod.rs │ ├── outputs.rs │ └── routing │ │ ├── mod.rs │ │ └── typed.rs │ ├── error.rs │ ├── generate.rs │ ├── helpers │ ├── mod.rs │ ├── no_api.rs │ ├── use_api.rs │ └── with_api.rs │ ├── impls │ ├── bytes.rs │ ├── http.rs │ ├── mod.rs │ └── serde_qs.rs │ ├── lib.rs │ ├── macros.rs │ ├── openapi │ ├── callback.rs │ ├── components.rs │ ├── contact.rs │ ├── discriminator.rs │ ├── encoding.rs │ ├── example.rs │ ├── external_documentation.rs │ ├── header.rs │ ├── info.rs │ ├── license.rs │ ├── link.rs │ ├── media_type.rs │ ├── mod.rs │ ├── openapi.rs │ ├── operation.rs │ ├── parameter.rs │ ├── paths.rs │ ├── reference.rs │ ├── request_body.rs │ ├── responses.rs │ ├── schema.rs │ ├── security_requirement.rs │ ├── security_scheme.rs │ ├── server.rs │ ├── server_variable.rs │ ├── status_code.rs │ ├── tag.rs │ └── variant_or.rs │ ├── operation.rs │ ├── redoc │ └── mod.rs │ ├── scalar │ └── mod.rs │ ├── swagger │ └── mod.rs │ ├── transform.rs │ └── util.rs └── examples ├── example-axum-worker ├── .gitignore ├── Cargo.toml ├── README.md ├── package.json ├── src │ ├── README.md │ ├── docs.rs │ ├── errors.rs │ ├── lib.rs │ ├── state.rs │ └── todos │ │ ├── mod.rs │ │ └── routes.rs └── wrangler.toml └── example-axum ├── Cargo.toml ├── README.md └── src ├── README.md ├── docs.rs ├── errors.rs ├── main.rs ├── state.rs └── todos ├── mod.rs └── routes.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Check formatting 19 | run: cargo fmt --check 20 | - name: Build 21 | run: cargo build --verbose 22 | - name: Run tests 23 | run: cargo test --verbose 24 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | name: Typos 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | typos: 11 | name: Check for typos 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: crate-ci/typos@v1.19.0 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | .local* 4 | .vscode 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | extend-ignore-re = [ 3 | "ff32ba0", 4 | ] 5 | 6 | [files] 7 | extend-exclude = [ 8 | "crates/aide/res/**/*.js", 9 | ] 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | - [Introduction](#introduction) 2 | - [Commit Message Format](#commit-message-format) 3 | - [Your First Contribution](#your-first-contribution) 4 | - [License](#license) 5 | 6 | # Introduction 7 | 8 | Thank you for your interest in contributing to Aide! 9 | 10 | This very short guide contains minimal ground rules regarding 11 | what is expected from contributors. 12 | 13 | # Commit Message Format 14 | 15 | Changelog is based on [conventional commit messages](https://www.conventionalcommits.org/en/v1.0.0/), so it is important for all contributors to follow these guideline. 16 | 17 | Additionally if a commit is scoped to one or multiple crates in the repository, the crate names must appear in the commit message scope. 18 | 19 | The commit that adds some new feature to `aide` should look like this: 20 | 21 | ``` 22 | feat(aide): added new feature 23 | ``` 24 | 25 | # Your First Contribution 26 | 27 | Before you even start working on any code, make sure that your contribution fits the project 28 | and no one is already working on the same thing. 29 | 30 | If you are unsure, feel free to open an [issue](https://github.com/tamasfe/aide/issues) or start a [discussion](https://github.com/tamasfe/aide/discussions)! 31 | 32 | # Updating a version (for collaborators) 33 | 34 | 1. Create a version bump commit 35 | 2. Create a tag `git tag release--` 36 | 3. Generate the changelog with `cargo install git-cliff`: 37 | Make sure you delete the old `# Changelog` title in the changelog file. 38 | ```sh 39 | git cliff --config "crates//cliff.toml" --include-path "crates//**/*" -l --prepend crates//CHANGELOG.md 40 | ``` 41 | 4. push the commits and the tag: `git push` and `git push origin release--` 42 | 43 | # License 44 | 45 | All contributions are licensed under MIT or Apache-2.0 at your option. 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*", "examples/*"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /LICENSE-Apache: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Ferenc Tamás 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2020 Ferenc Tamás 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - [Aide](#aide) 2 | - [Community Projects](#community-projects) 3 | - [Contributing](#contributing) 4 | - [License](#license) 5 | - [Similar Libraries](#similar-libraries) 6 | 7 | # Aide 8 | 9 | [![https://img.shields.io/crates/v/aide](https://img.shields.io/crates/v/aide)](https://crates.io/crates/aide) [![https://img.shields.io/docsrs/aide](https://img.shields.io/docsrs/aide)](https://docs.rs/aide/latest/aide/) 10 | 11 | A code-first API documentation and other utility libraries for Rust. 12 | 13 | Read the [docs](https://docs.rs/aide/latest/aide/). 14 | 15 | ## Community Projects 16 | 17 | If your project isn't listed here and you would like it to be, please feel free to create a PR. 18 | 19 | ### Community maintained aide ecosystem 20 | 21 | - [aide-axum-typed-multipart-2](https://crates.io/crates/aide-axum-typed-multipart-2): Wrapper around [`axum_typed_multipart`](https://docs.rs/axum_typed_multipart/0.11.0/axum_typed_multipart/) 22 | to generate documentation for multipart requests. 23 | 24 | ## Contributing 25 | 26 | All contributions are welcome! Make sure to read [CONTRIBUTING.md](./CONTRIBUTING.md). 27 | 28 | ## License 29 | 30 | All code in this repository is dual licensed under [MIT](./LICENSE-MIT) and [Apache-2.0](./LICENSE-Apache). 31 | 32 | ## Similar Libraries 33 | 34 | If Aide is not exactly what you are looking for, make sure to take a look at the alternatives: 35 | 36 | - [paperclip](https://crates.io/crates/paperclip) 37 | - [utoipa](https://github.com/juhaku/utoipa) 38 | - [okapi](https://github.com/GREsau/okapi) 39 | -------------------------------------------------------------------------------- /crates/aide-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aide-macros" 3 | version = "0.8.0" # remember to update the dependency in aide, even for patch releases 4 | authors = ["tamasfe"] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | description = "Macros for the Aide library" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | darling = "0.20.0" 14 | quote = "1.0.21" 15 | syn = "2.0.15" 16 | proc-macro2 = "1.0" 17 | 18 | [features] 19 | axum-extra-typed-routing = [] 20 | -------------------------------------------------------------------------------- /crates/aide-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use darling::FromDeriveInput; 2 | use proc_macro::TokenStream; 3 | use quote::quote; 4 | use syn::{parse_macro_input, parse_quote, DeriveInput, Type}; 5 | 6 | extern crate proc_macro; 7 | 8 | #[derive(Default, FromDeriveInput)] 9 | #[darling(default, attributes(aide))] 10 | struct OperationIoOpts { 11 | input: bool, 12 | input_with: Option, 13 | output: bool, 14 | output_with: Option, 15 | json_schema: bool, 16 | } 17 | 18 | /// A helper to reduce boilerplate for implementing [`OperationInput`] 19 | /// and [`OperationOutput`] for common use-cases. 20 | /// 21 | /// # Examples 22 | /// 23 | /// The following implements an empty [`OperationInput`] and 24 | /// [`OperationOutput`] so that the type can be used in documented 25 | /// handlers but does not modify the documentation generation in any way. 26 | /// 27 | /// ```ignore 28 | /// use aide::{OperationInput, OperationOutput}; 29 | /// # use aide_macros::{OperationInput, OperationOutput}; 30 | /// 31 | /// #[derive(OperationIo)] 32 | /// struct MyExtractor; 33 | /// ``` 34 | /// 35 | /// By default both [`OperationInput`] and [`OperationOutput`] are implemented. 36 | /// It is possible to restrict either with the `input` and `output` parameters. 37 | /// 38 | /// The following will only implement [`OperationOutput`]: 39 | /// 40 | /// ```ignore 41 | /// #[derive(OperationIo)] 42 | /// #[aide(output)] 43 | /// struct MyExtractor; 44 | /// ``` 45 | /// 46 | /// We can use the implementations of another type, 47 | /// this is useful for wrapping other (e.g. `Json`) extractors 48 | /// that might alter runtime behaviour but the documentation remains the same. 49 | /// 50 | /// Additionally passing the `json_schema` flag will put a 51 | /// [`JsonSchema`] bound to all generic parameters. 52 | /// 53 | /// ```ignore 54 | /// #[derive(OperationIo)] 55 | /// #[aide( 56 | /// input_with = "some_other::Json", 57 | /// output_with = "some_other::Json", 58 | /// json_schema 59 | /// )] 60 | /// struct Json(pub T); 61 | /// ``` 62 | /// 63 | /// [`JsonSchema`]: https://docs.rs/schemars/latest/schemars/trait.JsonSchema.html 64 | /// [`OperationInput`]: https://docs.rs/aide/latest/aide/operation/trait.OperationInput.html 65 | /// [`OperationOutput`]: https://docs.rs/aide/latest/aide/operation/trait.OperationOutput.html 66 | #[proc_macro_derive(OperationIo, attributes(aide))] 67 | pub fn derive_operation_io(ts: TokenStream) -> TokenStream { 68 | let mut derive_input = parse_macro_input!(ts as DeriveInput); 69 | 70 | let OperationIoOpts { 71 | input_with, 72 | output_with, 73 | input, 74 | output, 75 | json_schema, 76 | } = OperationIoOpts::from_derive_input(&derive_input).unwrap(); 77 | 78 | let name = &derive_input.ident; 79 | 80 | let generic_params = derive_input 81 | .generics 82 | .params 83 | .iter() 84 | .filter_map(|p| match p { 85 | syn::GenericParam::Type(t) => Some(t.ident.clone()), 86 | _ => None, 87 | }) 88 | .collect::>(); 89 | 90 | if json_schema { 91 | let wh = derive_input.generics.make_where_clause(); 92 | 93 | for param in generic_params { 94 | wh.predicates 95 | .push(parse_quote!(#param: schemars::JsonSchema)); 96 | } 97 | } 98 | 99 | let (i_gen, t_gen, w_gen) = derive_input.generics.split_for_impl(); 100 | 101 | let mut ts = quote!(); 102 | 103 | if !input && !output && input_with.is_none() && output_with.is_none() { 104 | ts.extend(quote! { 105 | impl #i_gen aide::OperationInput for #name #t_gen #w_gen {} 106 | impl #i_gen aide::OperationOutput for #name #t_gen #w_gen { 107 | type Inner = Self; 108 | } 109 | }); 110 | } else { 111 | if input { 112 | ts.extend(quote! { 113 | impl #i_gen aide::OperationInput for #name #t_gen #w_gen {} 114 | }); 115 | } 116 | if output { 117 | ts.extend(quote! { 118 | impl #i_gen aide::OperationOutput for #name #t_gen #w_gen { 119 | type Inner = Self; 120 | } 121 | }); 122 | } 123 | 124 | if let Some(input) = input_with { 125 | ts.extend(quote! { 126 | impl #i_gen aide::OperationInput for #name #t_gen #w_gen { 127 | fn operation_input( 128 | ctx: &mut aide::generate::GenContext, 129 | operation: &mut aide::openapi::Operation 130 | ) { 131 | <#input as aide::OperationInput>::operation_input( 132 | ctx, 133 | operation 134 | ); 135 | } 136 | } 137 | }); 138 | } 139 | 140 | if let Some(output) = output_with { 141 | ts.extend(quote! { 142 | impl #i_gen aide::OperationOutput for #name #t_gen #w_gen { 143 | type Inner = <#output as aide::OperationOutput>::Inner; 144 | fn operation_response( 145 | ctx: &mut aide::generate::GenContext, 146 | operation: &mut aide::openapi::Operation 147 | ) -> Option { 148 | <#output as aide::OperationOutput>::operation_response( 149 | ctx, 150 | operation 151 | ) 152 | } 153 | fn inferred_responses( 154 | ctx: &mut aide::generate::GenContext, 155 | operation: &mut aide::openapi::Operation 156 | ) -> Vec<(Option, aide::openapi::Response)> { 157 | <#output as aide::OperationOutput>::inferred_responses( 158 | ctx, 159 | operation 160 | ) 161 | } 162 | } 163 | }); 164 | } 165 | } 166 | 167 | ts.into() 168 | } 169 | 170 | #[cfg(feature = "axum-extra-typed-routing")] 171 | /// Example usage: 172 | /// ```ignore 173 | /// #[aide::axum_typed_path] 174 | /// #[typed_path("/foo/bar")] 175 | /// struct FooBar; 176 | /// ``` 177 | #[proc_macro_attribute] // functions tagged with `#[proc_macro_attribute]` must currently reside in the root of the crate 178 | pub fn axum_typed_path(_attr: TokenStream, item: TokenStream) -> TokenStream { 179 | let input = proc_macro2::TokenStream::from(item); 180 | quote! { 181 | #[derive( 182 | ::axum_extra::routing::TypedPath, 183 | ::aide_macros::OperationIo, 184 | ::schemars::JsonSchema, 185 | ::serde::Deserialize, 186 | )] 187 | #[aide(input_with = "aide::axum::routing::typed::TypedPath")] 188 | #input 189 | } 190 | .into() 191 | } 192 | -------------------------------------------------------------------------------- /crates/aide/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aide" 3 | version = "0.15.0" 4 | authors = ["tamasfe"] 5 | edition = "2021" 6 | keywords = ["generate", "api", "openapi", "documentation", "specification"] 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/tamasfe/aide" 9 | description = "A code-first API documentation library" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | indexmap = { version = "2.1", features = ["serde"] } 14 | schemars = { version = "0.9.0", features = ["indexmap2"] } 15 | serde = { version = "1.0.144", features = ["derive"] } 16 | serde_json = "1" 17 | thiserror = "2.0" 18 | tracing = "0.1" 19 | aide-macros = { version = "=0.8.0", path = "../aide-macros", optional = true } 20 | 21 | bytes = { version = "1", optional = true } 22 | http = { version = "1.0.0", optional = true } 23 | serde_qs = { version = "0.14", optional = true } 24 | 25 | axum = { version = "0.8.1", optional = true, default-features = false } 26 | axum-extra = { version = "0.10", optional = true } 27 | tower-layer = { version = "0.3", optional = true } 28 | tower-service = { version = "0.3", optional = true } 29 | cfg-if = "1.0.0" 30 | 31 | [features] 32 | macros = ["dep:aide-macros"] 33 | redoc = [] 34 | swagger = [] 35 | scalar = [] 36 | skip_serializing_defaults = [] 37 | serde_qs = ["dep:serde_qs"] 38 | 39 | axum = ["dep:axum", "bytes", "http", "dep:tower-layer", "dep:tower-service", "serde_qs?/axum"] 40 | axum-form = ["axum", "axum/form"] 41 | axum-json = ["axum", "axum/json"] 42 | axum-matched-path = ["axum", "axum/matched-path"] 43 | axum-multipart = ["axum", "axum/multipart"] 44 | axum-original-uri = ["axum", "axum/original-uri"] 45 | axum-query = ["axum", "axum/query"] 46 | axum-tokio = ["axum", "axum/tokio"] 47 | axum-ws = ["axum", "axum/ws"] 48 | 49 | axum-extra = ["axum", "dep:axum-extra"] 50 | axum-extra-cookie = ["axum-extra", "axum-extra/cookie"] 51 | axum-extra-cookie-private = ["axum-extra", "axum-extra/cookie-private"] 52 | axum-extra-form = ["axum-extra", "axum-extra/form"] 53 | axum-extra-headers = ["axum-extra/typed-header"] 54 | axum-extra-query = ["axum-extra", "axum-extra/query"] 55 | axum-extra-json-deserializer = ["axum-extra", "axum-extra/json-deserializer"] 56 | axum-extra-typed-routing = ["axum-extra", "axum-extra/typed-routing"] 57 | 58 | [dev-dependencies] 59 | tokio = { version = "1.21.0", features = ["macros", "rt-multi-thread"] } 60 | 61 | [package.metadata.docs.rs] 62 | all-features = true 63 | rustdoc-args = ["--cfg", "docsrs"] 64 | -------------------------------------------------------------------------------- /crates/aide/README.md: -------------------------------------------------------------------------------- 1 | # Aide 2 | 3 | A code-first API documentation and utility library. 4 | -------------------------------------------------------------------------------- /crates/aide/cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration file for git-cliff (0.1.0) 2 | 3 | [changelog] 4 | header = """ 5 | # Changelog\n 6 | """ 7 | # template for the changelog body 8 | # https://tera.netlify.app/docs/#introduction 9 | body = """ 10 | {% if version %}\ 11 | ## {{ version | trim_start_matches(pat="release-aide-") }} - {{ timestamp | date(format="%Y-%m-%d") }} 12 | {% else %}\ 13 | ## unreleased 14 | {% endif %}\ 15 | {% if previous %}\ 16 | {% if previous.commit_id %} 17 | [{{ previous.commit_id | truncate(length=7, end="") }}]({{ previous.commit_id }})...\ 18 | [{{ commit_id | truncate(length=7, end="") }}]({{ commit_id }}) 19 | {% endif %}\ 20 | {% endif %}\ 21 | {% for group, commits in commits | group_by(attribute="group") %} 22 | ### {{ group | upper_first }} 23 | {% for commit in commits %} 24 | - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))\ 25 | {% for footer in commit.footers -%} 26 | , {{ footer.token }}{{ footer.separator }}{{ footer.value }}\ 27 | {% endfor %}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # remove the leading and trailing whitespace from the template 32 | trim = true 33 | # changelog footer 34 | footer = """ 35 | 36 | """ 37 | 38 | [git] 39 | # parse the commits based on https://www.conventionalcommits.org 40 | conventional_commits = true 41 | # filter out the commits that are not conventional 42 | filter_unconventional = true 43 | # process each line of a commit as an individual commit 44 | split_commits = false 45 | # regex for preprocessing the commit messages 46 | commit_preprocessors = [ 47 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/tamasfe/aide/issues/${2}))" }, 48 | ] 49 | # regex for parsing and grouping commits 50 | commit_parsers = [ 51 | { message = '^feat\([^)]*(aide|\*)[^)]*\)', group = "Features" }, 52 | { message = '^fix\([^)]*(aide|\*)[^)]*\)', group = "Bug Fixes" }, 53 | { message = '^doc\([^)]*(aide|\*)[^)]*\)', group = "Documentation" }, 54 | { message = '^perf\([^)]*(aide|\*)[^)]*\)', group = "Performance" }, 55 | { message = '^refactor\([^)]*(aide|\*)[^)]*\)', group = "Refactor" }, 56 | { message = '^style\([^)]*(aide|\*)[^)]*\)', group = "Styling" }, 57 | { message = '^test\([^)]*(aide|\*)[^)]*\)', group = "Testing" }, 58 | { message = '^chore.*version\s*bump.*', skip = true }, 59 | { message = '^chore.*:\s*version\s*$', skip = true }, 60 | { message = '^chore.*changelog.*', skip = true }, 61 | { message = '^chore\([^)]*(aide|\*)[^)]*\)', group = "Miscellaneous Tasks" }, 62 | { body = '.*security', group = "Security" }, 63 | ] 64 | # filter out the commits that are not matched by commit parsers 65 | filter_commits = true 66 | # glob pattern for matching git tags 67 | tag_pattern = "release-aide-[0-9]*" 68 | # regex for skipping tags 69 | skip_tags = "v0.1.0-beta.1" 70 | # regex for ignoring tags 71 | ignore_tags = "" 72 | # sort the tags chronologically 73 | date_order = false 74 | # sort the commits inside sections by oldest/newest order 75 | sort_commits = "oldest" 76 | -------------------------------------------------------------------------------- /crates/aide/res/scalar/rust-theme.css: -------------------------------------------------------------------------------- 1 | root { 2 | --theme-font: "Inter", var(--system-fonts); 3 | } 4 | /* basic theme */ 5 | .light-mode { 6 | --theme-color-1: rgb(9, 9, 11); 7 | --theme-color-2: rgb(113, 113, 122); 8 | --theme-color-3: rgba(25, 25, 28, 0.5); 9 | --theme-color-accent: var(--theme-color-1); 10 | 11 | --theme-background-1: #fff; 12 | --theme-background-2: #f4f4f5; 13 | --theme-background-3: #e3e3e6; 14 | --theme-background-accent: #8ab4f81f; 15 | 16 | --theme-border-color: rgb(228, 228, 231); 17 | } 18 | .dark-mode { 19 | --theme-color-1: #fafafa; 20 | --theme-color-2: rgb(161, 161, 170); 21 | --theme-color-3: rgba(255, 255, 255, 0.533); 22 | --theme-color-accent: var(--theme-color-1); 23 | 24 | --theme-background-1: #09090b; 25 | --theme-background-2: #18181b; 26 | --theme-background-3: #2c2c30; 27 | --theme-background-accent: #8ab4f81f; 28 | 29 | --theme-border-color: rgba(255, 255, 255, 0.12); 30 | } 31 | /* Document header */ 32 | .light-mode .t-doc__header { 33 | --header-background-1: var(--theme-background-1); 34 | --header-border-color: var(--theme-border-color); 35 | --header-color-1: var(--theme-color-1); 36 | --header-color-2: var(--theme-color-2); 37 | --header-background-toggle: var(--theme-color-3); 38 | --header-call-to-action-color: var(--theme-color-accent); 39 | } 40 | 41 | .dark-mode .t-doc__header { 42 | --header-background-1: var(--theme-background-1); 43 | --header-border-color: var(--theme-border-color); 44 | --header-color-1: var(--theme-color-1); 45 | --header-color-2: var(--theme-color-2); 46 | --header-background-toggle: var(--theme-color-3); 47 | --header-call-to-action-color: var(--theme-color-accent); 48 | } 49 | /* Document Sidebar */ 50 | .light-mode .t-doc__sidebar { 51 | --sidebar-background-1: var(--theme-background-1); 52 | --sidebar-item-hover-color: currentColor; 53 | --sidebar-item-hover-background: var(--theme-background-2); 54 | --sidebar-item-active-background: #09090b; 55 | --sidebar-border-color: var(--theme-border-color); 56 | --sidebar-color-1: var(--theme-color-1); 57 | --sidebar-color-2: var(--theme-color-2); 58 | --sidebar-color-active: var(--theme-background-1); 59 | --sidebar-search-background: transparent; 60 | --sidebar-search-border-color: var(--theme-border-color); 61 | --sidebar-search--color: var(--theme-color-3); 62 | } 63 | 64 | .dark-mode .sidebar { 65 | --sidebar-background-1: var(--theme-background-1); 66 | --sidebar-item-hover-color: currentColor; 67 | --sidebar-item-hover-background: var(--theme-background-2); 68 | --sidebar-item-active-background: var(--theme-background-3); 69 | --sidebar-border-color: var(--theme-border-color); 70 | --sidebar-color-1: var(--theme-color-1); 71 | --sidebar-color-2: var(--theme-color-2); 72 | --sidebar-color-active: var(--theme-color-accent); 73 | --sidebar-search-background: transparent; 74 | --sidebar-search-border-color: var(--theme-border-color); 75 | --sidebar-search--color: var(--theme-color-3); 76 | } 77 | /* advanced */ 78 | .light-mode { 79 | --theme-button-1: rgb(49 53 56); 80 | --theme-button-1-color: #fff; 81 | --theme-button-1-hover: rgb(28 31 33); 82 | 83 | --theme-color-green: #069061; 84 | --theme-color-red: #ef0006; 85 | --theme-color-yellow: #edbe20; 86 | --theme-color-blue: #0082d0; 87 | --theme-color-orange: #fb892c; 88 | --theme-color-purple: #5203d1; 89 | 90 | --theme-scrollbar-color: rgba(0, 0, 0, 0.18); 91 | --theme-scrollbar-color-active: rgba(0, 0, 0, 0.36); 92 | } 93 | .dark-mode { 94 | --theme-button-1: #f6f6f6; 95 | --theme-button-1-color: #000; 96 | --theme-button-1-hover: #e7e7e7; 97 | 98 | --theme-color-green: #00b648; 99 | --theme-color-red: #dc1b19; 100 | --theme-color-yellow: #ffc90d; 101 | --theme-color-blue: #4eb3ec; 102 | --theme-color-orange: #ff8d4d; 103 | --theme-color-purple: #b191f9; 104 | 105 | --theme-scrollbar-color: rgba(255, 255, 255, 0.24); 106 | --theme-scrollbar-color-active: rgba(255, 255, 255, 0.48); 107 | } 108 | /* Adv customization */ 109 | .introduction-cards .scalar-card:first-of-type { 110 | overflow: visible; 111 | } 112 | .examples .scalar-card-footer { 113 | --theme-background-3: transparent; 114 | padding-top: 0; 115 | } 116 | .show-api-client-button:before { 117 | background: white !important; 118 | } 119 | .show-api-client-button span, 120 | .show-api-client-button svg { 121 | color: black !important; 122 | } 123 | .download-cta, 124 | .references-rendered .markdown a { 125 | text-decoration: underline !important; 126 | } 127 | .introduction-cards .scalar-card:first-of-type:before { 128 | content: ""; 129 | width: 140px; 130 | height: 140px; 131 | position: absolute; 132 | right: -12px; 133 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MS40MTQyMSIgdmlld0JveD0iMCAwIDEyMDAgODAwIj48cGF0aCBkPSJtMC03LTEyLTI1di0zYzAtMjgtMjktNTUtNzktNzV2MTdsLTE1LTZ2LThjLTcxLTI1LTE3Ny00Mi0yOTQtNDItMTAzIDAtMTk3IDEzLTI2NiAzM3YyOC0zNGMtNzEgMjMtMTE1IDU0LTExNSA4OWwzIDE2LTcgMTNzLTYgNyA0IDIzYTIxMzggMjEzOCAwIDAgMCA5NiAxMjBjMS0xLTctMjktMjctNjAtOS0xNy0yMS00MC0yNy01NiAxOSAxMiAxMzAgNjggMzU3IDY4Qy0xNTAgOTAtNDcgMTgtMzYgOGMtNCAxNi0xNyA0OC0yOCA3MS0xOCAzMi0yNCA2MC0yMyA2MWwxNS0yM0MtNTEgODctMTIgMzEtNCAxNyA1IDAgMC03IDAtNyIgc3R5bGU9ImZpbGw6I2E1MmIwMDtmaWxsLXJ1bGU6bm9uemVybyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAwOSA1MDYpIi8+PHBhdGggZD0ibTAgMjA0LTc5LTU4LTMtOCAyNi0zOGMzLTQgNC05IDItMTNzLTYtOC0xMC04bC00NC04LTUtMTAgMTgtNDJjMi01IDItMTAtMS0xNC0yLTQtNy02LTExLTZsLTQ1IDItNy05IDEwLTQ2YzEtNCAwLTktMy0xMi0zLTQtOC01LTEzLTRsLTQzIDExLTktOCAyLTQ2YzAtNS0yLTktNi0xMnMtOC0zLTEzLTFsLTQwIDE5LTEwLTUtNy00NmMtMS01LTQtOS04LTExcy05LTEtMTMgMmwtMzYgMjctMTEtMy0xNi00NGMtMS00LTUtOC05LTktNS0xLTkgMS0xMiA0bC0zMSAzNS0xMS0yLTIzLTM5Yy0zLTQtNy03LTEyLTctNCAwLTggMy0xMSA3bC0yMyAzOS0xMSAyLTMxLTM1YTEzIDEzIDAgMCAwLTIyIDVsLTE1IDQ0LTExIDMtMzYtMjdjLTQtMy05LTQtMTMtMnMtNyA2LTggMTFsLTcgNDYtMTAgNS00MS0xOWMtNC0yLTktMi0xMiAxLTQgMy02IDctNiAxMmwxIDQ2LTggOC00NC0xMWMtNC0xLTkgMC0xMiA0LTMgMy01IDgtMyAxMmwxMCA0Ni03IDktNDUtMmMtNSAwLTkgMi0xMSA2LTMgNC0zIDktMSAxNGwxOCA0Mi01IDEwLTQ0IDhjLTUgMC04IDQtMTAgOHMtMSA5IDEgMTNsMjYgMzh2MmwtNzQgNzhzLTExIDkgNSAzMGMxNSAxOCA4OSA5MSAxMjggMTI5IDE3IDE5IDI4IDMxIDMwIDMwcy04LTM2LTQxLTc4Yy0yNS0zNy01OC05NC01MC0xMDAgMCAwIDgtMTEgMjYtMTkgMSAwLTEtMSAwIDAgMCAwIDM3MCAxNzAgNzEyIDIgNDAtNyA2MyAxNCA2MyAxNCA4IDUtMTMgNjQtMzAgMTAzLTI0IDQ0LTI3IDc4LTI1IDc5IDEgMSAxMC0xMiAyMi0zMyAzMC00MiA4OC0xMjMgOTgtMTQyIDEyLTIzIDAtMjkgMC0yOSIgc3R5bGU9ImZpbGw6I2Y3NGMwMDtmaWxsLXJ1bGU6bm9uemVybyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTA3OSAyOTUpIi8+PHBhdGggZD0iTTAgMjMyczUzLTIgMTIzLTczbDEwLTIyUzI1IDI5IDE5My00NWMwIDAtMTUgMjQtMTAgMTE3IDAgMCA1MC0xOCA2NS0xMDAgMCAwIDc0IDk4LTgwIDE4OSAwIDAtNzMgMTE3LTE3NSAxMDZsNy0zNVoiIHN0eWxlPSJmaWxsOiNmNzRjMDA7ZmlsbC1ydWxlOm5vbnplcm8iIHRyYW5zZm9ybT0idHJhbnNsYXRlKDkxOCAyNDUpIi8+PHBhdGggZD0iTTAtNzhzMzctNDEgNzQgMGMwIDAgMjkgNTQgMCA4MSAwIDAtNDggMzctNzQgMCAwIDAtMzItMzAgMC04MSIgc3R5bGU9ImZpbGwtcnVsZTpub256ZXJvIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NzcgNDg4KSIvPjxwYXRoIGQ9Ik0wIDBjMCAxNi05IDI5LTIxIDI5LTExIDAtMjAtMTMtMjAtMjlzOS0yOSAyMC0yOUMtOS0yOSAwLTE2IDAgMCIgc3R5bGU9ImZpbGw6I2ZmZjtmaWxsLXJ1bGU6bm9uemVybyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzIwIDQyNSkiLz48cGF0aCBkPSJNMC04NHM2My0yOCA4MSAzNWMwIDAgMTggNzMtNTIgNzcgMCAwLTkwLTE3LTI5LTExMiIgc3R5bGU9ImZpbGwtcnVsZTpub256ZXJvIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1MTIgNDgzKSIvPjxwYXRoIGQ9Ik0wIDBjMCAxNi0xMCAyOS0yMSAyOS0xMiAwLTIyLTEzLTIyLTI5czEwLTI5IDIyLTI5Qy0xMC0yOSAwLTE2IDAgMCIgc3R5bGU9ImZpbGw6I2ZmZjtmaWxsLXJ1bGU6bm9uemVybyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNTQ0IDQyNikiLz48cGF0aCBkPSJtMC00MCA4MS03cy0zIDEzLTEzIDIzYzAgMC0yOCAzMC02MC0zIDAgMC01LTUtOC0xMyIgc3R5bGU9ImZpbGwtcnVsZTpub256ZXJvIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1OTMgNTc3KSIvPjxwYXRoIGQ9Ik0wIDE5MWgtMmMtMjYtNi0xMDAtMjUtMTE5LTU3IDAgMCA5OS0xMTItNTYtMTg1bDE4IDEyNXMtNDgtMzItNTgtMTAxYzAgMC0xMTcgODQgNTEgMTY1IDAgMCAxNSA1OCAxNjEgMTAzYTI1IDI1IDAgMCAwIDMwLTI1YzAtMTQtMTEtMjUtMjUtMjUiIHN0eWxlPSJmaWxsOiNmNzRjMDA7ZmlsbC1ydWxlOm5vbnplcm8iIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3MCAyNzEpIi8+PC9zdmc+); 134 | background-size: 100%; 135 | background-repeat: no-repeat; 136 | bottom: 0; 137 | background-position: center 70px; 138 | } -------------------------------------------------------------------------------- /crates/aide/res/swagger/update.sh: -------------------------------------------------------------------------------- 1 | curl https://raw.githubusercontent.com/swagger-api/swagger-ui/refs/heads/master/dist/swagger-ui.css -o swagger-ui.css 2 | curl https://raw.githubusercontent.com/swagger-api/swagger-ui/refs/heads/master/dist/swagger-ui-bundle.js -o swagger-ui-bundle.js -------------------------------------------------------------------------------- /crates/aide/src/axum/outputs.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | openapi::{MediaType, Operation, Response, SchemaObject}, 3 | util::no_content_response, 4 | }; 5 | #[cfg(feature = "axum-form")] 6 | use axum::extract::rejection::FormRejection; 7 | #[cfg(feature = "axum-json")] 8 | use axum::extract::rejection::JsonRejection; 9 | use axum::response::{Html, NoContent, Redirect}; 10 | #[cfg(any(feature = "axum-json", feature = "axum-form"))] 11 | use http::StatusCode; 12 | use indexmap::IndexMap; 13 | use schemars::json_schema; 14 | #[cfg(any(feature = "axum-form", feature = "axum-json"))] 15 | use schemars::JsonSchema; 16 | 17 | use crate::{generate::GenContext, operation::OperationOutput}; 18 | 19 | impl OperationOutput for NoContent { 20 | type Inner = (); 21 | 22 | fn operation_response(_ctx: &mut GenContext, _operation: &mut Operation) -> Option { 23 | Some(no_content_response()) 24 | } 25 | 26 | fn inferred_responses( 27 | _ctx: &mut GenContext, 28 | _operation: &mut Operation, 29 | ) -> Vec<(Option, Response)> { 30 | vec![(Some(204), no_content_response())] 31 | } 32 | } 33 | 34 | #[cfg(feature = "axum-json")] 35 | impl OperationOutput for axum::Json 36 | where 37 | T: JsonSchema, 38 | { 39 | type Inner = T; 40 | 41 | fn operation_response(ctx: &mut GenContext, _operation: &mut Operation) -> Option { 42 | let json_schema = ctx.schema.subschema_for::(); 43 | let resolved_schema = ctx.resolve_schema(&json_schema); 44 | 45 | Some(Response { 46 | description: resolved_schema 47 | .get("description") 48 | .and_then(|d| d.as_str()) 49 | .map(String::from) 50 | .unwrap_or_default(), 51 | content: IndexMap::from_iter([( 52 | "application/json".into(), 53 | MediaType { 54 | schema: Some(SchemaObject { 55 | json_schema, 56 | example: None, 57 | external_docs: None, 58 | }), 59 | ..Default::default() 60 | }, 61 | )]), 62 | ..Default::default() 63 | }) 64 | } 65 | 66 | fn inferred_responses( 67 | ctx: &mut crate::generate::GenContext, 68 | operation: &mut Operation, 69 | ) -> Vec<(Option, Response)> { 70 | if let Some(res) = Self::operation_response(ctx, operation) { 71 | let success_response = [(Some(200), res)]; 72 | 73 | if ctx.all_error_responses { 74 | [ 75 | &success_response, 76 | JsonRejection::inferred_responses(ctx, operation).as_slice(), 77 | ] 78 | .concat() 79 | } else { 80 | Vec::from(success_response) 81 | } 82 | } else { 83 | Vec::new() 84 | } 85 | } 86 | } 87 | 88 | #[cfg(feature = "axum-form")] 89 | impl OperationOutput for axum::extract::Form 90 | where 91 | T: JsonSchema, 92 | { 93 | type Inner = T; 94 | 95 | fn operation_response(ctx: &mut GenContext, _operation: &mut Operation) -> Option { 96 | let json_schema = ctx.schema.subschema_for::(); 97 | let resolved_schema = ctx.resolve_schema(&json_schema); 98 | 99 | Some(Response { 100 | description: resolved_schema 101 | .get("description") 102 | .and_then(|d| d.as_str()) 103 | .map(String::from) 104 | .unwrap_or_default(), 105 | content: IndexMap::from_iter([( 106 | "application/x-www-form-urlencoded".into(), 107 | MediaType { 108 | schema: Some(SchemaObject { 109 | json_schema: json_schema.into(), 110 | example: None, 111 | external_docs: None, 112 | }), 113 | ..Default::default() 114 | }, 115 | )]), 116 | ..Default::default() 117 | }) 118 | } 119 | 120 | fn inferred_responses( 121 | ctx: &mut crate::generate::GenContext, 122 | operation: &mut Operation, 123 | ) -> Vec<(Option, Response)> { 124 | if let Some(res) = Self::operation_response(ctx, operation) { 125 | let success_response = [(Some(200), res)]; 126 | 127 | if ctx.all_error_responses { 128 | [ 129 | &success_response, 130 | FormRejection::inferred_responses(ctx, operation).as_slice(), 131 | ] 132 | .concat() 133 | } else { 134 | Vec::from(success_response) 135 | } 136 | } else { 137 | Vec::new() 138 | } 139 | } 140 | } 141 | 142 | impl OperationOutput for Html { 143 | type Inner = String; 144 | 145 | fn operation_response(_ctx: &mut GenContext, _operation: &mut Operation) -> Option { 146 | Some(Response { 147 | description: "HTML content".into(), 148 | content: IndexMap::from_iter([( 149 | "text/html".into(), 150 | MediaType { 151 | schema: Some(SchemaObject { 152 | json_schema: json_schema!({ 153 | "type": "string", 154 | }), 155 | example: None, 156 | external_docs: None, 157 | }), 158 | ..Default::default() 159 | }, 160 | )]), 161 | ..Default::default() 162 | }) 163 | } 164 | 165 | fn inferred_responses( 166 | ctx: &mut crate::generate::GenContext, 167 | operation: &mut Operation, 168 | ) -> Vec<(Option, Response)> { 169 | if let Some(res) = Self::operation_response(ctx, operation) { 170 | Vec::from([(Some(200), res)]) 171 | } else { 172 | Vec::new() 173 | } 174 | } 175 | } 176 | 177 | #[cfg(feature = "axum-json")] 178 | impl OperationOutput for JsonRejection { 179 | type Inner = Self; 180 | 181 | fn operation_response(ctx: &mut GenContext, operation: &mut Operation) -> Option { 182 | String::operation_response(ctx, operation) 183 | } 184 | 185 | fn inferred_responses( 186 | ctx: &mut crate::generate::GenContext, 187 | operation: &mut Operation, 188 | ) -> Vec<(Option, Response)> { 189 | if let Some(res) = Self::operation_response(ctx, operation) { 190 | Vec::from([ 191 | // rejection_response(StatusCode::BAD_REQUEST, &res), 192 | rejection_response(StatusCode::PAYLOAD_TOO_LARGE, &res), 193 | rejection_response(StatusCode::UNSUPPORTED_MEDIA_TYPE, &res), 194 | // rejection_response(StatusCode::UNPROCESSABLE_ENTITY, &res), 195 | ]) 196 | } else { 197 | Vec::new() 198 | } 199 | } 200 | } 201 | 202 | #[cfg(feature = "axum-form")] 203 | impl OperationOutput for FormRejection { 204 | type Inner = Self; 205 | 206 | fn operation_response(ctx: &mut GenContext, operation: &mut Operation) -> Option { 207 | String::operation_response(ctx, operation) 208 | } 209 | 210 | fn inferred_responses( 211 | ctx: &mut crate::generate::GenContext, 212 | operation: &mut Operation, 213 | ) -> Vec<(Option, Response)> { 214 | if let Some(res) = Self::operation_response(ctx, operation) { 215 | Vec::from([ 216 | // rejection_response(StatusCode::BAD_REQUEST, &res), 217 | rejection_response(StatusCode::PAYLOAD_TOO_LARGE, &res), 218 | rejection_response(StatusCode::UNSUPPORTED_MEDIA_TYPE, &res), 219 | // rejection_response(StatusCode::UNPROCESSABLE_ENTITY, &res), 220 | ]) 221 | } else { 222 | Vec::new() 223 | } 224 | } 225 | } 226 | 227 | #[cfg(any(feature = "axum-json", feature = "axum-form"))] 228 | fn rejection_response(status_code: StatusCode, response: &Response) -> (Option, Response) { 229 | (Some(status_code.as_u16()), response.clone()) 230 | } 231 | 232 | impl OperationOutput for Redirect { 233 | type Inner = Self; 234 | fn operation_response(_ctx: &mut GenContext, _operation: &mut Operation) -> Option { 235 | Some(Response { 236 | description: "A redirect to the described URL".to_string(), 237 | ..Default::default() 238 | }) 239 | } 240 | } 241 | 242 | #[cfg(feature = "axum-extra")] 243 | #[allow(unused_imports)] 244 | mod extra { 245 | use axum_extra::extract; 246 | 247 | use super::*; 248 | use crate::operation::OperationOutput; 249 | 250 | #[cfg(feature = "axum-extra-cookie")] 251 | impl OperationOutput for extract::CookieJar { 252 | type Inner = (); 253 | } 254 | 255 | #[cfg(feature = "axum-extra-cookie-private")] 256 | impl OperationOutput for extract::PrivateCookieJar { 257 | type Inner = (); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /crates/aide/src/axum/routing/typed.rs: -------------------------------------------------------------------------------- 1 | //! Typed routing for Axum. 2 | 3 | use axum::extract::rejection::PathRejection; 4 | use axum_extra::routing::SecondElementIs; 5 | use http::request::Parts; 6 | use schemars::JsonSchema; 7 | use serde::de::DeserializeOwned; 8 | 9 | use super::*; 10 | use crate::operation::{add_parameters, parameters_from_schema, OperationHandler, ParamLocation}; 11 | 12 | impl crate::axum::ApiRouter 13 | where 14 | S: Clone + Send + Sync + 'static, 15 | { 16 | /// Add a typed `GET` route to the router. 17 | /// 18 | /// The path will be inferred from the first argument to the handler function which must 19 | /// implement [`TypedPath`]. 20 | pub fn typed_get(self, handler: H) -> Self 21 | where 22 | H: axum::handler::Handler + OperationHandler, 23 | T: SecondElementIs

+ 'static, 24 | I: OperationInput, 25 | O: OperationOutput, 26 | P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput, 27 | { 28 | self.api_route(P::PATH, crate::axum::routing::get(handler)) 29 | } 30 | 31 | /// Add a typed `DELETE` route to the router. 32 | /// 33 | /// The path will be inferred from the first argument to the handler function which must 34 | /// implement [`TypedPath`]. 35 | pub fn typed_delete(self, handler: H) -> Self 36 | where 37 | H: axum::handler::Handler + OperationHandler, 38 | T: SecondElementIs

+ 'static, 39 | I: OperationInput, 40 | O: OperationOutput, 41 | P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput, 42 | { 43 | self.api_route(P::PATH, crate::axum::routing::delete(handler)) 44 | } 45 | 46 | /// Add a typed `HEAD` route to the router. 47 | /// 48 | /// The path will be inferred from the first argument to the handler function which must 49 | /// implement [`TypedPath`]. 50 | pub fn typed_head(self, handler: H) -> Self 51 | where 52 | H: axum::handler::Handler + OperationHandler, 53 | T: SecondElementIs

+ 'static, 54 | I: OperationInput, 55 | O: OperationOutput, 56 | P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput, 57 | { 58 | self.api_route(P::PATH, crate::axum::routing::head(handler)) 59 | } 60 | 61 | /// Add a typed `OPTIONS` route to the router. 62 | /// 63 | /// The path will be inferred from the first argument to the handler function which must 64 | /// implement [`TypedPath`]. 65 | pub fn typed_options(self, handler: H) -> Self 66 | where 67 | H: axum::handler::Handler + OperationHandler, 68 | T: SecondElementIs

+ 'static, 69 | I: OperationInput, 70 | O: OperationOutput, 71 | P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput, 72 | { 73 | self.api_route(P::PATH, crate::axum::routing::options(handler)) 74 | } 75 | 76 | /// Add a typed `PATCH` route to the router. 77 | /// 78 | /// The path will be inferred from the first argument to the handler function which must 79 | /// implement [`TypedPath`]. 80 | pub fn typed_patch(self, handler: H) -> Self 81 | where 82 | H: axum::handler::Handler + OperationHandler, 83 | T: SecondElementIs

+ 'static, 84 | I: OperationInput, 85 | O: OperationOutput, 86 | P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput, 87 | { 88 | self.api_route(P::PATH, crate::axum::routing::patch(handler)) 89 | } 90 | 91 | /// Add a typed `POST` route to the router. 92 | /// 93 | /// The path will be inferred from the first argument to the handler function which must 94 | /// implement [`TypedPath`]. 95 | pub fn typed_post(self, handler: H) -> Self 96 | where 97 | H: axum::handler::Handler + OperationHandler, 98 | T: SecondElementIs

+ 'static, 99 | I: OperationInput, 100 | O: OperationOutput, 101 | P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput, 102 | { 103 | self.api_route(P::PATH, crate::axum::routing::post(handler)) 104 | } 105 | 106 | /// Add a typed `PUT` route to the router. 107 | /// 108 | /// The path will be inferred from the first argument to the handler function which must 109 | /// implement [`TypedPath`]. 110 | pub fn typed_put(self, handler: H) -> Self 111 | where 112 | H: axum::handler::Handler + OperationHandler, 113 | T: SecondElementIs

+ 'static, 114 | I: OperationInput, 115 | O: OperationOutput, 116 | P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput, 117 | { 118 | self.api_route(P::PATH, crate::axum::routing::put(handler)) 119 | } 120 | 121 | /// Add a typed `TRACE` route to the router. 122 | /// 123 | /// The path will be inferred from the first argument to the handler function which must 124 | /// implement [`TypedPath`]. 125 | pub fn typed_trace(self, handler: H) -> Self 126 | where 127 | H: axum::handler::Handler + OperationHandler, 128 | T: SecondElementIs

+ 'static, 129 | I: OperationInput, 130 | O: OperationOutput, 131 | P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput, 132 | { 133 | self.api_route(P::PATH, crate::axum::routing::trace(handler)) 134 | } 135 | } 136 | 137 | /// A wrapper around `axum_extra::routing::TypedPath` to implement `OperationInput`. 138 | /// Basically fix for Rust does not support `!Trait` and specialization on stable. 139 | #[derive(Debug)] 140 | pub struct TypedPath(pub T); 141 | 142 | impl OperationInput for TypedPath 143 | where 144 | T: axum_extra::routing::TypedPath + JsonSchema, 145 | { 146 | fn operation_input(ctx: &mut crate::generate::GenContext, operation: &mut Operation) { 147 | // `subschema_for` `description` is none, while `root_schema_for` is some 148 | let schema = ctx.schema.root_schema_for::(); 149 | operation.description = schema 150 | .get("description") 151 | .and_then(|d| d.as_str()) 152 | .map(String::from); 153 | let params = parameters_from_schema(ctx, schema, ParamLocation::Path); 154 | add_parameters(ctx, operation, params); 155 | } 156 | } 157 | 158 | impl axum::extract::FromRequestParts for TypedPath 159 | where 160 | T: DeserializeOwned + Send + axum_extra::routing::TypedPath + JsonSchema, 161 | S: Send + Sync, 162 | { 163 | type Rejection = PathRejection; 164 | 165 | async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 166 | let path = axum::extract::Path::::from_request_parts(parts, state).await?; 167 | Ok(Self(path.0)) 168 | } 169 | } 170 | 171 | impl axum::extract::OptionalFromRequestParts for TypedPath 172 | where 173 | T: DeserializeOwned + Send + 'static + axum_extra::routing::TypedPath + JsonSchema, 174 | S: Send + Sync, 175 | { 176 | type Rejection = PathRejection; 177 | 178 | async fn from_request_parts( 179 | parts: &mut Parts, 180 | state: &S, 181 | ) -> Result, Self::Rejection> { 182 | let path = axum::extract::Path::::from_request_parts(parts, state).await?; 183 | Ok(path.map(|x| Self(x.0))) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /crates/aide/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Crate-wide error types. 2 | 3 | use crate::openapi::StatusCode; 4 | use thiserror::Error; 5 | 6 | /// Errors during documentation generation. 7 | /// 8 | /// ## False Positives 9 | /// 10 | /// In some cases there is not enough contextual 11 | /// information to determine whether an error is indeed 12 | /// an error, in these cases the error is reported anyway 13 | /// just to be sure. 14 | #[allow(missing_docs)] 15 | #[derive(Debug, Error)] 16 | #[non_exhaustive] 17 | pub enum Error { 18 | #[error(r#"parameter "{0}" does not exist for the operation"#)] 19 | ParameterNotExists(String), 20 | #[error("the default response already exists for the operation")] 21 | DefaultResponseExists, 22 | #[error(r#"the response for status "{0}" already exists for the operation"#)] 23 | ResponseExists(StatusCode), 24 | #[error(r#"the operation "{1}" already exists for the path "{0}""#)] 25 | OperationExists(String, &'static str), 26 | #[error(r#"duplicate request body for the operation"#)] 27 | DuplicateRequestBody, 28 | #[error(r#"duplicate parameter "{0}" for the operation"#)] 29 | DuplicateParameter(String), 30 | #[error(r#"transformations do not support references"#)] 31 | UnexpectedReference, 32 | #[error("did not apply inferred response because a response for status {0} already exists")] 33 | InferredResponseConflict(u16), 34 | #[error("did not apply inferred default response because a default response already exists")] 35 | InferredDefaultResponseConflict, 36 | #[error("{0}")] 37 | Other(Box), 38 | } 39 | -------------------------------------------------------------------------------- /crates/aide/src/generate.rs: -------------------------------------------------------------------------------- 1 | //! Thread-local context for common settings for documentation generation. 2 | 3 | use std::cell::RefCell; 4 | 5 | use cfg_if::cfg_if; 6 | use schemars::{generate::SchemaSettings, Schema, SchemaGenerator}; 7 | use serde_json::Value; 8 | 9 | use crate::error::Error; 10 | 11 | thread_local! { 12 | static GEN_CTX: RefCell = RefCell::new(GenContext::new()); 13 | } 14 | 15 | /// Access the current thread-local context for 16 | /// API documentation generation. 17 | pub fn in_context(cb: F) -> R 18 | where 19 | F: FnOnce(&mut GenContext) -> R, 20 | { 21 | GEN_CTX.with(|ctx| cb(&mut ctx.borrow_mut())) 22 | } 23 | 24 | /// Register an error handler in the current thread-local context. 25 | /// 26 | /// Only one handler is allowed at a time, this 27 | /// function will overwrite the existing one. 28 | /// 29 | /// Due to the design of the library in some cases 30 | /// errors can be false positives that cannot be 31 | /// avoided. 32 | /// 33 | /// It is advised **not to panic** in this handler 34 | /// unless you are interested in the backtrace for 35 | /// a specific error. 36 | pub fn on_error(handler: impl Fn(Error) + 'static) { 37 | in_context(|ctx| ctx.error_handler = Some(Box::new(handler))); 38 | } 39 | 40 | /// Collect common schemas in the thread-local context, 41 | /// then store them under `#/components/schemas` the next 42 | /// time generated content is merged into [`OpenApi`]. 43 | /// This feature is enabled by default. 44 | /// 45 | /// This will automatically clear the schemas stored 46 | /// in the context when they are merged into the documentation. 47 | /// 48 | /// [`OpenApi`]: crate::openapi::OpenApi 49 | pub fn extract_schemas(extract: bool) { 50 | in_context(|ctx| { 51 | ctx.set_extract_schemas(extract); 52 | }); 53 | } 54 | 55 | /// Set the inferred status code of empty responses (`()`). 56 | /// 57 | /// Some frameworks might use `204` for empty responses, whereas 58 | /// others will set `200`. 59 | /// 60 | /// The default value depends on the framework feature. 61 | pub fn inferred_empty_response_status(status: u16) { 62 | in_context(|ctx| { 63 | ctx.no_content_status = status; 64 | }); 65 | } 66 | 67 | /// Infer responses based on request handler 68 | /// return types. 69 | /// 70 | /// This is enabled by default. 71 | pub fn infer_responses(infer: bool) { 72 | in_context(|ctx| { 73 | ctx.infer_responses = infer; 74 | }); 75 | } 76 | 77 | /// Output all theoretically possible error responses 78 | /// including framework-specific ones. 79 | /// 80 | /// This is disabled by default. 81 | pub fn all_error_responses(infer: bool) { 82 | in_context(|ctx| { 83 | ctx.all_error_responses = infer; 84 | }); 85 | } 86 | 87 | /// Reset the state of the thread-local context. 88 | /// 89 | /// Currently clears: 90 | /// 91 | /// - extracted schemas if [`extract_schemas`] was enabled 92 | /// - disables inferred responses 93 | /// 94 | /// This function is not required in most cases. 95 | pub fn reset_context() { 96 | in_context(|ctx| *ctx = GenContext::new()); 97 | } 98 | 99 | /// A context for API document generation 100 | /// that provides settings and a [`SchemaGenerator`]. 101 | pub struct GenContext { 102 | /// Schema generator that should be used 103 | /// for generating JSON schemas. 104 | pub schema: SchemaGenerator, 105 | 106 | pub(crate) infer_responses: bool, 107 | 108 | pub(crate) all_error_responses: bool, 109 | 110 | /// Extract schemas. 111 | pub(crate) extract_schemas: bool, 112 | 113 | /// Status code for no content. 114 | pub(crate) no_content_status: u16, 115 | 116 | /// The following filter is used internally 117 | /// to reduce the amount of false positives 118 | /// when possible. 119 | pub(crate) show_error: fn(&Error) -> bool, 120 | error_handler: Option>, 121 | } 122 | 123 | impl GenContext { 124 | fn new() -> Self { 125 | cfg_if! { 126 | if #[cfg(feature = "axum")] { 127 | let no_content_status = 200; 128 | } else { 129 | let no_content_status = 204; 130 | } 131 | } 132 | 133 | let mut this = Self { 134 | schema: SchemaGenerator::new(SchemaSettings::draft07()), 135 | infer_responses: true, 136 | all_error_responses: false, 137 | extract_schemas: true, 138 | show_error: default_error_filter, 139 | error_handler: None, 140 | no_content_status, 141 | }; 142 | this.set_extract_schemas(true); 143 | this 144 | } 145 | 146 | pub(crate) fn reset_error_filter(&mut self) { 147 | self.show_error = default_error_filter; 148 | } 149 | fn set_extract_schemas(&mut self, extract: bool) { 150 | if extract { 151 | self.schema = SchemaGenerator::new(SchemaSettings::draft07().with(|s| { 152 | s.inline_subschemas = false; 153 | s.definitions_path = "#/components/schemas/".into(); 154 | })); 155 | self.extract_schemas = true; 156 | } else { 157 | self.schema = SchemaGenerator::new(SchemaSettings::draft07().with(|s| { 158 | s.inline_subschemas = true; 159 | })); 160 | self.extract_schemas = false; 161 | } 162 | } 163 | 164 | /// Add an error in the current context. 165 | #[tracing::instrument(skip_all)] 166 | pub fn error(&mut self, error: Error) { 167 | if let Some(handler) = &self.error_handler { 168 | if !(self.show_error)(&error) { 169 | return; 170 | } 171 | 172 | handler(error); 173 | } 174 | } 175 | 176 | /// Resolve a schema reference to a schema that 177 | /// was generated by the schema generator. 178 | /// 179 | /// If the given schema is not a schema reference, 180 | /// or the target is not found in this generator, 181 | /// the given schema is returned as-is. 182 | /// 183 | /// This function is required before interacting with generated schemas 184 | /// if [`extract_schemas`] is enabled, in which case most generated 185 | /// schema objects are references. 186 | #[must_use] 187 | pub fn resolve_schema<'s>(&'s self, schema_or_ref: &'s Schema) -> &'s Schema { 188 | match &schema_or_ref.as_object().and_then(|o| o.get("$ref")) { 189 | Some(Value::String(r)) => self 190 | .schema 191 | .definitions() 192 | .get(r.strip_prefix("#/components/schemas/").unwrap_or(r)) 193 | .and_then(|s| Into::>::into(s.try_into()).ok()) 194 | .unwrap_or(schema_or_ref), 195 | _ => schema_or_ref, 196 | } 197 | } 198 | } 199 | 200 | fn default_error_filter(_: &Error) -> bool { 201 | true 202 | } 203 | -------------------------------------------------------------------------------- /crates/aide/src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod no_api; 2 | pub mod use_api; 3 | pub mod with_api; 4 | -------------------------------------------------------------------------------- /crates/aide/src/helpers/no_api.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{OperationInput, OperationOutput}; 6 | 7 | /// Allows non [`OperationInput`] or [`OperationOutput`] types to be used in aide handlers with a default empty documentation. 8 | /// 9 | /// For types that already implement [`OperationInput`] or [`OperationOutput`] it overrides the documentation and hides it. 10 | /// ```ignore 11 | /// pub async fn my_sqlx_tx_endpoint( 12 | /// NoApi(mut tx): NoApi> // allows usage of the TX 13 | /// ) -> NoApi> // Hides the API of the return type 14 | /// # {} 15 | /// ``` 16 | #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] 17 | pub struct NoApi(pub T); 18 | 19 | impl NoApi { 20 | /// Unwraps [Self] into its inner type 21 | pub fn into_inner(self) -> T { 22 | self.0 23 | } 24 | } 25 | 26 | impl Deref for NoApi { 27 | type Target = T; 28 | 29 | fn deref(&self) -> &Self::Target { 30 | &self.0 31 | } 32 | } 33 | 34 | impl DerefMut for NoApi { 35 | fn deref_mut(&mut self) -> &mut Self::Target { 36 | &mut self.0 37 | } 38 | } 39 | 40 | impl AsRef for NoApi { 41 | fn as_ref(&self) -> &T { 42 | &self.0 43 | } 44 | } 45 | 46 | impl AsMut for NoApi { 47 | fn as_mut(&mut self) -> &mut T { 48 | &mut self.0 49 | } 50 | } 51 | 52 | impl From for NoApi { 53 | fn from(value: T) -> Self { 54 | Self(value) 55 | } 56 | } 57 | 58 | impl OperationInput for NoApi {} 59 | 60 | impl OperationOutput for NoApi { 61 | type Inner = (); 62 | } 63 | 64 | #[cfg(feature = "axum")] 65 | mod axum { 66 | use axum::body::Body; 67 | use axum::extract::{FromRequest, FromRequestParts}; 68 | use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; 69 | use http::request::Parts; 70 | use http::Request; 71 | 72 | use crate::NoApi; 73 | 74 | impl IntoResponse for NoApi 75 | where 76 | T: IntoResponse, 77 | { 78 | fn into_response(self) -> Response { 79 | self.0.into_response() 80 | } 81 | } 82 | 83 | impl IntoResponseParts for NoApi 84 | where 85 | T: IntoResponseParts, 86 | { 87 | type Error = T::Error; 88 | 89 | fn into_response_parts(self, res: ResponseParts) -> Result { 90 | self.0.into_response_parts(res) 91 | } 92 | } 93 | 94 | impl FromRequestParts for NoApi 95 | where 96 | T: FromRequestParts, 97 | S: Send + Sync, 98 | { 99 | type Rejection = T::Rejection; 100 | 101 | async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 102 | Ok(Self(T::from_request_parts(parts, state).await?)) 103 | } 104 | } 105 | 106 | impl FromRequest for NoApi 107 | where 108 | T: FromRequest, 109 | S: Send + Sync, 110 | { 111 | type Rejection = ::Rejection; 112 | 113 | async fn from_request(req: Request, state: &S) -> Result { 114 | Ok(Self(::from_request(req, state).await?)) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/aide/src/helpers/use_api.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | marker::PhantomData, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::generate::GenContext; 9 | use crate::openapi::{Operation, Response}; 10 | use crate::{OperationInput, OperationOutput}; 11 | 12 | /// helper trait to allow simplified use of [`UseApi`] in responses 13 | pub trait IntoApi { 14 | /// into [`UseApi`] 15 | fn into_api(self) -> UseApi 16 | where 17 | Self: Sized; 18 | } 19 | 20 | impl IntoApi for T { 21 | fn into_api(self) -> UseApi 22 | where 23 | Self: Sized, 24 | { 25 | self.into() 26 | } 27 | } 28 | 29 | /// Allows non [`OperationInput`] or [`OperationOutput`] types to be used in aide handlers with the api documentation of [A]. 30 | /// 31 | /// For types that already implement [`OperationInput`] or [`OperationOutput`] it overrides the documentation with the provided one. 32 | #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] 33 | pub struct UseApi(pub T, pub PhantomData); 34 | 35 | impl UseApi { 36 | /// Unwraps [Self] into its inner type 37 | pub fn into_inner(self) -> T { 38 | self.0 39 | } 40 | } 41 | 42 | impl Deref for UseApi { 43 | type Target = T; 44 | 45 | fn deref(&self) -> &Self::Target { 46 | &self.0 47 | } 48 | } 49 | 50 | impl DerefMut for UseApi { 51 | fn deref_mut(&mut self) -> &mut Self::Target { 52 | &mut self.0 53 | } 54 | } 55 | 56 | impl AsRef for UseApi { 57 | fn as_ref(&self) -> &T { 58 | &self.0 59 | } 60 | } 61 | 62 | impl AsMut for UseApi { 63 | fn as_mut(&mut self) -> &mut T { 64 | &mut self.0 65 | } 66 | } 67 | 68 | impl From for UseApi { 69 | fn from(value: T) -> Self { 70 | Self(value, Default::default()) 71 | } 72 | } 73 | 74 | impl OperationInput for UseApi 75 | where 76 | A: OperationInput, 77 | { 78 | fn operation_input(ctx: &mut GenContext, operation: &mut Operation) { 79 | A::operation_input(ctx, operation); 80 | } 81 | 82 | fn inferred_early_responses( 83 | ctx: &mut GenContext, 84 | operation: &mut Operation, 85 | ) -> Vec<(Option, Response)> { 86 | A::inferred_early_responses(ctx, operation) 87 | } 88 | } 89 | 90 | impl OperationOutput for UseApi 91 | where 92 | A: OperationOutput, 93 | { 94 | type Inner = A::Inner; 95 | 96 | fn operation_response(ctx: &mut GenContext, operation: &mut Operation) -> Option { 97 | A::operation_response(ctx, operation) 98 | } 99 | 100 | fn inferred_responses( 101 | ctx: &mut GenContext, 102 | operation: &mut Operation, 103 | ) -> Vec<(Option, Response)> { 104 | A::inferred_responses(ctx, operation) 105 | } 106 | } 107 | 108 | #[cfg(feature = "axum")] 109 | mod axum { 110 | use axum::body::Body; 111 | use axum::extract::{FromRequest, FromRequestParts}; 112 | use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; 113 | use http::request::Parts; 114 | use http::Request; 115 | 116 | use crate::UseApi; 117 | 118 | impl IntoResponse for UseApi 119 | where 120 | T: IntoResponse, 121 | { 122 | fn into_response(self) -> Response { 123 | self.0.into_response() 124 | } 125 | } 126 | 127 | impl IntoResponseParts for UseApi 128 | where 129 | T: IntoResponseParts, 130 | { 131 | type Error = T::Error; 132 | 133 | fn into_response_parts(self, res: ResponseParts) -> Result { 134 | self.0.into_response_parts(res) 135 | } 136 | } 137 | 138 | impl FromRequestParts for UseApi 139 | where 140 | T: FromRequestParts, 141 | S: Send + Sync, 142 | { 143 | type Rejection = T::Rejection; 144 | 145 | async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 146 | Ok(Self( 147 | T::from_request_parts(parts, state).await?, 148 | Default::default(), 149 | )) 150 | } 151 | } 152 | 153 | impl FromRequest for UseApi 154 | where 155 | T: FromRequest, 156 | S: Send + Sync, 157 | { 158 | type Rejection = T::Rejection; 159 | 160 | async fn from_request(req: Request, state: &S) -> Result { 161 | Ok(Self(T::from_request(req, state).await?, Default::default())) 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /crates/aide/src/helpers/with_api.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | marker::PhantomData, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::generate::GenContext; 9 | use crate::openapi::{Operation, Response}; 10 | use crate::{OperationInput, OperationOutput}; 11 | 12 | /// Trait that allows implementing a custom Api definition for any type. 13 | /// 14 | /// Two approaches are possible: 15 | /// 16 | /// 1. Simple Type override for concrete types 17 | /// ``` 18 | /// # use aide::{ApiOverride, OperationInput, WithApi}; 19 | /// # #[derive(Eq, PartialEq, Debug)] struct SomeType; 20 | /// 21 | /// #[derive(Debug)] 22 | /// struct MyApiOverride; 23 | /// 24 | /// impl ApiOverride for MyApiOverride { 25 | /// type Target = SomeType; 26 | /// } 27 | /// 28 | /// impl OperationInput for MyApiOverride { 29 | /// // override stuff 30 | /// // can be done with OperationOutput as well 31 | /// } 32 | /// 33 | /// async fn my_handler(WithApi(ty, ..): WithApi) -> bool { 34 | /// assert_eq!(ty, SomeType); 35 | /// true 36 | /// } 37 | /// ``` 38 | /// 39 | /// 2. Generic Type Override 40 | /// ``` 41 | /// # use std::marker::PhantomData; 42 | /// # use aide::{ApiOverride, OperationInput, WithApi}; 43 | /// # #[derive(Eq, PartialEq, Debug)] struct SomeType; 44 | /// # #[derive(Eq, PartialEq, Debug)] struct CustomXML(T); 45 | /// 46 | /// #[derive(Debug)] 47 | /// struct MyCustomXML(PhantomData); 48 | /// 49 | /// impl ApiOverride for MyCustomXML { 50 | /// type Target = CustomXML; 51 | /// } 52 | /// 53 | /// impl OperationInput for MyCustomXML { 54 | /// // override stuff with access to T 55 | /// // can be done with OperationOutput as well 56 | /// } 57 | /// 58 | /// async fn my_handler(WithApi(ty, ..): WithApi>) -> bool { 59 | /// assert_eq!(ty, CustomXML(SomeType)); 60 | /// true 61 | /// } 62 | /// ``` 63 | pub trait ApiOverride { 64 | /// The type that is being overridden 65 | type Target; 66 | } 67 | 68 | /// Allows non [`OperationInput`] or [`OperationOutput`] types to be used in aide handlers with a provided documentation. 69 | /// 70 | /// For types that already implement [`OperationInput`] or [`OperationOutput`] it overrides the documentation with the provided one. 71 | /// See [`ApiOverride`] on how to implement such an override 72 | #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] 73 | pub struct WithApi(pub T::Target, pub PhantomData) 74 | where 75 | T: ApiOverride; 76 | 77 | impl WithApi 78 | where 79 | T: ApiOverride, 80 | { 81 | /// Unwraps [Self] into its inner type 82 | pub fn into_inner(self) -> T::Target { 83 | self.0 84 | } 85 | } 86 | 87 | impl Deref for WithApi 88 | where 89 | T: ApiOverride, 90 | { 91 | type Target = T::Target; 92 | 93 | fn deref(&self) -> &Self::Target { 94 | &self.0 95 | } 96 | } 97 | 98 | impl DerefMut for WithApi 99 | where 100 | T: ApiOverride, 101 | { 102 | fn deref_mut(&mut self) -> &mut Self::Target { 103 | &mut self.0 104 | } 105 | } 106 | 107 | impl AsRef for WithApi 108 | where 109 | T: ApiOverride, 110 | { 111 | fn as_ref(&self) -> &T::Target { 112 | &self.0 113 | } 114 | } 115 | 116 | impl AsMut for WithApi 117 | where 118 | T: ApiOverride, 119 | { 120 | fn as_mut(&mut self) -> &mut T::Target { 121 | &mut self.0 122 | } 123 | } 124 | 125 | impl OperationInput for WithApi 126 | where 127 | T: OperationInput + ApiOverride, 128 | { 129 | fn operation_input(ctx: &mut GenContext, operation: &mut Operation) { 130 | T::operation_input(ctx, operation); 131 | } 132 | 133 | fn inferred_early_responses( 134 | ctx: &mut GenContext, 135 | operation: &mut Operation, 136 | ) -> Vec<(Option, Response)> { 137 | T::inferred_early_responses(ctx, operation) 138 | } 139 | } 140 | 141 | impl OperationOutput for WithApi 142 | where 143 | T: OperationOutput + ApiOverride, 144 | { 145 | type Inner = T::Inner; 146 | 147 | fn operation_response(ctx: &mut GenContext, operation: &mut Operation) -> Option { 148 | T::operation_response(ctx, operation) 149 | } 150 | 151 | fn inferred_responses( 152 | ctx: &mut GenContext, 153 | operation: &mut Operation, 154 | ) -> Vec<(Option, Response)> { 155 | T::inferred_responses(ctx, operation) 156 | } 157 | } 158 | 159 | #[cfg(feature = "axum")] 160 | mod axum { 161 | use crate::helpers::with_api::ApiOverride; 162 | use axum::body::Body; 163 | use axum::extract::{FromRequest, FromRequestParts}; 164 | use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; 165 | use http::request::Parts; 166 | use http::Request; 167 | 168 | use crate::WithApi; 169 | 170 | impl IntoResponse for WithApi 171 | where 172 | T: ApiOverride, 173 | T::Target: IntoResponse, 174 | { 175 | fn into_response(self) -> Response { 176 | self.0.into_response() 177 | } 178 | } 179 | 180 | impl IntoResponseParts for WithApi 181 | where 182 | T: ApiOverride, 183 | T::Target: IntoResponseParts, 184 | { 185 | type Error = ::Error; 186 | 187 | fn into_response_parts(self, res: ResponseParts) -> Result { 188 | self.0.into_response_parts(res) 189 | } 190 | } 191 | 192 | impl FromRequestParts for WithApi 193 | where 194 | T: ApiOverride, 195 | T::Target: FromRequestParts, 196 | S: Send + Sync, 197 | { 198 | type Rejection = >::Rejection; 199 | 200 | async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 201 | Ok(Self( 202 | >::from_request_parts(parts, state).await?, 203 | Default::default(), 204 | )) 205 | } 206 | } 207 | 208 | impl FromRequest for WithApi 209 | where 210 | T: ApiOverride, 211 | T::Target: FromRequest, 212 | S: Send + Sync, 213 | { 214 | type Rejection = >::Rejection; 215 | 216 | async fn from_request(req: Request, state: &S) -> Result { 217 | Ok(Self( 218 | >::from_request(req, state).await?, 219 | Default::default(), 220 | )) 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /crates/aide/src/impls/bytes.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Bytes, BytesMut}; 2 | use indexmap::IndexMap; 3 | 4 | use crate::{ 5 | openapi::{MediaType, Operation, RequestBody, Response}, 6 | operation::set_body, 7 | OperationInput, OperationOutput, 8 | }; 9 | 10 | impl OperationInput for Bytes { 11 | fn operation_input( 12 | ctx: &mut crate::generate::GenContext, 13 | operation: &mut crate::openapi::Operation, 14 | ) { 15 | set_body( 16 | ctx, 17 | operation, 18 | RequestBody { 19 | description: None, 20 | content: IndexMap::from_iter([( 21 | "application/octet-stream".into(), 22 | MediaType::default(), 23 | )]), 24 | required: true, 25 | extensions: IndexMap::default(), 26 | }, 27 | ); 28 | } 29 | } 30 | 31 | impl OperationInput for BytesMut { 32 | fn operation_input( 33 | ctx: &mut crate::generate::GenContext, 34 | operation: &mut crate::openapi::Operation, 35 | ) { 36 | Bytes::operation_input(ctx, operation); 37 | } 38 | } 39 | 40 | impl OperationOutput for Bytes { 41 | type Inner = Self; 42 | 43 | fn operation_response( 44 | _ctx: &mut crate::generate::GenContext, 45 | _operation: &mut Operation, 46 | ) -> Option { 47 | Some(Response { 48 | description: "byte stream".into(), 49 | content: IndexMap::from_iter([( 50 | "application/octet-stream".into(), 51 | MediaType::default(), 52 | )]), 53 | ..Default::default() 54 | }) 55 | } 56 | 57 | fn inferred_responses( 58 | ctx: &mut crate::generate::GenContext, 59 | operation: &mut Operation, 60 | ) -> Vec<(Option, Response)> { 61 | if let Some(res) = Self::operation_response(ctx, operation) { 62 | Vec::from([(Some(200), res)]) 63 | } else { 64 | Vec::new() 65 | } 66 | } 67 | } 68 | 69 | impl OperationOutput for BytesMut { 70 | type Inner = Self; 71 | 72 | fn operation_response( 73 | ctx: &mut crate::generate::GenContext, 74 | operation: &mut Operation, 75 | ) -> Option { 76 | Bytes::operation_response(ctx, operation) 77 | } 78 | 79 | fn inferred_responses( 80 | ctx: &mut crate::generate::GenContext, 81 | operation: &mut Operation, 82 | ) -> Vec<(Option, Response)> { 83 | Bytes::inferred_responses(ctx, operation) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/aide/src/impls/http.rs: -------------------------------------------------------------------------------- 1 | use http::{request::Parts, HeaderMap, Method, Request, Response, StatusCode, Uri, Version}; 2 | 3 | use crate::{OperationInput, OperationOutput}; 4 | 5 | impl OperationInput for Request {} 6 | impl OperationInput for Method {} 7 | impl OperationInput for Uri {} 8 | impl OperationInput for Version {} 9 | impl OperationInput for HeaderMap {} 10 | impl OperationInput for Parts {} 11 | 12 | impl OperationOutput for Response { 13 | type Inner = Self; 14 | } 15 | 16 | impl OperationOutput for StatusCode { 17 | type Inner = Self; 18 | } 19 | -------------------------------------------------------------------------------- /crates/aide/src/impls/serde_qs.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | 3 | use crate::{ 4 | openapi::Operation, 5 | operation::{add_parameters, parameters_from_schema, ParamLocation}, 6 | OperationInput, 7 | }; 8 | 9 | #[cfg(feature = "axum")] 10 | impl OperationInput for serde_qs::axum::QsQuery 11 | where 12 | T: JsonSchema, 13 | { 14 | fn operation_input(ctx: &mut crate::generate::GenContext, operation: &mut Operation) { 15 | let schema = ctx.schema.subschema_for::(); 16 | let params = parameters_from_schema(ctx, schema, ParamLocation::Query); 17 | add_parameters(ctx, operation, params); 18 | } 19 | } 20 | 21 | #[cfg(feature = "axum")] 22 | impl OperationInput for serde_qs::axum::OptionalQsQuery 23 | where 24 | T: JsonSchema, 25 | { 26 | fn operation_input(ctx: &mut crate::generate::GenContext, operation: &mut Operation) { 27 | let schema = ctx.schema.subschema_for::(); 28 | let params = parameters_from_schema(ctx, schema, ParamLocation::Query); 29 | add_parameters(ctx, operation, params); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/aide/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Aide 2 | //! 3 | //! `aide` is a code-first [Open API](https://www.openapis.org/) documentation 4 | //! generator library. It aims for tight integrations with frameworks and 5 | //! following their conventions, while tries to be out 6 | //! of the way when it is not needed. 7 | //! 8 | //! The goal is to minimize the learning curve, mental context switches 9 | //! and make documentation somewhat slightly less of a chore. 10 | //! 11 | //! See the [examples](https://github.com/tamasfe/aide/tree/master/examples) 12 | //! to see how Aide is used with various frameworks. 13 | //! 14 | //! Currently only Open API version `3.1.0` is supported. 15 | //! 16 | //! Previous releases of aide relied heavily on macros, and the 17 | //! [`linkme`](https://docs.rs/linkme/latest/linkme/) crate for automagic global state. 18 | //! While it all worked, macros were hard to reason about, 19 | //! rustfmt did not work with them, code completion was hit-and-miss. 20 | //! 21 | //! With `0.5.0`, aide was rewritten and instead it is based on on good old functions, 22 | //! type inference and declarative APIs based on the builder pattern. 23 | //! 24 | //! Now all documentation can be traced in the source code[^1], 25 | //! no more macro and global magic all over the place.[^2] 26 | //! 27 | //! [^1]: and with [tracing] spans 28 | //! 29 | //! [^2]: A thread-local context is still used for some settings and 30 | //! shared state. 31 | //! 32 | //! ## Type-based Generation 33 | //! 34 | //! The library uses [`schemars`] for schema generation for types. 35 | //! It should be enough to slap [`JsonSchema`](schemars::JsonSchema) 36 | //! alongside [serde]'s `Serialize/Deserialize` for JSON-based APIs. 37 | //! 38 | //! Additionally the [`OperationInput`] and [`OperationOutput`] traits 39 | //! are used for extractor and response types in frameworks to automatically generate 40 | //! expected HTTP parameter and response documentation. 41 | //! 42 | //! For example a `Json` extractor will generate an `application/json` 43 | //! request body with the schema of `T` if it implements 44 | //! [`JsonSchema`](schemars::JsonSchema). 45 | //! 46 | //! ## Declarative Documentation 47 | //! 48 | //! All manual documentation is based on composable [`transform`] 49 | //! functions and builder-pattern-like API. 50 | //! 51 | //! ## Supported Frameworks 52 | //! 53 | //! - [axum](https://docs.rs/axum/latest/axum/): [`aide::axum`](axum). 54 | //! - [actix-web](https://docs.rs/actix-web/latest/actix_web/) is **not 55 | //! supported** since `0.5.0` only due to lack of developer capacity, 56 | //! but it's likely to be supported again in the future. If you use 57 | //! `actix-web` you can still use the macro-based `0.4.*` version of the 58 | //! library for the time being. 59 | //! 60 | //! ## Errors 61 | //! 62 | //! Some errors occur during code generation, these 63 | //! are usually safe to ignore but might indicate bugs. 64 | //! 65 | //! By default no action is taken on errors, in order to handle them 66 | //! it is possible to register an error handler in the thread-local context 67 | //! with [`aide::generate::on_error`](crate::generate::on_error). 68 | //! 69 | //! False positives are chosen over silently swallowing potential 70 | //! errors, these might happen when there is not enough contextual 71 | //! information to determine whether an error is in fact an error. 72 | //! It is important to keep this in mind, without any filters 73 | //! **simply panicking on all errors is not advised**, especially 74 | //! not in production. 75 | //! 76 | //! ## Feature Flags 77 | //! 78 | //! No features are enabled by default. 79 | //! 80 | //! - `macros`: additional helper macros 81 | //! 82 | //! ### Third-party trait implementations 83 | //! 84 | //! - `bytes` 85 | //! - `http` 86 | //! 87 | //! ### axum integration 88 | //! 89 | //! `axum` and its features gates: 90 | //! 91 | //! - `axum` 92 | //! - `axum-form` 93 | //! - `axum-json` 94 | //! - `axum-matched-path` 95 | //! - `axum-multipart` 96 | //! - `axum-original-uri` 97 | //! - `axum-query` 98 | //! - `axum-tokio` (for `ConnectInfo`) 99 | //! - `axum-ws` (WebSockets) 100 | //! 101 | //! `axum-extra` and its features gates: 102 | //! 103 | //! - `axum-extra` 104 | //! - `axum-extra-cookie` 105 | //! - `axum-extra-cookie-private` 106 | //! - `axum-extra-form` 107 | //! - `axum-extra-headers` 108 | //! - `axum-extra-query` 109 | //! - `axum-extra-json-deserializer` 110 | //! 111 | //! ## MSRV 112 | //! 113 | //! The library will always support the latest stable Rust version, 114 | //! it might support older versions but without guarantees. 115 | //! 116 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 117 | #![warn(clippy::pedantic, missing_docs)] 118 | #![allow( 119 | clippy::default_trait_access, 120 | clippy::doc_markdown, 121 | clippy::module_name_repetitions, 122 | clippy::wildcard_imports, 123 | clippy::too_many_lines, 124 | clippy::single_match_else, 125 | clippy::manual_let_else 126 | )] 127 | 128 | #[macro_use] 129 | mod macros; 130 | mod impls; 131 | 132 | pub mod error; 133 | pub mod generate; 134 | pub mod operation; 135 | 136 | pub mod openapi; 137 | pub mod transform; 138 | pub mod util; 139 | 140 | #[cfg(feature = "axum")] 141 | pub mod axum; 142 | 143 | mod helpers; 144 | #[cfg(feature = "redoc")] 145 | pub mod redoc; 146 | 147 | #[cfg(feature = "swagger")] 148 | pub mod swagger; 149 | 150 | #[cfg(feature = "scalar")] 151 | pub mod scalar; 152 | 153 | pub use helpers::{ 154 | no_api::NoApi, use_api::IntoApi, use_api::UseApi, with_api::ApiOverride, with_api::WithApi, 155 | }; 156 | 157 | pub use error::Error; 158 | pub use operation::{OperationInput, OperationOutput}; 159 | 160 | #[cfg(feature = "macros")] 161 | pub use aide_macros::OperationIo; 162 | -------------------------------------------------------------------------------- /crates/aide/src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! all_the_tuples { 2 | ($name:ident) => { 3 | $name!(T1); 4 | $name!(T1, T2); 5 | $name!(T1, T2, T3); 6 | $name!(T1, T2, T3, T4); 7 | $name!(T1, T2, T3, T4, T5); 8 | $name!(T1, T2, T3, T4, T5, T6); 9 | $name!(T1, T2, T3, T4, T5, T6, T7); 10 | $name!(T1, T2, T3, T4, T5, T6, T7, T8); 11 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9); 12 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); 13 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); 14 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); 15 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); 16 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); 17 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15); 18 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/callback.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::*; 2 | use indexmap::IndexMap; 3 | 4 | /// A map of possible out-of band callbacks related to the parent operation. 5 | /// Each value in the map is a Path Item Object that describes a set of 6 | /// requests that may be initiated by the API provider and the expected 7 | /// responses. The key value used to identify the callback object is an 8 | /// expression, evaluated at runtime, that identifies a URL to use for the 9 | /// callback operation. 10 | pub type Callback = IndexMap>; 11 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/components.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::*; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Holds a set of reusable objects for different aspects of the OAS. 6 | /// All objects defined within the components object will have no effect 7 | /// on the API unless they are explicitly referenced from properties 8 | /// outside the components object. 9 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] 10 | #[serde(rename_all = "camelCase")] 11 | #[derive(schemars::JsonSchema)] 12 | pub struct Components { 13 | /// An object to hold reusable Security Scheme Objects. 14 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 15 | pub security_schemes: IndexMap>, 16 | /// An object to hold reusable Response Objects. 17 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 18 | pub responses: IndexMap>, 19 | /// An object to hold reusable Parameter Objects. 20 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 21 | pub parameters: IndexMap>, 22 | /// An object to hold reusable Example Objects. 23 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 24 | pub examples: IndexMap>, 25 | /// An object to hold reusable Request Body Objects. 26 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 27 | pub request_bodies: IndexMap>, 28 | /// An object to hold reusable Header Objects. 29 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 30 | pub headers: IndexMap>, 31 | /// An object to hold reusable Schema Objects. 32 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 33 | pub schemas: IndexMap, 34 | /// An object to hold reusable Link Objects. 35 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 36 | pub links: IndexMap>, 37 | /// An object to hold reusable Callback Objects. 38 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 39 | pub callbacks: IndexMap>, 40 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 41 | /// An object to hold reusable Path Item Objects. 42 | pub path_items: IndexMap>, 43 | /// Inline extensions to this object. 44 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 45 | pub extensions: IndexMap, 46 | } 47 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/contact.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Contact information for the exposed API. 5 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 6 | pub struct Contact { 7 | /// The identifying name of the contact person/organization. 8 | #[serde(skip_serializing_if = "Option::is_none")] 9 | pub name: Option, 10 | /// The URL pointing to the contact information. 11 | /// This MUST be in the format of a URL. 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub url: Option, 14 | /// The email address of the contact person/organization. 15 | /// This MUST be in the format of an email address. 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub email: Option, 18 | /// Inline extensions to this object. 19 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 20 | pub extensions: IndexMap, 21 | } 22 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/discriminator.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// When request bodies or response payloads may be one of a number of different 5 | /// schemas, a discriminator object can be used to aid in serialization, 6 | /// deserialization, and validation. The discriminator is a specific object in a 7 | /// schema which is used to inform the consumer of the specification of an 8 | /// alternative schema based on the value associated with it. 9 | /// 10 | /// When using the discriminator, inline schemas will not be considered. 11 | #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] 12 | #[serde(rename_all = "camelCase")] 13 | #[derive(schemars::JsonSchema)] 14 | pub struct Discriminator { 15 | /// REQUIRED. The name of the property in the payload that 16 | /// will hold the discriminator value. 17 | pub property_name: String, 18 | /// An object to hold mappings between payload values and schema names or 19 | /// references. 20 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 21 | pub mapping: IndexMap, 22 | /// Inline extensions to this object. 23 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 24 | pub extensions: IndexMap, 25 | } 26 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/encoding.rs: -------------------------------------------------------------------------------- 1 | use crate::{openapi::*, util::*}; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// A single encoding definition applied to a single schema property. 6 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] 7 | #[serde(rename_all = "camelCase")] 8 | #[derive(schemars::JsonSchema)] 9 | pub struct Encoding { 10 | /// The Content-Type for encoding a specific property. 11 | /// Default value depends on the property type: 12 | /// for object - application/json; 13 | /// for array – the default is defined based on the inner type. 14 | /// for all other cases the default is `application/octet-stream`. 15 | /// The value can be a specific media type (e.g. application/json), 16 | /// a wildcard media type (e.g. image/*), or a comma-separated list of the 17 | /// two types. 18 | pub content_type: Option, 19 | /// A map allowing additional information to be provided as headers, 20 | /// for example Content-Disposition. Content-Type is described separately 21 | /// and SHALL be ignored in this section. This property SHALL be ignored 22 | /// if the request body media type is not a multipart. 23 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 24 | pub headers: IndexMap>, 25 | /// Describes how a specific property value will be serialized depending 26 | /// on its type. See Parameter Object for details on the style property. 27 | /// The behavior follows the same values as query parameters, including 28 | /// default values. This property SHALL be ignored if the request body 29 | /// media type is not application/x-www-form-urlencoded or 30 | /// multipart/form-data. If a value is explicitly defined, then the value of 31 | /// `contentType` (implicit or explicit) SHALL be ignored. 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | pub style: Option, 34 | /// When this is true, property values of type array or object generate 35 | /// separate parameters for each value of the array, or key-value-pair 36 | /// of the map. For other types of properties this property has no effect. 37 | /// When style is form, the default value is true. 38 | /// For all other styles, the default value is false. This property 39 | /// SHALL be ignored if the request body media type is 40 | /// not application/x-www-form-urlencoded or multipart/form-data. If a value 41 | /// is explicitly defined, then the value of `contentType` (implicit or 42 | /// explicit) SHALL be ignored. 43 | /// 44 | /// In this Library this value defaults to false always despite the 45 | /// specification. 46 | #[serde(default, skip_serializing_if = "is_false")] 47 | pub explode: bool, 48 | /// Determines whether the parameter value SHOULD allow reserved characters, 49 | /// as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without 50 | /// percent-encoding. The default value is false. This property SHALL be 51 | /// ignored if the request body media type is not 52 | /// application/x-www-form-urlencoded or multipart/form-data. If a value is 53 | /// explicitly defined, then the value of `contentType` (implicit or 54 | /// explicit) SHALL be ignored. 55 | #[serde(default, skip_serializing_if = "is_false")] 56 | pub allow_reserved: bool, 57 | /// Inline extensions to this object. 58 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 59 | pub extensions: IndexMap, 60 | } 61 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/example.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 5 | pub struct Example { 6 | /// Short description for the example. 7 | #[serde(skip_serializing_if = "Option::is_none")] 8 | pub summary: Option, 9 | /// Long description for the example. 10 | /// CommonMark syntax MAY be used for rich text representation. 11 | #[serde(skip_serializing_if = "Option::is_none")] 12 | pub description: Option, 13 | /// Embedded literal example. The `value` field and `externalValue` 14 | /// field are mutually exclusive. To represent examples of 15 | /// media types that cannot naturally represented in JSON or YAML, 16 | /// use a string value to contain the example, escaping where necessary. 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub value: Option, 19 | /// A URI that points to the literal example. 20 | /// This provides the capability to reference examples that cannot 21 | /// easily be included in JSON or YAML documents. The `value` field and 22 | /// `externalValue` field are mutually exclusive. See the rules for 23 | /// resolving Relative References. 24 | #[serde(rename = "externalValue")] 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub external_value: Option, 27 | /// Inline extensions to this object. 28 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 29 | pub extensions: IndexMap, 30 | } 31 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/external_documentation.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Allows referencing an external resource for extended documentation. 5 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 6 | pub struct ExternalDocumentation { 7 | /// A description of the target documentation. 8 | /// CommonMark syntax MAY be used for rich text representation. 9 | #[serde(skip_serializing_if = "Option::is_none")] 10 | pub description: Option, 11 | /// REQUIRED. The URL for the target documentation. 12 | /// This MUST be in the format of a URL. 13 | pub url: String, 14 | /// Inline extensions to this object. 15 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 16 | pub extensions: IndexMap, 17 | } 18 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/header.rs: -------------------------------------------------------------------------------- 1 | use crate::{openapi::*, util::*}; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// The Header Object follows the structure of the Parameter Object with the 6 | /// following changes: 7 | /// 8 | /// 1) name MUST NOT be specified, it is given in the corresponding headers map. 9 | /// 2) in MUST NOT be specified, it is implicitly in header. 10 | /// 3) All traits that are affected by the location MUST be applicable to a 11 | /// location of header (for example, style). 12 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 13 | #[serde(rename_all = "camelCase")] 14 | #[derive(schemars::JsonSchema)] 15 | pub struct Header { 16 | /// A brief description of the parameter. This could 17 | /// contain examples of use. CommonMark syntax MAY be 18 | /// used for rich text representation. 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub description: Option, 21 | #[serde(default)] 22 | pub style: HeaderStyle, 23 | /// Determines whether this parameter is mandatory. 24 | /// If the parameter location is "path", this property 25 | /// is REQUIRED and its value MUST be true. Otherwise, 26 | /// the property MAY be included and its default value 27 | /// is false. 28 | #[serde(default, skip_serializing_if = "is_false")] 29 | pub required: bool, 30 | /// Specifies that a parameter is deprecated and SHOULD 31 | /// be transitioned out of usage. 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | pub deprecated: Option, 34 | #[serde(flatten)] 35 | pub format: ParameterSchemaOrContent, 36 | #[serde(skip_serializing_if = "Option::is_none")] 37 | pub example: Option, 38 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 39 | pub examples: IndexMap>, 40 | /// Inline extensions to this object. 41 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 42 | pub extensions: IndexMap, 43 | } 44 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/info.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::*; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// The object provides metadata about the API. 6 | /// The metadata MAY be used by the clients if needed, 7 | /// and MAY be presented in editing or documentation generation tools for 8 | /// convenience. 9 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 10 | pub struct Info { 11 | /// REQUIRED. The title of the application. 12 | pub title: String, 13 | /// A short summary of the API. 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub summary: Option, 16 | /// A description of the API. 17 | /// CommonMark syntax MAY be used for rich text representation. 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub description: Option, 20 | /// A URL to the Terms of Service for the API. 21 | /// This MUST be in the format of a URL. 22 | #[serde(rename = "termsOfService", skip_serializing_if = "Option::is_none")] 23 | pub terms_of_service: Option, 24 | /// The contact information for the exposed API. 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub contact: Option, 27 | /// The license information for the exposed API. 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub license: Option, 30 | /// REQUIRED. The version of the OpenAPI document (which is distinct from 31 | /// the OpenAPI Specification version or the API implementation version). 32 | pub version: String, 33 | /// Inline extensions to this object. 34 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 35 | pub extensions: IndexMap, 36 | } 37 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/license.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// License information for the exposed API. 5 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 6 | pub struct License { 7 | /// REQUIRED. The license name used for the API. 8 | pub name: String, 9 | /// An [SPDX](https://spdx.org/spdx-specification-21-web-version#h.jxpfx0ykyb60) license expression for the API. The `identifier` field is mutually exclusive of the `url` field. 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub identifier: Option, 12 | /// A URL to the license used for the API. This MUST be in the form of a 13 | /// URL. The `url` field is mutually exclusive of the `identifier` field. 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub url: Option, 16 | /// Inline extensions to this object. 17 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 18 | pub extensions: IndexMap, 19 | } 20 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/link.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::*; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// The Link object represents a possible design-time link for a response. 6 | /// The presence of a link does not guarantee the caller's ability to 7 | /// successfully invoke it, rather it provides a known relationship and 8 | /// traversal mechanism between responses and other operations. 9 | /// 10 | /// Unlike dynamic links (i.e. links provided in the response payload), 11 | /// the OAS linking mechanism does not require link information in the runtime 12 | /// response. 13 | /// 14 | /// For computing links, and providing instructions to execute them, 15 | /// a runtime expression is used for accessing values in an operation 16 | /// and using them as parameters while invoking the linked operation. 17 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 18 | #[serde(rename_all = "camelCase")] 19 | #[derive(schemars::JsonSchema)] 20 | pub struct Link { 21 | /// A description of the link. 22 | /// CommonMark syntax MAY be used for rich text representation. 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub description: Option, 25 | /// Either a operationRef or operationId 26 | #[serde(flatten)] 27 | pub operation: LinkOperation, 28 | /// A literal value or {expression} to use as a request body 29 | /// when calling the target operation. 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub request_body: Option, 32 | /// A map representing parameters to pass to an operation 33 | /// as specified with operationId or identified via operationRef. 34 | /// The key is the parameter name to be used, whereas the value 35 | /// can be a constant or an expression to be evaluated and passed 36 | /// to the linked operation. The parameter name can be qualified 37 | /// using the parameter location [{in}.]{name} for operations 38 | /// that use the same parameter name in different locations (e.g. path.id). 39 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 40 | pub parameters: IndexMap, 41 | /// A server object to be used by the target operation. 42 | #[serde(skip_serializing_if = "Option::is_none")] 43 | pub server: Option, 44 | /// Inline extensions to this object. 45 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 46 | pub extensions: IndexMap, 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 50 | #[serde(rename_all = "camelCase")] 51 | #[derive(schemars::JsonSchema)] 52 | pub enum LinkOperation { 53 | /// A relative or absolute reference to an OAS operation. 54 | /// This field is mutually exclusive of the operationId field, 55 | /// and MUST point to an Operation Object. Relative operationRef 56 | /// values MAY be used to locate an existing Operation Object 57 | /// in the OpenAPI definition. See the rules for resolving Relative 58 | /// References. 59 | OperationRef(String), 60 | /// The name of an existing, resolvable OAS operation, 61 | /// as defined with a unique operationId. This field is 62 | /// mutually exclusive of the operationRef field. 63 | OperationId(String), 64 | } 65 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/media_type.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::*; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 6 | pub struct MediaType { 7 | #[serde(skip_serializing_if = "Option::is_none")] 8 | pub schema: Option, 9 | #[serde(skip_serializing_if = "Option::is_none")] 10 | pub example: Option, 11 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 12 | pub examples: IndexMap>, 13 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 14 | pub encoding: IndexMap, 15 | /// Inline extensions to this object. 16 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 17 | pub extensions: IndexMap, 18 | } 19 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Type definitions taken from 2 | //! 3 | //! Expect this module to change in the future, hopefully once 4 | //! the referenced PR is merged into `openapiv3`. 5 | // FIXME: remove the code below when the upstream openapiv3 3.1 is available. 6 | #![allow(clippy::all, clippy::pedantic, missing_docs)] 7 | mod callback; 8 | mod components; 9 | mod contact; 10 | mod discriminator; 11 | mod encoding; 12 | mod example; 13 | mod external_documentation; 14 | mod header; 15 | mod info; 16 | mod license; 17 | mod link; 18 | mod media_type; 19 | mod openapi; 20 | mod operation; 21 | mod parameter; 22 | mod paths; 23 | mod reference; 24 | mod request_body; 25 | mod responses; 26 | mod schema; 27 | mod security_requirement; 28 | mod security_scheme; 29 | mod server; 30 | mod server_variable; 31 | mod status_code; 32 | mod tag; 33 | mod variant_or; 34 | 35 | pub use self::callback::*; 36 | pub use self::components::*; 37 | pub use self::contact::*; 38 | pub use self::discriminator::*; 39 | pub use self::encoding::*; 40 | pub use self::example::*; 41 | pub use self::external_documentation::*; 42 | pub use self::header::*; 43 | pub use self::info::*; 44 | pub use self::license::*; 45 | pub use self::link::*; 46 | pub use self::media_type::*; 47 | pub use self::openapi::*; 48 | pub use self::operation::*; 49 | pub use self::parameter::*; 50 | pub use self::paths::*; 51 | pub use self::reference::*; 52 | pub use self::request_body::*; 53 | pub use self::responses::*; 54 | pub use self::schema::*; 55 | pub use self::security_requirement::*; 56 | pub use self::security_scheme::*; 57 | pub use self::server::*; 58 | pub use self::server_variable::*; 59 | pub use self::status_code::*; 60 | pub use self::tag::*; 61 | pub use self::variant_or::*; 62 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/openapi.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::openapi::*; 4 | use indexmap::IndexMap; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 8 | pub struct OpenApi { 9 | #[serde(with = "serde_version")] 10 | #[schemars(with = "&'static str")] 11 | pub openapi: Cow<'static, str>, 12 | /// REQUIRED. Provides metadata about the API. 13 | /// The metadata MAY be used by tooling as required. 14 | pub info: Info, 15 | /// The default value for the `$schema` keyword within Schema Objects 16 | /// contained within this OAS document. This MUST be in the form of a URI. 17 | #[serde(rename = "jsonSchemaDialect")] 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub json_schema_dialect: Option, 20 | /// An array of Server Objects, which provide connectivity information to a 21 | /// target server. If the servers property is not provided, or is an empty 22 | /// array, the default value would be a Server Object with a url value of /. 23 | #[serde(default)] 24 | #[serde(skip_serializing_if = "Vec::is_empty")] 25 | pub servers: Vec, 26 | /// The available paths and operations for the API. 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub paths: Option, 29 | /// The incoming webhooks that MAY be received as part of this API and that 30 | /// the API consumer MAY choose to implement. Closely related to the 31 | /// `callbacks` feature, this section describes requests initiated other 32 | /// than by an API call, for example by an out of band registration. The key 33 | /// name is a unique string to refer to each webhook, while the (optionally 34 | /// referenced) Path Item Object describes a request that may be initiated 35 | /// by the API provider and the expected responses. 36 | #[serde(default)] 37 | #[serde(skip_serializing_if = "IndexMap::is_empty")] 38 | pub webhooks: IndexMap>, 39 | /// An element to hold various schemas for the document. 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub components: Option, 42 | /// A declaration of which security mechanisms can be used across the API. 43 | /// The list of values includes alternative security requirement objects 44 | /// that can be used. Only one of the security requirement objects need to 45 | /// be satisfied to authorize a request. Individual operations can override 46 | /// this definition. Global security settings may be overridden on a 47 | /// per-path basis. 48 | #[serde(default)] 49 | #[serde(skip_serializing_if = "Vec::is_empty")] 50 | pub security: Vec, 51 | /// A list of tags used by the document with additional metadata. 52 | /// The order of the tags can be used to reflect on their order by the 53 | /// parsing tools. Not all tags that are used by the Operation Object 54 | /// must be declared. The tags that are not declared MAY be organized 55 | /// randomly or based on the tool's logic. Each tag name in the list 56 | /// MUST be unique. 57 | #[serde(default)] 58 | #[serde(skip_serializing_if = "Vec::is_empty")] 59 | pub tags: Vec, 60 | /// Additional external documentation. 61 | #[serde(rename = "externalDocs")] 62 | #[serde(skip_serializing_if = "Option::is_none")] 63 | pub external_docs: Option, 64 | /// Inline extensions to this object. 65 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 66 | pub extensions: IndexMap, 67 | } 68 | 69 | impl OpenApi { 70 | /// Iterates through all [Operation]s in this API. 71 | /// 72 | /// The iterated items are tuples of `(&str, &str, &Operation)` containing 73 | /// the path, method, and the operation. 74 | /// 75 | /// Path items containing `$ref`s are skipped. 76 | pub fn operations(&self) -> impl Iterator { 77 | self.paths.iter().flat_map(|paths| { 78 | paths 79 | .iter() 80 | .filter_map(|(path, item)| item.as_item().map(|i| (path, i))) 81 | .flat_map(|(path, item)| { 82 | item.iter() 83 | .map(move |(method, op)| (path.as_str(), method, op)) 84 | }) 85 | }) 86 | } 87 | } 88 | 89 | mod serde_version { 90 | use std::borrow::Cow; 91 | 92 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 93 | 94 | pub fn serialize(_: &Cow<'static, str>, ser: S) -> Result { 95 | Cow::Borrowed("3.1.0").serialize(ser) 96 | } 97 | 98 | pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result, D::Error> { 99 | <&'de str>::deserialize(de).and_then(|s| match s == "3.1.0" { 100 | true => Ok(Cow::Owned("3.1.0".to_owned())), 101 | false => Err(serde::de::Error::custom("expected 3.1.0")), 102 | }) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use crate::openapi::OpenApi; 109 | 110 | #[test] 111 | fn test_default_openapi_deserialize() { 112 | let mut api = OpenApi::default(); 113 | api.openapi = std::borrow::Cow::Borrowed("3.1.0"); 114 | 115 | let json = serde_json::to_string(&api).unwrap(); 116 | 117 | let deser_api = serde_json::from_str::(&json).unwrap(); 118 | 119 | assert_eq!(api, deser_api); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/operation.rs: -------------------------------------------------------------------------------- 1 | use crate::{openapi::*, util::*}; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Describes a single API operation on a path. 6 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] 7 | #[serde(rename_all = "camelCase")] 8 | #[derive(schemars::JsonSchema)] 9 | pub struct Operation { 10 | /// A list of tags for API documentation control. 11 | /// Tags can be used for logical grouping of operations 12 | /// by resources or any other qualifier. 13 | #[serde(default)] 14 | #[serde(skip_serializing_if = "Vec::is_empty")] 15 | pub tags: Vec, 16 | /// A short summary of what the operation does. 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub summary: Option, 19 | /// A verbose explanation of the operation behavior. 20 | /// CommonMark syntax MAY be used for rich text representation. 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub description: Option, 23 | /// Additional external documentation for this operation. 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub external_docs: Option, 26 | /// Unique string used to identify the operation. 27 | /// The id MUST be unique among all operations described in the API. 28 | /// Tools and libraries MAY use the operationId to uniquely identify 29 | /// an operation, therefore, it is RECOMMENDED to follow common 30 | /// programming naming conventions. 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | pub operation_id: Option, 33 | /// A list of parameters that are applicable for this operation. 34 | /// If a parameter is already defined at the Path Item, the new 35 | /// definition will override it but can never remove it. 36 | /// The list MUST NOT include duplicated parameters. A unique 37 | /// parameter is defined by a combination of a name and location. 38 | /// The list can use the Reference Object to link to parameters 39 | /// that are defined at the OpenAPI Object's components/parameters. 40 | #[serde(default)] 41 | #[serde(skip_serializing_if = "Vec::is_empty")] 42 | pub parameters: Vec>, 43 | /// The request body applicable for this operation. 44 | /// The requestBody is fully supported in HTTP methods 45 | /// where the HTTP 1.1 specification RFC7231 has explicitly 46 | /// defined semantics for request bodies. In other cases where 47 | /// the HTTP spec is vague (such as 48 | /// [GET](https://tools.ietf.org/html/rfc7231#section-4.3.1), 49 | /// [HEAD](https://tools.ietf.org/html/rfc7231#section-4.3.2) and 50 | /// [DELETE](https://tools.ietf.org/html/rfc7231#section-4.3.5)), 51 | /// requestBody is permitted but does not have well-defined semantics and 52 | /// SHOULD be avoided if possible. 53 | #[serde(skip_serializing_if = "Option::is_none")] 54 | pub request_body: Option>, 55 | /// The list of possible responses as they are returned 56 | /// from executing this operation. 57 | #[serde(skip_serializing_if = "Option::is_none")] 58 | pub responses: Option, 59 | /// Declares this operation to be deprecated.Default value is false. 60 | #[serde(default, skip_serializing_if = "is_false")] 61 | pub deprecated: bool, 62 | /// A declaration of which security mechanisms can be used for this 63 | /// operation. The list of values includes alternative security 64 | /// requirement objects that can be used. Only one of the security 65 | /// requirement objects need to be satisfied to authorize a request. 66 | /// This definition overrides any declared top-level security. To remove 67 | /// a top-level security declaration, an empty array can be used. 68 | #[serde(default)] 69 | #[serde(skip_serializing_if = "Vec::is_empty")] 70 | pub security: Vec, 71 | /// An alternative server array to service this operation. 72 | /// If an alternative server object is specified at the 73 | /// Path Item Object or Root level, it will be overridden by this value. 74 | #[serde(default)] 75 | #[serde(skip_serializing_if = "Vec::is_empty")] 76 | pub servers: Vec, 77 | /// Callbacks for the operation. 78 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 79 | pub callbacks: IndexMap>, 80 | /// Inline extensions to this object. 81 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 82 | pub extensions: IndexMap, 83 | } 84 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/parameter.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::large_enum_variant)] 2 | use crate::{openapi::*, util::*}; 3 | use indexmap::IndexMap; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Describes a single operation parameter. 7 | /// 8 | /// A unique parameter is defined by a combination of a name and location. 9 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] 10 | pub struct ParameterData { 11 | /// REQUIRED. The name of the parameter. Parameter names are case sensitive. 12 | /// If in is "path", the name field MUST correspond to the associated path 13 | /// segment from the path field in the Paths Object. See Path Templating for 14 | /// further information. 15 | /// 16 | /// If in is "header" and the name field is "Accept", "Content-Type" or 17 | /// "Authorization", the parameter definition SHALL be ignored. 18 | /// 19 | /// For all other cases, the name corresponds to the parameter name 20 | /// used by the in property. 21 | pub name: String, 22 | /// A brief description of the parameter. This could 23 | /// contain examples of use. CommonMark syntax MAY be 24 | /// used for rich text representation. 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub description: Option, 27 | /// Determines whether this parameter is mandatory. 28 | /// If the parameter location is "path", this property 29 | /// is REQUIRED and its value MUST be true. Otherwise, 30 | /// the property MAY be included and its default value 31 | /// is false. 32 | #[serde(default, skip_serializing_if = "is_false")] 33 | pub required: bool, 34 | /// Specifies that a parameter is deprecated and SHOULD 35 | /// be transitioned out of usage. 36 | #[serde(skip_serializing_if = "Option::is_none")] 37 | pub deprecated: Option, 38 | #[serde(flatten)] 39 | pub format: ParameterSchemaOrContent, 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub example: Option, 42 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 43 | pub examples: IndexMap>, 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub explode: Option, 46 | /// Inline extensions to this object. 47 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 48 | pub extensions: IndexMap, 49 | } 50 | 51 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 52 | #[serde(rename_all = "camelCase")] 53 | #[derive(schemars::JsonSchema)] 54 | pub enum ParameterSchemaOrContent { 55 | /// The schema defining the type used for the parameter. 56 | Schema(SchemaObject), 57 | /// A map containing the representations for the parameter. The key is the 58 | /// media type and the value describes it. The map MUST only contain one 59 | /// entry. 60 | Content(Content), 61 | } 62 | 63 | pub type Content = IndexMap; 64 | 65 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 66 | #[serde(tag = "in", rename_all = "camelCase")] 67 | #[derive(schemars::JsonSchema)] 68 | pub enum Parameter { 69 | Query { 70 | #[serde(flatten)] 71 | parameter_data: ParameterData, 72 | /// Determines whether the parameter value SHOULD allow reserved 73 | /// characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included 74 | /// without percent-encoding. This property only applies to parameters 75 | /// with an in value of query. The default value is false. 76 | #[serde(default, skip_serializing_if = "is_false")] 77 | allow_reserved: bool, 78 | /// Describes how the parameter value will be serialized depending on 79 | /// the type of the parameter value. Default values (based on value of 80 | /// in): for query - form; for path - simple; for header - simple; for 81 | /// cookie - form. 82 | #[serde(default, skip_serializing_if = "SkipSerializeIfDefault::skip")] 83 | style: QueryStyle, 84 | /// Sets the ability to pass empty-valued parameters. This is 85 | /// valid only for query parameters and allows sending a parameter 86 | /// with an empty value. Default value is false. If style is used, 87 | /// and if behavior is n/a (cannot be serialized), the value of 88 | /// allowEmptyValue SHALL be ignored. 89 | #[serde(skip_serializing_if = "Option::is_none")] 90 | allow_empty_value: Option, 91 | }, 92 | Header { 93 | #[serde(flatten)] 94 | parameter_data: ParameterData, 95 | /// Describes how the parameter value will be serialized depending on 96 | /// the type of the parameter value. Default values (based on value of 97 | /// in): for query - form; for path - simple; for header - simple; for 98 | /// cookie - form. 99 | #[serde(default, skip_serializing_if = "SkipSerializeIfDefault::skip")] 100 | style: HeaderStyle, 101 | }, 102 | Path { 103 | #[serde(flatten)] 104 | parameter_data: ParameterData, 105 | /// Describes how the parameter value will be serialized depending on 106 | /// the type of the parameter value. Default values (based on value of 107 | /// in): for query - form; for path - simple; for header - simple; for 108 | /// cookie - form. 109 | #[serde(default, skip_serializing_if = "SkipSerializeIfDefault::skip")] 110 | style: PathStyle, 111 | }, 112 | Cookie { 113 | #[serde(flatten)] 114 | parameter_data: ParameterData, 115 | /// Describes how the parameter value will be serialized depending on 116 | /// the type of the parameter value. Default values (based on value of 117 | /// in): for query - form; for path - simple; for header - simple; for 118 | /// cookie - form. 119 | #[serde(default, skip_serializing_if = "SkipSerializeIfDefault::skip")] 120 | style: CookieStyle, 121 | }, 122 | } 123 | 124 | impl Parameter { 125 | /// Returns the `parameter_data` field of this [ParameterData]. 126 | pub fn parameter_data(self) -> ParameterData { 127 | match self { 128 | Parameter::Query { 129 | parameter_data, 130 | allow_reserved: _, 131 | style: _, 132 | allow_empty_value: _, 133 | } => parameter_data, 134 | Parameter::Header { 135 | parameter_data, 136 | style: _, 137 | } => parameter_data, 138 | Parameter::Path { 139 | parameter_data, 140 | style: _, 141 | } => parameter_data, 142 | Parameter::Cookie { 143 | parameter_data, 144 | style: _, 145 | } => parameter_data, 146 | } 147 | } 148 | 149 | /// Returns the `parameter_data` field of this [ParameterData] by reference. 150 | pub fn parameter_data_ref(&self) -> &ParameterData { 151 | match self { 152 | Parameter::Query { 153 | parameter_data, 154 | allow_reserved: _, 155 | style: _, 156 | allow_empty_value: _, 157 | } => parameter_data, 158 | Parameter::Header { 159 | parameter_data, 160 | style: _, 161 | } => parameter_data, 162 | Parameter::Path { 163 | parameter_data, 164 | style: _, 165 | } => parameter_data, 166 | Parameter::Cookie { 167 | parameter_data, 168 | style: _, 169 | } => parameter_data, 170 | } 171 | } 172 | 173 | /// Returns the `parameter_data` field of this [ParameterData] by mutable reference. 174 | pub fn parameter_data_mut(&mut self) -> &mut ParameterData { 175 | match self { 176 | Parameter::Query { 177 | parameter_data, 178 | allow_reserved: _, 179 | style: _, 180 | allow_empty_value: _, 181 | } => parameter_data, 182 | Parameter::Header { 183 | parameter_data, 184 | style: _, 185 | } => parameter_data, 186 | Parameter::Path { 187 | parameter_data, 188 | style: _, 189 | } => parameter_data, 190 | Parameter::Cookie { 191 | parameter_data, 192 | style: _, 193 | } => parameter_data, 194 | } 195 | } 196 | } 197 | 198 | struct SkipSerializeIfDefault; 199 | impl SkipSerializeIfDefault { 200 | #[cfg(feature = "skip_serializing_defaults")] 201 | fn skip(value: &D) -> bool { 202 | value == &Default::default() 203 | } 204 | #[cfg(not(feature = "skip_serializing_defaults"))] 205 | fn skip(_value: &D) -> bool { 206 | false 207 | } 208 | } 209 | 210 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 211 | #[serde(rename_all = "camelCase")] 212 | #[derive(schemars::JsonSchema)] 213 | pub enum PathStyle { 214 | Matrix, 215 | Label, 216 | Simple, 217 | } 218 | 219 | impl Default for PathStyle { 220 | fn default() -> Self { 221 | PathStyle::Simple 222 | } 223 | } 224 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 225 | #[serde(rename_all = "camelCase")] 226 | #[derive(schemars::JsonSchema)] 227 | pub enum QueryStyle { 228 | Form, 229 | SpaceDelimited, 230 | PipeDelimited, 231 | DeepObject, 232 | } 233 | 234 | impl Default for QueryStyle { 235 | fn default() -> Self { 236 | QueryStyle::Form 237 | } 238 | } 239 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 240 | #[serde(rename_all = "camelCase")] 241 | #[derive(schemars::JsonSchema)] 242 | pub enum CookieStyle { 243 | Form, 244 | } 245 | 246 | impl Default for CookieStyle { 247 | fn default() -> CookieStyle { 248 | CookieStyle::Form 249 | } 250 | } 251 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 252 | #[serde(rename_all = "camelCase")] 253 | #[derive(schemars::JsonSchema)] 254 | pub enum HeaderStyle { 255 | Simple, 256 | } 257 | 258 | impl Default for HeaderStyle { 259 | fn default() -> HeaderStyle { 260 | HeaderStyle::Simple 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/paths.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{openapi::*, util::*}; 4 | use indexmap::IndexMap; 5 | use serde::{Deserialize, Deserializer, Serialize}; 6 | use tracing::warn; 7 | 8 | /// Describes the operations available on a single path. 9 | /// A Path Item MAY be empty, due to ACL constraints. 10 | /// The path itself is still exposed to the documentation 11 | /// viewer but they will not know which operations and 12 | /// parameters are available. 13 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 14 | pub struct PathItem { 15 | /// Allows for a referenced definition of this path item. The referenced 16 | /// structure MUST be in the form of a Path Item Object. In case a Path 17 | /// Item Object field appears both in the defined object and the referenced 18 | /// object, the behavior is undefined. See the rules for resolving Relative 19 | /// References. 20 | #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")] 21 | pub reference: Option, 22 | /// An optional, string summary, intended to apply to all operations in 23 | /// this path. 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub summary: Option, 26 | /// An optional, string description, intended to apply to all operations in 27 | /// this path. CommonMark syntax MAY be used for rich text representation. 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub description: Option, 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub get: Option, 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | pub put: Option, 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub post: Option, 36 | #[serde(skip_serializing_if = "Option::is_none")] 37 | pub delete: Option, 38 | #[serde(skip_serializing_if = "Option::is_none")] 39 | pub options: Option, 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub head: Option, 42 | #[serde(skip_serializing_if = "Option::is_none")] 43 | pub patch: Option, 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub trace: Option, 46 | /// An alternative server array to service all operations in this path. 47 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 48 | pub servers: Vec, 49 | /// A list of parameters that are applicable for all the 50 | /// operations described under this path. These parameters 51 | /// can be overridden at the operation level, but cannot be 52 | /// removed there. The list MUST NOT include duplicated parameters. 53 | /// A unique parameter is defined by a combination of a name and location. 54 | /// The list can use the Reference Object to link to parameters that 55 | /// are defined at the OpenAPI Object's components/parameters. 56 | #[serde(default)] 57 | #[serde(skip_serializing_if = "Vec::is_empty")] 58 | pub parameters: Vec>, 59 | /// Inline extensions to this object. 60 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 61 | pub extensions: IndexMap, 62 | } 63 | 64 | impl PathItem { 65 | /// Returns an iterator of references to the [Operation]s in the [PathItem]. 66 | pub fn iter(&self) -> impl Iterator { 67 | vec![ 68 | ("get", &self.get), 69 | ("put", &self.put), 70 | ("post", &self.post), 71 | ("delete", &self.delete), 72 | ("options", &self.options), 73 | ("head", &self.head), 74 | ("patch", &self.patch), 75 | ("trace", &self.trace), 76 | ] 77 | .into_iter() 78 | .filter_map(|(method, maybe_op)| maybe_op.as_ref().map(|op| (method, op))) 79 | } 80 | 81 | /// Merges two path items as well as it can. Global settings will likely conflict. Conflicts always favor self. 82 | pub fn merge(mut self, other: Self) -> Self { 83 | self.merge_with(other); 84 | self 85 | } 86 | 87 | /// Merges self with other path item as well as it can. Global settings will likely conflict. Conflicts always favor self. 88 | pub fn merge_with(&mut self, mut other: Self) { 89 | self.servers.append(&mut other.servers); 90 | 91 | self.parameters.append(&mut other.parameters); 92 | 93 | for (k, ext) in other.extensions { 94 | self.extensions 95 | .entry(k.clone()) 96 | .and_modify(|_| warn!("Conflict on merging extension {}", k)) 97 | .or_insert(ext); 98 | } 99 | macro_rules! merge { 100 | ($id:ident) => { 101 | if let Some($id) = other.$id { 102 | if self.$id.is_some() { 103 | warn!( 104 | "Conflict on merging {}, ignoring duplicate", 105 | stringify!($id) 106 | ); 107 | } else { 108 | let _ = self.$id.insert($id); 109 | } 110 | } 111 | }; 112 | } 113 | merge!(reference); 114 | merge!(summary); 115 | merge!(description); 116 | merge!(get); 117 | merge!(put); 118 | merge!(post); 119 | merge!(delete); 120 | merge!(options); 121 | merge!(head); 122 | merge!(patch); 123 | merge!(trace); 124 | } 125 | } 126 | 127 | impl IntoIterator for PathItem { 128 | type Item = (&'static str, Operation); 129 | 130 | type IntoIter = std::vec::IntoIter; 131 | 132 | /// Returns an iterator of the [Operation]s in the [PathItem]. 133 | fn into_iter(self) -> Self::IntoIter { 134 | vec![ 135 | ("get", self.get), 136 | ("put", self.put), 137 | ("post", self.post), 138 | ("delete", self.delete), 139 | ("options", self.options), 140 | ("head", self.head), 141 | ("patch", self.patch), 142 | ("trace", self.trace), 143 | ] 144 | .into_iter() 145 | .filter_map(|(method, maybe_op)| maybe_op.map(|op| (method, op))) 146 | .collect::>() 147 | .into_iter() 148 | } 149 | } 150 | 151 | /// Holds the relative paths to the individual endpoints and 152 | /// their operations. The path is appended to the URL from the 153 | /// Server Object in order to construct the full URL. The Paths 154 | /// MAY be empty, due to Access Control List (ACL) constraints. 155 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 156 | pub struct Paths { 157 | /// A map of PathItems or references to them. 158 | #[serde(flatten, deserialize_with = "deserialize_paths")] 159 | pub paths: IndexMap>, 160 | /// Inline extensions to this object. 161 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 162 | pub extensions: IndexMap, 163 | } 164 | 165 | impl Paths { 166 | /// Iterate over path items. 167 | pub fn iter(&self) -> indexmap::map::Iter> { 168 | self.paths.iter() 169 | } 170 | } 171 | 172 | impl IntoIterator for Paths { 173 | type Item = (String, ReferenceOr); 174 | 175 | type IntoIter = indexmap::map::IntoIter>; 176 | 177 | fn into_iter(self) -> Self::IntoIter { 178 | self.paths.into_iter() 179 | } 180 | } 181 | 182 | fn deserialize_paths<'de, D>( 183 | deserializer: D, 184 | ) -> Result>, D::Error> 185 | where 186 | D: Deserializer<'de>, 187 | { 188 | deserializer.deserialize_map(PredicateVisitor( 189 | |key: &String| key.starts_with('/'), 190 | PhantomData, 191 | )) 192 | } 193 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/reference.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 4 | #[serde(untagged)] 5 | #[derive(schemars::JsonSchema)] 6 | pub enum ReferenceOr { 7 | Reference { 8 | /// REQUIRED. The reference identifier. This MUST be in the form of a 9 | /// URI. 10 | #[serde(rename = "$ref")] 11 | reference: String, 12 | /// A short summary which by default SHOULD override that of the 13 | /// referenced component. If the referenced object-type does not allow a 14 | /// `summary` field, then this field has no effect. 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | summary: Option, 17 | /// A description which by default SHOULD override that of the 18 | /// referenced component. CommonMark syntax MAY be used for rich text 19 | /// representation. If the referenced object-type does not allow a 20 | /// `description` field, then this field has no effect. 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | description: Option, 23 | }, 24 | Item(T), 25 | } 26 | 27 | impl ReferenceOr { 28 | pub fn ref_(r: &str) -> Self { 29 | ReferenceOr::Reference { 30 | reference: r.to_owned(), 31 | summary: None, 32 | description: None, 33 | } 34 | } 35 | pub fn boxed_item(item: T) -> ReferenceOr> { 36 | ReferenceOr::Item(Box::new(item)) 37 | } 38 | 39 | /// Converts this [ReferenceOr] to the item inside, if it exists. 40 | /// 41 | /// The return value will be [Option::Some] if this was a 42 | /// [ReferenceOr::Item] or [Option::None] if this was a 43 | /// [ReferenceOr::Reference]. 44 | /// 45 | /// # Examples 46 | /// 47 | /// ``` 48 | /// # use aide::openapi::ReferenceOr; 49 | /// 50 | /// let i = ReferenceOr::Item(1); 51 | /// assert_eq!(i.into_item(), Some(1)); 52 | /// 53 | /// let j: ReferenceOr = ReferenceOr::Reference { reference: String::new(), summary: None, description: None }; 54 | /// assert_eq!(j.into_item(), None); 55 | /// ``` 56 | pub fn into_item(self) -> Option { 57 | match self { 58 | ReferenceOr::Reference { .. } => None, 59 | ReferenceOr::Item(i) => Some(i), 60 | } 61 | } 62 | 63 | /// Returns a reference to to the item inside this [ReferenceOr], if it 64 | /// exists. 65 | /// 66 | /// The return value will be [Option::Some] if this was a 67 | /// [ReferenceOr::Item] or [Option::None] if this was a 68 | /// [ReferenceOr::Reference]. 69 | /// 70 | /// # Examples 71 | /// 72 | /// ``` 73 | /// # use aide::openapi::ReferenceOr; 74 | /// 75 | /// let i = ReferenceOr::Item(1); 76 | /// assert_eq!(i.as_item(), Some(&1)); 77 | /// 78 | /// let j: ReferenceOr = ReferenceOr::Reference { reference: String::new(), summary: None, description: None }; 79 | /// assert_eq!(j.as_item(), None); 80 | /// ``` 81 | pub fn as_item(&self) -> Option<&T> { 82 | match self { 83 | ReferenceOr::Reference { .. } => None, 84 | ReferenceOr::Item(i) => Some(i), 85 | } 86 | } 87 | 88 | /// Mutable version of [`as_item`](ReferenceOr::as_item). 89 | pub fn as_item_mut(&mut self) -> Option<&mut T> { 90 | match self { 91 | ReferenceOr::Reference { .. } => None, 92 | ReferenceOr::Item(i) => Some(i), 93 | } 94 | } 95 | } 96 | 97 | impl ReferenceOr> { 98 | pub fn unbox(self) -> ReferenceOr { 99 | match self { 100 | ReferenceOr::Reference { 101 | reference, 102 | summary, 103 | description, 104 | } => ReferenceOr::Reference { 105 | reference, 106 | summary, 107 | description, 108 | }, 109 | ReferenceOr::Item(boxed) => ReferenceOr::Item(*boxed), 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/request_body.rs: -------------------------------------------------------------------------------- 1 | use crate::{openapi::*, util::*}; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 6 | pub struct RequestBody { 7 | /// A brief description of the request body. 8 | /// This could contain examples of use. 9 | /// CommonMark syntax MAY be used for rich text representation. 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub description: Option, 12 | /// REQUIRED. The content of the request body. 13 | /// The key is a media type or media type range and 14 | /// the value describes it. For requests that match 15 | /// multiple keys, only the most specific key is applicable. 16 | /// e.g. text/plain overrides text/* 17 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 18 | pub content: IndexMap, 19 | /// Determines if the request body is required in the 20 | /// request. Defaults to false. 21 | #[serde(default, skip_serializing_if = "is_false")] 22 | pub required: bool, 23 | /// Inline extensions to this object. 24 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 25 | pub extensions: IndexMap, 26 | } 27 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/responses.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{openapi::*, util::*}; 4 | use indexmap::IndexMap; 5 | use serde::{Deserialize, Deserializer, Serialize}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 8 | pub struct Responses { 9 | /// The documentation of responses other than the ones declared 10 | /// for specific HTTP response codes. Use this field to cover 11 | /// undeclared responses. 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub default: Option>, 14 | /// Any HTTP status code can be used as the property name, 15 | /// but only one property per code, to describe the expected 16 | /// response for that HTTP status code. This field MUST be enclosed in 17 | /// quotation marks (for example, "200") for compatibility between 18 | /// JSON and YAML. To define a range of response codes, this field 19 | /// MAY contain the uppercase wildcard character X. For example, 20 | /// 2XX represents all response codes between [200-299]. The following 21 | /// range definitions are allowed: 1XX, 2XX, 3XX, 4XX, and 5XX. 22 | /// If a response range is defined using an explicit code, the 23 | /// explicit code definition takes precedence over the range 24 | /// definition for that code. 25 | #[serde(flatten, deserialize_with = "deserialize_responses")] 26 | pub responses: IndexMap>, 27 | /// Inline extensions to this object. 28 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 29 | pub extensions: IndexMap, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 33 | pub struct Response { 34 | /// REQUIRED. A description of the response. 35 | /// CommonMark syntax MAY be used for rich text representation. 36 | pub description: String, 37 | 38 | /// Maps a header name to its definition. 39 | /// RFC7230 states header names are case insensitive. 40 | /// If a response header is defined with the name "Content-Type", 41 | /// it SHALL be ignored. 42 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 43 | pub headers: IndexMap>, 44 | 45 | /// A map containing descriptions of potential response payloads. 46 | /// The key is a media type or media type range and the value 47 | /// describes it. For responses that match multiple keys, 48 | /// only the most specific key is applicable. e.g. text/plain 49 | /// overrides text/* 50 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 51 | pub content: IndexMap, 52 | 53 | /// A map of operations links that can be followed from the response. 54 | /// The key of the map is a short name for the link, following 55 | /// the naming constraints of the names for Component Objects. 56 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 57 | pub links: IndexMap>, 58 | 59 | /// Inline extensions to this object. 60 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 61 | pub extensions: IndexMap, 62 | } 63 | 64 | fn deserialize_responses<'de, D>( 65 | deserializer: D, 66 | ) -> Result>, D::Error> 67 | where 68 | D: Deserializer<'de>, 69 | { 70 | // We rely on the result of StatusCode::deserialize to act as our 71 | // predicate; it will succeed only for status code. 72 | deserializer.deserialize_map(PredicateVisitor(|_: &StatusCode| true, PhantomData)) 73 | } 74 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/schema.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::large_enum_variant)] 2 | use crate::openapi::*; 3 | use schemars::Schema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)] 7 | pub struct SchemaObject { 8 | #[serde(flatten)] 9 | pub json_schema: Schema, 10 | /// Additional external documentation for this schema. 11 | #[serde(rename = "externalDocs")] 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub external_docs: Option, 14 | /// A free-form property to include an example of an instance for this 15 | /// schema. To represent examples that cannot be naturally represented in 16 | /// JSON or YAML, a string value can be used to contain the example with 17 | /// escaping where necessary. **Deprecated:** The `example` property has 18 | /// been deprecated in favor of the JSON Schema `examples` keyword. Use 19 | /// of `example` is discouraged, and later versions of this 20 | /// specification may remove it. 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub example: Option, 23 | } 24 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/security_requirement.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | 3 | /// Lists the required security schemes to execute this operation. 4 | /// The name used for each property MUST correspond to a security 5 | /// scheme declared in the Security Schemes under the Components Object. 6 | /// 7 | /// Security Requirement Objects that contain multiple schemes require 8 | /// that all schemes MUST be satisfied for a request to be authorized. 9 | /// This enables support for scenarios where multiple query parameters or 10 | /// HTTP headers are required to convey security information. 11 | /// 12 | /// When a list of Security Requirement Objects is defined on the 13 | /// Open API object or Operation Object, only one of 14 | /// Security Requirement Objects in the list needs to be satisfied 15 | /// to authorize the request. 16 | pub type SecurityRequirement = IndexMap>; 17 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/security_scheme.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::large_enum_variant)] 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Defines a security scheme that can be used by the operations. 6 | /// Supported schemes are HTTP authentication, an API key (either as a 7 | /// header or as a query parameter), OAuth2's common flows (implicit, password, 8 | /// application and access code) as defined in RFC6749, and OpenID Connect 9 | /// Discovery. 10 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 11 | #[serde(tag = "type")] 12 | #[derive(schemars::JsonSchema)] 13 | pub enum SecurityScheme { 14 | #[serde(rename = "apiKey")] 15 | ApiKey { 16 | #[serde(rename = "in")] 17 | location: ApiKeyLocation, 18 | name: String, 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | description: Option, 21 | /// Inline extensions to this object. 22 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 23 | extensions: IndexMap, 24 | }, 25 | #[serde(rename = "http")] 26 | Http { 27 | scheme: String, 28 | #[serde(rename = "bearerFormat")] 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | bearer_format: Option, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | description: Option, 33 | /// Inline extensions to this object. 34 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 35 | extensions: IndexMap, 36 | }, 37 | #[serde(rename = "oauth2")] 38 | OAuth2 { 39 | flows: OAuth2Flows, 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | description: Option, 42 | /// Inline extensions to this object. 43 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 44 | extensions: IndexMap, 45 | }, 46 | #[serde(rename = "openIdConnect")] 47 | OpenIdConnect { 48 | #[serde(rename = "openIdConnectUrl")] 49 | open_id_connect_url: String, 50 | #[serde(skip_serializing_if = "Option::is_none")] 51 | description: Option, 52 | /// Inline extensions to this object. 53 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 54 | extensions: IndexMap, 55 | }, 56 | #[serde(rename = "mutualTLS")] 57 | MutualTls { 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | description: Option, 60 | /// Inline extensions to this object. 61 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 62 | extensions: IndexMap, 63 | }, 64 | } 65 | 66 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 67 | #[serde(rename_all = "camelCase")] 68 | #[derive(schemars::JsonSchema)] 69 | pub enum ApiKeyLocation { 70 | Query, 71 | Header, 72 | Cookie, 73 | } 74 | 75 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] 76 | #[serde(rename_all = "camelCase")] 77 | #[derive(schemars::JsonSchema)] 78 | pub struct OAuth2Flows { 79 | #[serde(flatten)] 80 | pub implicit: Option, 81 | #[serde(flatten)] 82 | pub password: Option, 83 | #[serde(flatten)] 84 | pub client_credentials: Option, 85 | #[serde(flatten)] 86 | pub authorization_code: Option, 87 | } 88 | 89 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 90 | #[serde(rename_all = "camelCase")] 91 | #[derive(schemars::JsonSchema)] 92 | pub enum OAuth2Flow { 93 | #[serde(rename_all = "camelCase")] 94 | Implicit { 95 | authorization_url: String, 96 | refresh_url: Option, 97 | #[serde(default)] 98 | scopes: IndexMap, 99 | }, 100 | #[serde(rename_all = "camelCase")] 101 | Password { 102 | refresh_url: Option, 103 | token_url: String, 104 | #[serde(default)] 105 | scopes: IndexMap, 106 | }, 107 | #[serde(rename_all = "camelCase")] 108 | ClientCredentials { 109 | refresh_url: Option, 110 | token_url: String, 111 | #[serde(default)] 112 | scopes: IndexMap, 113 | }, 114 | #[serde(rename_all = "camelCase")] 115 | AuthorizationCode { 116 | authorization_url: String, 117 | token_url: String, 118 | refresh_url: Option, 119 | #[serde(default)] 120 | scopes: IndexMap, 121 | }, 122 | } 123 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/server.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::*; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// An object representing a Server. 6 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 7 | pub struct Server { 8 | /// REQUIRED. A URL to the target host. 9 | /// This URL supports Server Variables and MAY be relative, 10 | /// to indicate that the host location is relative to the 11 | /// location where the OpenAPI document is being served. 12 | /// Variable substitutions will be made when a variable 13 | /// is named in {brackets}. 14 | pub url: String, 15 | /// An optional string describing the host designated 16 | /// by the URL. CommonMark syntax MAY be used for rich 17 | /// text representation. 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub description: Option, 20 | /// A map between a variable name and its value. 21 | /// The value is used for substitution in the server's URL template. 22 | #[serde(default, skip_serializing_if = "IndexMap::is_empty")] 23 | pub variables: IndexMap, 24 | /// Inline extensions to this object. 25 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 26 | pub extensions: IndexMap, 27 | } 28 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/server_variable.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// An object representing a Server Variable 5 | /// for server URL template substitution. 6 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 7 | pub struct ServerVariable { 8 | /// An enumeration of string values to be 9 | /// used if the substitution options are from a limited set. 10 | #[serde(rename = "enum")] 11 | #[serde(default)] 12 | #[serde(skip_serializing_if = "Vec::is_empty")] 13 | pub enumeration: Vec, 14 | /// REQUIRED. The default value to use for substitution, 15 | /// and to send, if an alternate value is not supplied. 16 | /// Unlike the Schema Object's default, this value MUST 17 | /// be provided by the consumer. 18 | pub default: String, 19 | /// An optional description for the server 20 | /// variable. CommonMark syntax MAY be used 21 | /// for rich text representation. 22 | pub description: Option, 23 | /// Inline extensions to this object. 24 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 25 | pub extensions: IndexMap, 26 | } 27 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/status_code.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, schemars::JsonSchema)] 5 | pub enum StatusCode { 6 | Code(u16), 7 | Range(u16), 8 | } 9 | 10 | impl fmt::Display for StatusCode { 11 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | match self { 13 | StatusCode::Code(n) => write!(f, "{}", n), 14 | StatusCode::Range(n) => write!(f, "{}XX", n), 15 | } 16 | } 17 | } 18 | 19 | impl<'de> Deserialize<'de> for StatusCode { 20 | fn deserialize(deserializer: D) -> Result 21 | where 22 | D: serde::Deserializer<'de>, 23 | { 24 | use serde::de::{self, Unexpected, Visitor}; 25 | 26 | struct StatusCodeVisitor; 27 | 28 | impl<'de> Visitor<'de> for StatusCodeVisitor { 29 | type Value = StatusCode; 30 | 31 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 32 | formatter.write_str("number between 100 and 999 (as string or integer) or a string that matches `\\dXX`") 33 | } 34 | 35 | fn visit_i64(self, value: i64) -> Result 36 | where 37 | E: de::Error, 38 | { 39 | if (100..1000).contains(&value) { 40 | Ok(StatusCode::Code(value as u16)) 41 | } else { 42 | Err(E::invalid_value(Unexpected::Signed(value), &self)) 43 | } 44 | } 45 | 46 | fn visit_u64(self, value: u64) -> Result 47 | where 48 | E: de::Error, 49 | { 50 | if (100..1000).contains(&value) { 51 | Ok(StatusCode::Code(value as u16)) 52 | } else { 53 | Err(E::invalid_value(Unexpected::Unsigned(value), &self)) 54 | } 55 | } 56 | 57 | fn visit_str(self, value: &str) -> Result 58 | where 59 | E: de::Error, 60 | { 61 | if value.len() != 3 { 62 | return Err(E::invalid_value(Unexpected::Str(value), &"length 3")); 63 | } 64 | 65 | if let Ok(number) = value.parse::() { 66 | return self.visit_i64(number); 67 | } 68 | 69 | if !value.is_ascii() { 70 | return Err(E::invalid_value( 71 | Unexpected::Str(value), 72 | &"ascii, format `\\dXX`", 73 | )); 74 | } 75 | 76 | let v = value.as_bytes().to_ascii_uppercase(); 77 | 78 | match [v[0], v[1], v[2]] { 79 | [n, b'X', b'X'] if n.is_ascii_digit() => { 80 | Ok(StatusCode::Range(u16::from(n - b'0'))) 81 | } 82 | _ => Err(E::invalid_value(Unexpected::Str(value), &"format `\\dXX`")), 83 | } 84 | } 85 | } 86 | 87 | deserializer.deserialize_any(StatusCodeVisitor) 88 | } 89 | } 90 | 91 | impl Serialize for StatusCode { 92 | fn serialize(&self, serializer: S) -> Result 93 | where 94 | S: serde::Serializer, 95 | { 96 | serializer.serialize_str(&self.to_string()) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/tag.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::*; 2 | use indexmap::IndexMap; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Adds metadata to a single tag that is used by the 6 | /// Operation Object. It is not mandatory to have a 7 | /// Tag Object per tag defined in the Operation Object instances. 8 | #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, schemars::JsonSchema)] 9 | pub struct Tag { 10 | /// REQUIRED. The name of the tag. 11 | pub name: String, 12 | /// A description for the tag. 13 | /// CommonMark syntax MAY be used for rich text representation. 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub description: Option, 16 | /// Additional external documentation for this tag. 17 | #[serde(rename = "externalDocs", skip_serializing_if = "Option::is_none")] 18 | pub external_docs: Option, 19 | /// Inline extensions to this object. 20 | #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] 21 | pub extensions: IndexMap, 22 | } 23 | -------------------------------------------------------------------------------- /crates/aide/src/openapi/variant_or.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 4 | #[serde(untagged)] 5 | #[derive(schemars::JsonSchema)] 6 | pub enum VariantOrUnknown { 7 | Item(T), 8 | Unknown(String), 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 12 | #[serde(untagged)] 13 | #[derive(schemars::JsonSchema)] 14 | pub enum VariantOrUnknownOrEmpty { 15 | Item(T), 16 | Unknown(String), 17 | Empty, 18 | } 19 | 20 | impl VariantOrUnknownOrEmpty { 21 | pub fn is_empty(&self) -> bool { 22 | matches!(self, VariantOrUnknownOrEmpty::Empty) 23 | } 24 | } 25 | 26 | impl Default for VariantOrUnknownOrEmpty { 27 | fn default() -> Self { 28 | VariantOrUnknownOrEmpty::Empty 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/aide/src/redoc/mod.rs: -------------------------------------------------------------------------------- 1 | //! Generate [Redoc] ui. This feature requires the `axum` feature. 2 | //! 3 | //! ## Example: 4 | //! 5 | //! ```no_run 6 | //! // Replace some of the `axum::` types with `aide::axum::` ones. 7 | //! use aide::{ 8 | //! axum::{ 9 | //! routing::{get, post}, 10 | //! ApiRouter, IntoApiResponse, 11 | //! }, 12 | //! openapi::{Info, OpenApi}, 13 | //! redoc::Redoc, 14 | //! }; 15 | //! use axum::{Extension, Json}; 16 | //! use schemars::JsonSchema; 17 | //! use serde::Deserialize; 18 | //! 19 | //! // We'll need to derive `JsonSchema` for 20 | //! // all types that appear in the api documentation. 21 | //! #[derive(Deserialize, JsonSchema)] 22 | //! struct User { 23 | //! name: String, 24 | //! } 25 | //! 26 | //! async fn hello_user(Json(user): Json) -> impl IntoApiResponse { 27 | //! format!("hello {}", user.name) 28 | //! } 29 | //! 30 | //! // Note that this clones the document on each request. 31 | //! // To be more efficient, we could wrap it into an Arc, 32 | //! // or even store it as a serialized string. 33 | //! async fn serve_api(Extension(api): Extension) -> impl IntoApiResponse { 34 | //! Json(api) 35 | //! } 36 | //! 37 | //! #[tokio::main] 38 | //! async fn main() { 39 | //! let app = ApiRouter::new() 40 | //! // generate redoc-ui using the openapi spec route 41 | //! .route("/redoc", Redoc::new("/api.json").axum_route()) 42 | //! // Change `route` to `api_route` for the route 43 | //! // we'd like to expose in the documentation. 44 | //! .api_route("/hello", post(hello_user)) 45 | //! // We'll serve our generated document here. 46 | //! .route("/api.json", get(serve_api)); 47 | //! 48 | //! let mut api = OpenApi { 49 | //! info: Info { 50 | //! description: Some("an example API".to_string()), 51 | //! ..Info::default() 52 | //! }, 53 | //! ..OpenApi::default() 54 | //! }; 55 | //! 56 | //! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 57 | //! 58 | //! axum::serve( 59 | //! listener, 60 | //! app 61 | //! // Generate the documentation. 62 | //! .finish_api(&mut api) 63 | //! // Expose the documentation to the handlers. 64 | //! .layer(Extension(api)) 65 | //! .into_make_service(), 66 | //! ) 67 | //! .await 68 | //! .unwrap(); 69 | //! } 70 | //! ``` 71 | 72 | /// A wrapper to embed [Redoc](https://redocly.com/) in your app. 73 | #[must_use] 74 | pub struct Redoc { 75 | title: String, 76 | spec_url: String, 77 | } 78 | 79 | impl Redoc { 80 | /// Create a new [`Redoc`] wrapper with the given spec url. 81 | pub fn new(spec_url: impl Into) -> Self { 82 | Self { 83 | title: "Redoc".into(), 84 | spec_url: spec_url.into(), 85 | } 86 | } 87 | 88 | /// Set the title of the Redoc page. 89 | pub fn with_title(mut self, title: &str) -> Self { 90 | self.title = title.into(); 91 | self 92 | } 93 | 94 | /// Build the redoc-ui html page. 95 | #[must_use] 96 | pub fn html(&self) -> String { 97 | format!( 98 | r#" 99 | 100 | 101 | 102 | {title} 103 | 104 | 105 | 106 |

107 | 114 | 115 | 116 | "#, 117 | redoc_js = include_str!("../../res/redoc/redoc.standalone.js"), 118 | title = self.title, 119 | spec_url = self.spec_url 120 | ) 121 | } 122 | } 123 | 124 | #[cfg(feature = "axum")] 125 | mod axum_impl { 126 | use crate::axum::{ 127 | routing::{get, ApiMethodRouter}, 128 | AxumOperationHandler, 129 | }; 130 | use crate::redoc::get_static_str; 131 | use axum::response::Html; 132 | 133 | impl super::Redoc { 134 | /// Returns an [`ApiMethodRouter`] to expose the Redoc UI. 135 | /// 136 | /// # Examples 137 | /// 138 | /// ``` 139 | /// # use aide::axum::{ApiRouter, routing::get}; 140 | /// # use aide::redoc::Redoc; 141 | /// ApiRouter::<()>::new() 142 | /// .route("/docs", Redoc::new("/openapi.json").axum_route()); 143 | /// ``` 144 | pub fn axum_route(&self) -> ApiMethodRouter 145 | where 146 | S: Clone + Send + Sync + 'static, 147 | { 148 | get(self.axum_handler()) 149 | } 150 | 151 | /// Returns an axum [`Handler`](axum::handler::Handler) that can be used 152 | /// with API routes. 153 | /// 154 | /// # Examples 155 | /// 156 | /// ``` 157 | /// # use aide::axum::{ApiRouter, routing::get_with}; 158 | /// # use aide::redoc::Redoc; 159 | /// ApiRouter::<()>::new().api_route( 160 | /// "/docs", 161 | /// get_with(Redoc::new("/openapi.json").axum_handler(), |op| { 162 | /// op.description("This documentation page.") 163 | /// }), 164 | /// ); 165 | /// ``` 166 | #[must_use] 167 | pub fn axum_handler( 168 | &self, 169 | ) -> impl AxumOperationHandler<(), Html<&'static str>, ((),), S> { 170 | let html = self.html(); 171 | // This string will be used during the entire lifetime of the program 172 | // so it's safe to leak it 173 | // we can't use once_cell::sync::Lazy because it will cache the first access to the function, 174 | // so you won't be able to have multiple instances of Redoc 175 | // e.g. /v1/docs and /v2/docs 176 | // Without caching we will have to clone whole html string on each request 177 | // which will use 3GiBs of RAM for 200+ concurrent requests 178 | let html: &'static str = get_static_str(html); 179 | 180 | move || async move { Html(html) } 181 | } 182 | } 183 | } 184 | 185 | fn get_static_str(string: String) -> &'static str { 186 | let static_str = Box::leak(string.into_boxed_str()); 187 | static_str 188 | } 189 | -------------------------------------------------------------------------------- /crates/aide/src/scalar/mod.rs: -------------------------------------------------------------------------------- 1 | //! Generate [Scalar] API References. This feature requires the `axum` feature. 2 | //! 3 | //! ## Example: 4 | //! 5 | //! ```no_run 6 | //! // Replace some of the `axum::` types with `aide::axum::` ones. 7 | //! use aide::{ 8 | //! axum::{ 9 | //! routing::{get, post}, 10 | //! ApiRouter, IntoApiResponse, 11 | //! }, 12 | //! openapi::{Info, OpenApi}, 13 | //! scalar::Scalar, 14 | //! }; 15 | //! use axum::{Extension, Json}; 16 | //! use schemars::JsonSchema; 17 | //! use serde::Deserialize; 18 | //! 19 | //! // We'll need to derive `JsonSchema` for 20 | //! // all types that appear in the api documentation. 21 | //! #[derive(Deserialize, JsonSchema)] 22 | //! struct User { 23 | //! name: String, 24 | //! } 25 | //! 26 | //! async fn hello_user(Json(user): Json) -> impl IntoApiResponse { 27 | //! format!("hello {}", user.name) 28 | //! } 29 | //! 30 | //! // Note that this clones the document on each request. 31 | //! // To be more efficient, we could wrap it into an Arc, 32 | //! // or even store it as a serialized string. 33 | //! async fn serve_api(Extension(api): Extension) -> impl IntoApiResponse { 34 | //! Json(api) 35 | //! } 36 | //! 37 | //! #[tokio::main] 38 | //! async fn main() { 39 | //! let app = ApiRouter::new() 40 | //! // generate Scalar API References using the openapi spec route 41 | //! .route("/scalar", Scalar::new("/api.json").axum_route()) 42 | //! // Change `route` to `api_route` for the route 43 | //! // we'd like to expose in the documentation. 44 | //! .api_route("/hello", post(hello_user)) 45 | //! // We'll serve our generated document here. 46 | //! .route("/api.json", get(serve_api)); 47 | //! 48 | //! let mut api = OpenApi { 49 | //! info: Info { 50 | //! description: Some("an example API".to_string()), 51 | //! ..Info::default() 52 | //! }, 53 | //! ..OpenApi::default() 54 | //! }; 55 | //! 56 | //! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 57 | //! 58 | //! axum::serve( 59 | //! listener, 60 | //! app 61 | //! // Generate the documentation. 62 | //! .finish_api(&mut api) 63 | //! // Expose the documentation to the handlers. 64 | //! .layer(Extension(api)) 65 | //! .into_make_service(), 66 | //! ) 67 | //! .await 68 | //! .unwrap(); 69 | //! } 70 | //! ``` 71 | 72 | /// A wrapper to embed [Scalar](https://github.com/scalar/scalar) in your app. 73 | #[must_use] 74 | pub struct Scalar { 75 | title: String, 76 | spec_url: String, 77 | } 78 | 79 | impl Scalar { 80 | /// Create a new [`Scalar`] wrapper with the given spec url. 81 | pub fn new(spec_url: impl Into) -> Self { 82 | Self { 83 | title: "Scalar".into(), 84 | spec_url: spec_url.into(), 85 | } 86 | } 87 | 88 | /// Set the title of the Scalar page. 89 | pub fn with_title(mut self, title: &str) -> Self { 90 | self.title = title.into(); 91 | self 92 | } 93 | 94 | /// Build the Scalar API References html page. 95 | #[must_use] 96 | pub fn html(&self) -> String { 97 | format!( 98 | r#" 99 | 100 | 101 | 102 | {title} 103 | 104 | 107 | 112 | 113 | 114 | 116 | 128 | 131 | 132 | 133 | "#, 134 | scalar_js = include_str!("../../res/scalar/scalar.standalone.min.js"), 135 | scalar_css = include_str!("../../res/scalar/rust-theme.css"), 136 | title = self.title, 137 | spec_url = self.spec_url 138 | ) 139 | } 140 | } 141 | 142 | #[cfg(feature = "axum")] 143 | mod axum_impl { 144 | use crate::axum::{ 145 | routing::{get, ApiMethodRouter}, 146 | AxumOperationHandler, 147 | }; 148 | use axum::response::Html; 149 | 150 | impl super::Scalar { 151 | /// Returns an [`ApiMethodRouter`] to expose the Scalar API References. 152 | /// 153 | /// # Examples 154 | /// 155 | /// ``` 156 | /// # use aide::axum::{ApiRouter, routing::get}; 157 | /// # use aide::scalar::Scalar; 158 | /// ApiRouter::<()>::new() 159 | /// .route("/docs", Scalar::new("/openapi.json").axum_route()); 160 | /// ``` 161 | pub fn axum_route(&self) -> ApiMethodRouter 162 | where 163 | S: Clone + Send + Sync + 'static, 164 | { 165 | get(self.axum_handler()) 166 | } 167 | 168 | /// Returns an axum [`Handler`](axum::handler::Handler) that can be used 169 | /// with API routes. 170 | /// 171 | /// # Examples 172 | /// 173 | /// ``` 174 | /// # use aide::axum::{ApiRouter, routing::get_with}; 175 | /// # use aide::scalar::Scalar; 176 | /// ApiRouter::<()>::new().api_route( 177 | /// "/docs", 178 | /// get_with(Scalar::new("/openapi.json").axum_handler(), |op| { 179 | /// op.description("This documentation page.") 180 | /// }), 181 | /// ); 182 | /// ``` 183 | #[must_use] 184 | pub fn axum_handler( 185 | &self, 186 | ) -> impl AxumOperationHandler<(), Html<&'static str>, ((),), S> { 187 | let html = self.html(); 188 | // This string will be used during the entire lifetime of the program 189 | // so it's safe to leak it 190 | // we can't use once_cell::sync::Lazy because it will cache the first access to the function, 191 | // so you won't be able to have multiple instances of Scalar 192 | // e.g. /v1/docs and /v2/docs 193 | // Without caching we will have to clone whole html string on each request 194 | // which will use 3GiBs of RAM for 200+ concurrent requests 195 | let html: &'static str = get_static_str(html); 196 | 197 | move || async move { Html(html) } 198 | } 199 | } 200 | 201 | fn get_static_str(string: String) -> &'static str { 202 | let static_str = Box::leak(string.into_boxed_str()); 203 | static_str 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /crates/aide/src/swagger/mod.rs: -------------------------------------------------------------------------------- 1 | //! Generate [Swagger] ui. This feature requires the `axum` feature. 2 | //! 3 | //! ## Example: 4 | //! 5 | //! ```no_run 6 | //! // Replace some of the `axum::` types with `aide::axum::` ones. 7 | //! use aide::{ 8 | //! axum::{ 9 | //! routing::{get, post}, 10 | //! ApiRouter, IntoApiResponse, 11 | //! }, 12 | //! openapi::{Info, OpenApi}, 13 | //! swagger::Swagger, 14 | //! }; 15 | //! use axum::{Extension, Json}; 16 | //! use schemars::JsonSchema; 17 | //! use serde::Deserialize; 18 | //! 19 | //! // We'll need to derive `JsonSchema` for 20 | //! // all types that appear in the api documentation. 21 | //! #[derive(Deserialize, JsonSchema)] 22 | //! struct User { 23 | //! name: String, 24 | //! } 25 | //! 26 | //! async fn hello_user(Json(user): Json) -> impl IntoApiResponse { 27 | //! format!("hello {}", user.name) 28 | //! } 29 | //! 30 | //! // Note that this clones the document on each request. 31 | //! // To be more efficient, we could wrap it into an Arc, 32 | //! // or even store it as a serialized string. 33 | //! async fn serve_api(Extension(api): Extension) -> impl IntoApiResponse { 34 | //! Json(api) 35 | //! } 36 | //! 37 | //! #[tokio::main] 38 | //! async fn main() { 39 | //! let app = ApiRouter::new() 40 | //! // generate swagger-ui using the openapi spec route 41 | //! .route("/swagger", Swagger::new("/api.json").axum_route()) 42 | //! // Change `route` to `api_route` for the route 43 | //! // we'd like to expose in the documentation. 44 | //! .api_route("/hello", post(hello_user)) 45 | //! // We'll serve our generated document here. 46 | //! .route("/api.json", get(serve_api)); 47 | //! 48 | //! let mut api = OpenApi { 49 | //! info: Info { 50 | //! description: Some("an example API".to_string()), 51 | //! ..Info::default() 52 | //! }, 53 | //! ..OpenApi::default() 54 | //! }; 55 | //! 56 | //! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 57 | //! 58 | //! axum::serve( 59 | //! listener, 60 | //! app 61 | //! // Generate the documentation. 62 | //! .finish_api(&mut api) 63 | //! // Expose the documentation to the handlers. 64 | //! .layer(Extension(api)) 65 | //! .into_make_service(), 66 | //! ) 67 | //! .await 68 | //! .unwrap(); 69 | //! } 70 | //! ``` 71 | 72 | /// A wrapper to embed [Swagger](https://swagger.io/) in your app. 73 | #[must_use] 74 | pub struct Swagger { 75 | title: String, 76 | spec_url: String, 77 | } 78 | 79 | impl Swagger { 80 | /// Create a new [`Swagger`] wrapper with the given spec url. 81 | pub fn new(spec_url: impl Into) -> Self { 82 | Self { 83 | title: "Swagger - UI".into(), 84 | spec_url: spec_url.into(), 85 | } 86 | } 87 | 88 | /// Set the title of the Swagger page. 89 | pub fn with_title(mut self, title: &str) -> Self { 90 | self.title = title.into(); 91 | self 92 | } 93 | 94 | /// Build the swagger-ui html page. 95 | #[must_use] 96 | pub fn html(&self) -> String { 97 | format!( 98 | r#" 99 | 100 | 101 | 102 | {title} 103 | 104 | 105 |
106 |
107 | 108 | 109 | 123 | 124 | 125 | "#, 126 | swagger_js = include_str!("../../res/swagger/swagger-ui-bundle.js"), 127 | swagger_css = include_str!("../../res/swagger/swagger-ui.css"), 128 | title = self.title, 129 | spec_url = self.spec_url 130 | ) 131 | } 132 | } 133 | 134 | #[cfg(feature = "axum")] 135 | mod axum_impl { 136 | use crate::axum::{ 137 | routing::{get, ApiMethodRouter}, 138 | AxumOperationHandler, 139 | }; 140 | use crate::swagger::get_static_str; 141 | use axum::response::Html; 142 | 143 | impl super::Swagger { 144 | /// Returns an [`ApiMethodRouter`] to expose the Swagger UI. 145 | /// 146 | /// # Examples 147 | /// 148 | /// ``` 149 | /// # use aide::axum::{ApiRouter, routing::get}; 150 | /// # use aide::swagger::Swagger; 151 | /// ApiRouter::<()>::new() 152 | /// .route("/docs", Swagger::new("/openapi.json").axum_route()); 153 | /// ``` 154 | pub fn axum_route(&self) -> ApiMethodRouter 155 | where 156 | S: Clone + Send + Sync + 'static, 157 | { 158 | get(self.axum_handler()) 159 | } 160 | 161 | /// Returns an axum [`Handler`](axum::handler::Handler) that can be used 162 | /// with API routes. 163 | /// 164 | /// # Examples 165 | /// 166 | /// ``` 167 | /// # use aide::axum::{ApiRouter, routing::get_with}; 168 | /// # use aide::swagger::Swagger; 169 | /// ApiRouter::<()>::new().api_route( 170 | /// "/docs", 171 | /// get_with(Swagger::new("/openapi.json").axum_handler(), |op| { 172 | /// op.description("This documentation page.") 173 | /// }), 174 | /// ); 175 | /// ``` 176 | #[must_use] 177 | pub fn axum_handler( 178 | &self, 179 | ) -> impl AxumOperationHandler<(), Html<&'static str>, ((),), S> { 180 | let html = self.html(); 181 | // This string will be used during the entire lifetime of the program 182 | // so it's safe to leak it 183 | // we can't use once_cell::sync::Lazy because it will cache the first access to the function, 184 | // so you won't be able to have multiple instances of Swagger 185 | // e.g. /v1/docs and /v2/docs 186 | // Without caching we will have to clone whole html string on each request 187 | // which will use 3GiBs of RAM for 200+ concurrent requests 188 | let html: &'static str = get_static_str(html); 189 | 190 | move || async move { Html(html) } 191 | } 192 | } 193 | } 194 | 195 | fn get_static_str(string: String) -> &'static str { 196 | let static_str = Box::leak(string.into_boxed_str()); 197 | static_str 198 | } 199 | -------------------------------------------------------------------------------- /crates/aide/src/util.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all, clippy::pedantic, missing_docs, dead_code)] 2 | //! Miscellaneous utilities. 3 | 4 | use crate::{ 5 | generate::GenContext, 6 | openapi::{Operation, PathItem, Response}, 7 | Error, 8 | }; 9 | 10 | /// Iterate over all operations in a path item. 11 | pub fn iter_operations_mut( 12 | path: &mut PathItem, 13 | ) -> impl Iterator { 14 | let mut vec = Vec::with_capacity(8); 15 | 16 | if let Some(op) = path.get.as_mut() { 17 | vec.push(("get", op)); 18 | } 19 | if let Some(op) = path.put.as_mut() { 20 | vec.push(("put", op)); 21 | } 22 | if let Some(op) = path.post.as_mut() { 23 | vec.push(("post", op)); 24 | } 25 | if let Some(op) = path.delete.as_mut() { 26 | vec.push(("delete", op)); 27 | } 28 | if let Some(op) = path.options.as_mut() { 29 | vec.push(("options", op)); 30 | } 31 | if let Some(op) = path.head.as_mut() { 32 | vec.push(("head", op)); 33 | } 34 | if let Some(op) = path.patch.as_mut() { 35 | vec.push(("patch", op)); 36 | } 37 | if let Some(op) = path.trace.as_mut() { 38 | vec.push(("trace", op)); 39 | } 40 | 41 | vec.into_iter() 42 | } 43 | 44 | /// Helper function for nesting functions in Axum. 45 | /// 46 | /// Based on Axum's own implementation of nested paths. 47 | pub(crate) fn path_for_nested_route<'a>(path: &'a str, route: &'a str) -> String { 48 | if path.ends_with('/') { 49 | format!("{path}{}", route.trim_start_matches('/')).into() 50 | } else if route == "/" { 51 | path.into() 52 | } else { 53 | format!("{path}{route}").into() 54 | } 55 | } 56 | 57 | pub(crate) fn merge_paths(ctx: &mut GenContext, path: &str, target: &mut PathItem, from: PathItem) { 58 | if let Some(op) = from.get { 59 | if target.get.is_some() { 60 | ctx.error(Error::OperationExists(path.to_string(), "get")); 61 | } else { 62 | target.get = Some(op); 63 | } 64 | } 65 | if let Some(op) = from.put { 66 | if target.put.is_some() { 67 | ctx.error(Error::OperationExists(path.to_string(), "put")); 68 | } else { 69 | target.put = Some(op); 70 | } 71 | } 72 | if let Some(op) = from.post { 73 | if target.post.is_some() { 74 | ctx.error(Error::OperationExists(path.to_string(), "post")); 75 | } else { 76 | target.post = Some(op); 77 | } 78 | } 79 | if let Some(op) = from.delete { 80 | if target.delete.is_some() { 81 | ctx.error(Error::OperationExists(path.to_string(), "delete")); 82 | } else { 83 | target.delete = Some(op); 84 | } 85 | } 86 | if let Some(op) = from.options { 87 | if target.options.is_some() { 88 | ctx.error(Error::OperationExists(path.to_string(), "options")); 89 | } else { 90 | target.options = Some(op); 91 | } 92 | } 93 | if let Some(op) = from.head { 94 | if target.head.is_some() { 95 | ctx.error(Error::OperationExists(path.to_string(), "head")); 96 | } else { 97 | target.head = Some(op); 98 | } 99 | } 100 | if let Some(op) = from.patch { 101 | if target.patch.is_some() { 102 | ctx.error(Error::OperationExists(path.to_string(), "patch")); 103 | } else { 104 | target.patch = Some(op); 105 | } 106 | } 107 | if let Some(op) = from.trace { 108 | if target.trace.is_some() { 109 | ctx.error(Error::OperationExists(path.to_string(), "trace")); 110 | } else { 111 | target.trace = Some(op); 112 | } 113 | } 114 | 115 | if let Some(new_desc) = from.description { 116 | match &mut target.description { 117 | Some(desc) => { 118 | desc.push('\n'); 119 | desc.push_str(&new_desc); 120 | } 121 | None => target.description = Some(new_desc), 122 | } 123 | } 124 | 125 | if let Some(new_summary) = from.summary { 126 | match &mut target.summary { 127 | Some(summary) => { 128 | summary.push('\n'); 129 | summary.push_str(&new_summary); 130 | } 131 | None => target.summary = Some(new_summary), 132 | } 133 | } 134 | target.parameters.extend(from.parameters); 135 | target.extensions.extend(from.extensions); 136 | } 137 | 138 | pub(crate) fn no_content_response() -> Response { 139 | Response { 140 | description: "no content".to_string(), 141 | ..Default::default() 142 | } 143 | } 144 | 145 | // FIXME: remove the code below when the upstream openapiv3 3.1 is available. 146 | pub(crate) use spec::*; 147 | mod spec { 148 | use std::hash::Hash; 149 | use std::marker::PhantomData; 150 | 151 | use indexmap::IndexMap; 152 | use serde::{ 153 | de::{IgnoredAny, Visitor}, 154 | Deserialize, Deserializer, 155 | }; 156 | 157 | #[allow(clippy::trivially_copy_pass_by_ref)] // needs to match signature for use in serde attribute 158 | #[inline] 159 | pub(crate) const fn is_false(v: &bool) -> bool { 160 | !(*v) 161 | } 162 | 163 | pub(crate) fn deserialize_extensions<'de, D>( 164 | deserializer: D, 165 | ) -> Result, D::Error> 166 | where 167 | D: Deserializer<'de>, 168 | { 169 | deserializer.deserialize_map(PredicateVisitor( 170 | |key: &String| key.starts_with("x-"), 171 | PhantomData, 172 | )) 173 | } 174 | 175 | /// Used to deserialize IndexMap that are flattened within other structs. 176 | /// This only adds keys that satisfy the given predicate. 177 | pub(crate) struct PredicateVisitor(pub F, pub PhantomData<(K, V)>); 178 | 179 | impl<'de, F, K, V> Visitor<'de> for PredicateVisitor 180 | where 181 | F: Fn(&K) -> bool, 182 | K: Deserialize<'de> + Eq + Hash, 183 | V: Deserialize<'de>, 184 | { 185 | type Value = IndexMap; 186 | 187 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 188 | formatter.write_str("a map whose fields obey a predicate") 189 | } 190 | 191 | fn visit_map
(self, mut map: A) -> Result 192 | where 193 | A: serde::de::MapAccess<'de>, 194 | { 195 | let mut ret = Self::Value::default(); 196 | 197 | loop { 198 | match map.next_key::() { 199 | Err(_) => (), 200 | Ok(None) => break, 201 | Ok(Some(key)) if self.0(&key) => { 202 | let _ = ret.insert(key, map.next_value()?); 203 | } 204 | Ok(Some(_)) => { 205 | let _ = map.next_value::()?; 206 | } 207 | } 208 | } 209 | 210 | Ok(ret) 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /examples/example-axum-worker/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .wrangler/ -------------------------------------------------------------------------------- /examples/example-axum-worker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-axum-worker" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | aide = { path = "../../crates/aide", features = [ 12 | "redoc", 13 | "scalar", 14 | "swagger", 15 | "axum-json", 16 | "macros", 17 | ] } 18 | tower-service = "0.3.2" 19 | worker = { version = "0.5.0", features = ["http", "axum"] } 20 | console_error_panic_hook = "0.1.7" 21 | axum = { version = "0.8.1", default-features = false, features = ["macros", "form", "matched-path", "query", "original-uri"] } 22 | schemars = { version = "0.9.0", features = ["uuid1"] } 23 | serde = { version = "1.0.144", features = ["derive", "rc"] } 24 | serde_json = "1.0.85" 25 | uuid = { version = "1.1.2", features = ["serde", "v4"] } 26 | -------------------------------------------------------------------------------- /examples/example-axum-worker/README.md: -------------------------------------------------------------------------------- 1 | # Aide axum 2 | 3 | A minimal to-do axum cloudflare worker documented with aide. 4 | 5 | You can run it with `npm run dev`, and then visit the documentation at `http://localhost:3000`. 6 | -------------------------------------------------------------------------------- /examples/example-axum-worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-axum-worker", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev --env dev" 7 | }, 8 | "devDependencies": { 9 | "wrangler": "^3.49.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-axum-worker/src/README.md: -------------------------------------------------------------------------------- 1 | # Todo API 2 | 3 | A very simple Todo server with documentation. 4 | 5 | The purpose is to showcase the documentation workflow of Aide rather 6 | than a correct implementation. 7 | -------------------------------------------------------------------------------- /examples/example-axum-worker/src/docs.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::state::AppState; 4 | use aide::swagger::Swagger; 5 | use aide::{ 6 | axum::{ 7 | routing::{get, get_with}, 8 | ApiRouter, IntoApiResponse, 9 | }, 10 | openapi::OpenApi, 11 | redoc::Redoc, 12 | scalar::Scalar, 13 | }; 14 | use axum::{response::IntoResponse, Extension, Json}; 15 | 16 | pub fn docs_routes(state: AppState) -> ApiRouter { 17 | // We infer the return types for these routes 18 | // as an example. 19 | // 20 | // As a result, the `serve_redoc` route will 21 | // have the `text/html` content-type correctly set 22 | // with a 200 status. 23 | aide::generate::infer_responses(true); 24 | 25 | let router: ApiRouter = ApiRouter::new() 26 | .api_route_with( 27 | "/", 28 | get_with( 29 | Scalar::new("/docs/private/api.json") 30 | .with_title("Aide Axum") 31 | .axum_handler(), 32 | |op| op.description("This documentation page."), 33 | ), 34 | |p| p.security_requirement("ApiKey"), 35 | ) 36 | .api_route_with( 37 | "/redoc", 38 | get_with( 39 | Redoc::new("/docs/private/api.json") 40 | .with_title("Aide Axum") 41 | .axum_handler(), 42 | |op| op.description("This documentation page."), 43 | ), 44 | |p| p.security_requirement("ApiKey"), 45 | ) 46 | .api_route_with( 47 | "/swagger", 48 | get_with( 49 | Swagger::new("/docs/private/api.json") 50 | .with_title("Aide Axum") 51 | .axum_handler(), 52 | |op| op.description("This documentation page."), 53 | ), 54 | |p| p.security_requirement("ApiKey"), 55 | ) 56 | .route("/private/api.json", get(serve_docs)) 57 | .with_state(state); 58 | 59 | // Afterwards we disable response inference because 60 | // it might be incorrect for other routes. 61 | aide::generate::infer_responses(false); 62 | 63 | router 64 | } 65 | 66 | async fn serve_docs(Extension(api): Extension>) -> impl IntoApiResponse { 67 | Json(api).into_response() 68 | } 69 | -------------------------------------------------------------------------------- /examples/example-axum-worker/src/errors.rs: -------------------------------------------------------------------------------- 1 | use axum::{http::StatusCode, response::IntoResponse}; 2 | use schemars::JsonSchema; 3 | use serde::Serialize; 4 | use serde_json::Value; 5 | use uuid::Uuid; 6 | 7 | /// A default error response for most API errors. 8 | #[derive(Debug, Serialize, JsonSchema)] 9 | pub struct AppError { 10 | /// An error message. 11 | pub error: String, 12 | /// A unique error ID. 13 | pub error_id: Uuid, 14 | #[serde(skip)] 15 | pub status: StatusCode, 16 | /// Optional Additional error details. 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub error_details: Option, 19 | } 20 | 21 | impl AppError { 22 | pub fn new(error: &str) -> Self { 23 | Self { 24 | error: error.to_string(), 25 | error_id: Uuid::new_v4(), 26 | status: StatusCode::BAD_REQUEST, 27 | error_details: None, 28 | } 29 | } 30 | 31 | pub fn with_status(mut self, status: StatusCode) -> Self { 32 | self.status = status; 33 | self 34 | } 35 | 36 | pub fn with_details(mut self, details: Value) -> Self { 37 | self.error_details = Some(details); 38 | self 39 | } 40 | } 41 | 42 | impl IntoResponse for AppError { 43 | fn into_response(self) -> axum::response::Response { 44 | let status = self.status; 45 | let mut res = axum::Json(self).into_response(); 46 | *res.status_mut() = status; 47 | res 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/example-axum-worker/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use aide::{ 4 | axum::ApiRouter, 5 | openapi::{OpenApi, Tag}, 6 | transform::TransformOpenApi, 7 | }; 8 | use axum::{http, http::StatusCode, Extension, Json}; 9 | use docs::docs_routes; 10 | use errors::AppError; 11 | use state::AppState; 12 | use todos::routes::todo_routes; 13 | use tower_service::Service; 14 | use uuid::Uuid; 15 | use worker::{console_log, event, Context, Env, HttpRequest}; 16 | 17 | pub mod docs; 18 | pub mod errors; 19 | pub mod state; 20 | pub mod todos; 21 | 22 | #[event(start)] 23 | fn start() { 24 | console_log!("Example docs are accessible at http://127.0.0.1:3000/docs"); 25 | } 26 | 27 | #[event(fetch)] 28 | async fn fetch( 29 | req: HttpRequest, 30 | _env: Env, 31 | _ctx: Context, 32 | ) -> worker::Result> { 33 | console_error_panic_hook::set_once(); 34 | aide::generate::on_error(|error| { 35 | println!("{error}"); 36 | }); 37 | 38 | aide::generate::extract_schemas(true); 39 | 40 | let state = AppState::default(); 41 | 42 | let mut api = OpenApi::default(); 43 | 44 | let mut app = ApiRouter::new() 45 | .nest_api_service("/todo", todo_routes(state.clone())) 46 | .nest_api_service("/docs", docs_routes(state.clone())) 47 | .finish_api_with(&mut api, api_docs) 48 | .layer(Extension(Arc::new(api))) // Arc is very important here or you will face massive memory and performance issues 49 | .with_state(state); 50 | 51 | Ok(app.call(req).await?) 52 | } 53 | 54 | fn api_docs(api: TransformOpenApi) -> TransformOpenApi { 55 | api.title("Aide axum Open API") 56 | .summary("An example Todo application") 57 | .description(include_str!("README.md")) 58 | .tag(Tag { 59 | name: "todo".into(), 60 | description: Some("Todo Management".into()), 61 | ..Default::default() 62 | }) 63 | .security_scheme( 64 | "ApiKey", 65 | aide::openapi::SecurityScheme::ApiKey { 66 | location: aide::openapi::ApiKeyLocation::Header, 67 | name: "X-Auth-Key".into(), 68 | description: Some("A key that is ignored.".into()), 69 | extensions: Default::default(), 70 | }, 71 | ) 72 | .default_response_with::, _>(|res| { 73 | res.example(AppError { 74 | error: "some error happened".to_string(), 75 | error_details: None, 76 | error_id: Uuid::nil(), 77 | // This is not visible. 78 | status: StatusCode::IM_A_TEAPOT, 79 | }) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /examples/example-axum-worker/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | use uuid::Uuid; 7 | 8 | use crate::todos::TodoItem; 9 | 10 | #[derive(Debug, Clone, Default)] 11 | pub struct AppState { 12 | pub todos: Arc>>, 13 | } 14 | -------------------------------------------------------------------------------- /examples/example-axum-worker/src/todos/mod.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use uuid::Uuid; 4 | 5 | pub mod routes; 6 | 7 | /// A single Todo item. 8 | #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] 9 | pub struct TodoItem { 10 | pub id: Uuid, 11 | /// The description of the item. 12 | pub description: String, 13 | /// Whether the item was completed. 14 | pub complete: bool, 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-axum-worker/src/todos/routes.rs: -------------------------------------------------------------------------------- 1 | use aide::{ 2 | axum::{ 3 | routing::{get_with, post_with, put_with}, 4 | ApiRouter, IntoApiResponse, 5 | }, 6 | transform::TransformOperation, 7 | }; 8 | use axum::{ 9 | extract::{Path, State}, 10 | http::StatusCode, 11 | response::IntoResponse, 12 | Json, 13 | }; 14 | use schemars::JsonSchema; 15 | use serde::{Deserialize, Serialize}; 16 | use uuid::Uuid; 17 | 18 | use crate::state::AppState; 19 | 20 | use super::TodoItem; 21 | 22 | pub fn todo_routes(state: AppState) -> ApiRouter { 23 | ApiRouter::new() 24 | .api_route( 25 | "/", 26 | post_with(create_todo, create_todo_docs).get_with(list_todos, list_todos_docs), 27 | ) 28 | .api_route( 29 | "/{id}", 30 | get_with(get_todo, get_todo_docs).delete_with(delete_todo, delete_todo_docs), 31 | ) 32 | .api_route( 33 | "/{id}/complete", 34 | put_with(complete_todo, complete_todo_docs), 35 | ) 36 | .with_state(state) 37 | } 38 | 39 | /// New Todo details. 40 | #[derive(Deserialize, JsonSchema)] 41 | struct NewTodo { 42 | /// The description for the new Todo. 43 | description: String, 44 | } 45 | 46 | /// New Todo details. 47 | #[derive(Serialize, JsonSchema)] 48 | struct TodoCreated { 49 | /// The ID of the new Todo. 50 | id: Uuid, 51 | } 52 | 53 | async fn create_todo( 54 | State(app): State, 55 | Json(todo): Json, 56 | ) -> impl IntoApiResponse { 57 | let id = Uuid::new_v4(); 58 | app.todos.lock().unwrap().insert( 59 | id, 60 | TodoItem { 61 | complete: false, 62 | description: todo.description, 63 | id, 64 | }, 65 | ); 66 | 67 | (StatusCode::CREATED, Json(TodoCreated { id })) 68 | } 69 | 70 | fn create_todo_docs(op: TransformOperation) -> TransformOperation { 71 | op.description("Create a new incomplete Todo item.") 72 | .response::<201, Json>() 73 | } 74 | 75 | #[derive(Serialize, JsonSchema)] 76 | struct TodoList { 77 | todo_ids: Vec, 78 | } 79 | 80 | async fn list_todos(State(app): State) -> impl IntoApiResponse { 81 | Json(TodoList { 82 | todo_ids: app.todos.lock().unwrap().keys().copied().collect(), 83 | }) 84 | } 85 | 86 | fn list_todos_docs(op: TransformOperation) -> TransformOperation { 87 | op.description("List all Todo items.") 88 | } 89 | 90 | #[derive(Deserialize, JsonSchema)] 91 | struct SelectTodo { 92 | /// The ID of the Todo. 93 | id: Uuid, 94 | } 95 | 96 | async fn get_todo( 97 | State(app): State, 98 | Path(todo): Path, 99 | ) -> impl IntoApiResponse { 100 | if let Some(todo) = app.todos.lock().unwrap().get(&todo.id) { 101 | Json(todo.clone()).into_response() 102 | } else { 103 | StatusCode::NOT_FOUND.into_response() 104 | } 105 | } 106 | 107 | fn get_todo_docs(op: TransformOperation) -> TransformOperation { 108 | op.description("Get a single Todo item.") 109 | .response_with::<200, Json, _>(|res| { 110 | res.example(TodoItem { 111 | complete: false, 112 | description: "fix bugs".into(), 113 | id: Uuid::nil(), 114 | }) 115 | }) 116 | .response_with::<404, (), _>(|res| res.description("todo was not found")) 117 | } 118 | 119 | async fn delete_todo( 120 | State(app): State, 121 | Path(todo): Path, 122 | ) -> impl IntoApiResponse { 123 | if app.todos.lock().unwrap().remove(&todo.id).is_some() { 124 | StatusCode::NO_CONTENT 125 | } else { 126 | StatusCode::NOT_FOUND 127 | } 128 | } 129 | 130 | fn delete_todo_docs(op: TransformOperation) -> TransformOperation { 131 | op.description("Delete a Todo item.") 132 | .response_with::<204, (), _>(|res| res.description("The Todo has been deleted.")) 133 | .response_with::<404, (), _>(|res| res.description("The todo was not found")) 134 | } 135 | 136 | async fn complete_todo( 137 | State(app): State, 138 | Path(todo): Path, 139 | ) -> impl IntoApiResponse { 140 | if let Some(todo) = app.todos.lock().unwrap().get_mut(&todo.id) { 141 | todo.complete = true; 142 | StatusCode::NO_CONTENT 143 | } else { 144 | StatusCode::NOT_FOUND 145 | } 146 | } 147 | 148 | fn complete_todo_docs(op: TransformOperation) -> TransformOperation { 149 | op.description("Complete a Todo.").response::<204, ()>() 150 | } 151 | -------------------------------------------------------------------------------- /examples/example-axum-worker/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "worker-rust" 2 | main = "build/worker/shim.mjs" 3 | compatibility_date = "2024-04-08" 4 | 5 | [dev] 6 | port = 3000 7 | 8 | [env.dev] 9 | build = { command = "cargo install -q worker-build && worker-build --dev" } 10 | -------------------------------------------------------------------------------- /examples/example-axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-axum" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | aide = { path = "../../crates/aide", features = [ 9 | "redoc", 10 | "swagger", 11 | "scalar", 12 | "axum-json", 13 | "macros", 14 | ] } 15 | axum = { version = "0.8.1", features = ["macros"] } 16 | schemars = { version = "0.9.0", features = ["uuid1"] } 17 | serde = { version = "1.0.144", features = ["derive", "rc"] } 18 | serde_json = "1.0.85" 19 | tokio = { version = "1.21.0", features = ["macros", "rt-multi-thread"] } 20 | uuid = { version = "1.1.2", features = ["serde", "v4"] } 21 | -------------------------------------------------------------------------------- /examples/example-axum/README.md: -------------------------------------------------------------------------------- 1 | # Aide axum 2 | 3 | A minimal to-do axum application documented with aide. 4 | 5 | You can run it with `cargo run --bin example-axum`, and then visit the documentation at `http://localhost:3000`. 6 | -------------------------------------------------------------------------------- /examples/example-axum/src/README.md: -------------------------------------------------------------------------------- 1 | # Todo API 2 | 3 | A very simple Todo server with documentation. 4 | 5 | The purpose is to showcase the documentation workflow of Aide rather 6 | than a correct implementation. 7 | -------------------------------------------------------------------------------- /examples/example-axum/src/docs.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::state::AppState; 4 | use aide::swagger::Swagger; 5 | use aide::{ 6 | axum::{ 7 | routing::{get, get_with}, 8 | ApiRouter, IntoApiResponse, 9 | }, 10 | openapi::OpenApi, 11 | redoc::Redoc, 12 | scalar::Scalar, 13 | }; 14 | use axum::{response::IntoResponse, Extension, Json}; 15 | 16 | pub fn docs_routes(state: AppState) -> ApiRouter { 17 | // We infer the return types for these routes 18 | // as an example. 19 | // 20 | // As a result, the `serve_redoc` route will 21 | // have the `text/html` content-type correctly set 22 | // with a 200 status. 23 | aide::generate::infer_responses(true); 24 | 25 | let router: ApiRouter = ApiRouter::new() 26 | .api_route_with( 27 | "/", 28 | get_with( 29 | Scalar::new("/docs/private/api.json") 30 | .with_title("Aide Axum") 31 | .axum_handler(), 32 | |op| op.description("This documentation page."), 33 | ), 34 | |p| p.security_requirement("ApiKey"), 35 | ) 36 | .api_route_with( 37 | "/redoc", 38 | get_with( 39 | Redoc::new("/docs/private/api.json") 40 | .with_title("Aide Axum") 41 | .axum_handler(), 42 | |op| op.description("This documentation page."), 43 | ), 44 | |p| p.security_requirement("ApiKey"), 45 | ) 46 | .api_route_with( 47 | "/swagger", 48 | get_with( 49 | Swagger::new("/docs/private/api.json") 50 | .with_title("Aide Axum") 51 | .axum_handler(), 52 | |op| op.description("This documentation page."), 53 | ), 54 | |p| p.security_requirement("ApiKey"), 55 | ) 56 | .route("/private/api.json", get(serve_docs)) 57 | .with_state(state); 58 | 59 | // Afterwards we disable response inference because 60 | // it might be incorrect for other routes. 61 | aide::generate::infer_responses(false); 62 | 63 | router 64 | } 65 | 66 | async fn serve_docs(Extension(api): Extension>) -> impl IntoApiResponse { 67 | Json(api).into_response() 68 | } 69 | -------------------------------------------------------------------------------- /examples/example-axum/src/errors.rs: -------------------------------------------------------------------------------- 1 | use axum::{http::StatusCode, response::IntoResponse}; 2 | use schemars::JsonSchema; 3 | use serde::Serialize; 4 | use serde_json::Value; 5 | use uuid::Uuid; 6 | 7 | /// A default error response for most API errors. 8 | #[derive(Debug, Serialize, JsonSchema)] 9 | pub struct AppError { 10 | /// An error message. 11 | pub error: String, 12 | /// A unique error ID. 13 | pub error_id: Uuid, 14 | #[serde(skip)] 15 | pub status: StatusCode, 16 | /// Optional Additional error details. 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub error_details: Option, 19 | } 20 | 21 | impl AppError { 22 | pub fn new(error: &str) -> Self { 23 | Self { 24 | error: error.to_string(), 25 | error_id: Uuid::new_v4(), 26 | status: StatusCode::BAD_REQUEST, 27 | error_details: None, 28 | } 29 | } 30 | 31 | pub fn with_status(mut self, status: StatusCode) -> Self { 32 | self.status = status; 33 | self 34 | } 35 | 36 | pub fn with_details(mut self, details: Value) -> Self { 37 | self.error_details = Some(details); 38 | self 39 | } 40 | } 41 | 42 | impl IntoResponse for AppError { 43 | fn into_response(self) -> axum::response::Response { 44 | let status = self.status; 45 | let mut res = axum::Json(self).into_response(); 46 | *res.status_mut() = status; 47 | res 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/example-axum/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use aide::{ 4 | axum::ApiRouter, 5 | openapi::{OpenApi, Tag}, 6 | transform::TransformOpenApi, 7 | }; 8 | use axum::{http::StatusCode, Extension, Json}; 9 | use docs::docs_routes; 10 | use errors::AppError; 11 | use state::AppState; 12 | use todos::routes::todo_routes; 13 | use tokio::net::TcpListener; 14 | use uuid::Uuid; 15 | 16 | pub mod docs; 17 | pub mod errors; 18 | pub mod state; 19 | pub mod todos; 20 | 21 | #[tokio::main] 22 | async fn main() { 23 | aide::generate::on_error(|error| { 24 | println!("{error}"); 25 | }); 26 | 27 | aide::generate::extract_schemas(true); 28 | 29 | let state = AppState::default(); 30 | 31 | let mut api = OpenApi::default(); 32 | 33 | let app = ApiRouter::new() 34 | .nest_api_service("/todo", todo_routes(state.clone())) 35 | .nest_api_service("/docs", docs_routes(state.clone())) 36 | .finish_api_with(&mut api, api_docs) 37 | .layer(Extension(Arc::new(api))) // Arc is very important here or you will face massive memory and performance issues 38 | .with_state(state); 39 | 40 | println!("Example docs are accessible at http://127.0.0.1:3000/docs"); 41 | 42 | let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); 43 | 44 | axum::serve(listener, app).await.unwrap(); 45 | } 46 | 47 | fn api_docs(api: TransformOpenApi) -> TransformOpenApi { 48 | api.title("Aide axum Open API") 49 | .summary("An example Todo application") 50 | .description(include_str!("README.md")) 51 | .tag(Tag { 52 | name: "todo".into(), 53 | description: Some("Todo Management".into()), 54 | ..Default::default() 55 | }) 56 | .security_scheme( 57 | "ApiKey", 58 | aide::openapi::SecurityScheme::ApiKey { 59 | location: aide::openapi::ApiKeyLocation::Header, 60 | name: "X-Auth-Key".into(), 61 | description: Some("A key that is ignored.".into()), 62 | extensions: Default::default(), 63 | }, 64 | ) 65 | .default_response_with::, _>(|res| { 66 | res.example(AppError { 67 | error: "some error happened".to_string(), 68 | error_details: None, 69 | error_id: Uuid::nil(), 70 | // This is not visible. 71 | status: StatusCode::IM_A_TEAPOT, 72 | }) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /examples/example-axum/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | use uuid::Uuid; 7 | 8 | use crate::todos::TodoItem; 9 | 10 | #[derive(Debug, Clone, Default)] 11 | pub struct AppState { 12 | pub todos: Arc>>, 13 | } 14 | -------------------------------------------------------------------------------- /examples/example-axum/src/todos/mod.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use uuid::Uuid; 4 | 5 | pub mod routes; 6 | 7 | /// A single Todo item. 8 | #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] 9 | pub struct TodoItem { 10 | pub id: Uuid, 11 | /// The description of the item. 12 | pub description: String, 13 | /// Whether the item was completed. 14 | pub complete: bool, 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-axum/src/todos/routes.rs: -------------------------------------------------------------------------------- 1 | use aide::{ 2 | axum::{ 3 | routing::{get_with, post_with, put_with}, 4 | ApiRouter, IntoApiResponse, 5 | }, 6 | transform::TransformOperation, 7 | }; 8 | use axum::{ 9 | extract::{Path, State}, 10 | http::StatusCode, 11 | response::IntoResponse, 12 | Json, 13 | }; 14 | use schemars::JsonSchema; 15 | use serde::{Deserialize, Serialize}; 16 | use uuid::Uuid; 17 | 18 | use crate::state::AppState; 19 | 20 | use super::TodoItem; 21 | 22 | pub fn todo_routes(state: AppState) -> ApiRouter { 23 | ApiRouter::new() 24 | .api_route( 25 | "/", 26 | post_with(create_todo, create_todo_docs).get_with(list_todos, list_todos_docs), 27 | ) 28 | .api_route( 29 | "/{id}", 30 | get_with(get_todo, get_todo_docs).delete_with(delete_todo, delete_todo_docs), 31 | ) 32 | .api_route( 33 | "/{id}/complete", 34 | put_with(complete_todo, complete_todo_docs), 35 | ) 36 | .with_state(state) 37 | } 38 | 39 | /// New Todo details. 40 | #[derive(Deserialize, JsonSchema)] 41 | struct NewTodo { 42 | /// The description for the new Todo. 43 | description: String, 44 | } 45 | 46 | /// New Todo details. 47 | #[derive(Serialize, JsonSchema)] 48 | struct TodoCreated { 49 | /// The ID of the new Todo. 50 | id: Uuid, 51 | } 52 | 53 | async fn create_todo( 54 | State(app): State, 55 | Json(todo): Json, 56 | ) -> impl IntoApiResponse { 57 | let id = Uuid::new_v4(); 58 | app.todos.lock().unwrap().insert( 59 | id, 60 | TodoItem { 61 | complete: false, 62 | description: todo.description, 63 | id, 64 | }, 65 | ); 66 | 67 | (StatusCode::CREATED, Json(TodoCreated { id })) 68 | } 69 | 70 | fn create_todo_docs(op: TransformOperation) -> TransformOperation { 71 | op.description("Create a new incomplete Todo item.") 72 | .response::<201, Json>() 73 | } 74 | 75 | #[derive(Serialize, JsonSchema)] 76 | struct TodoList { 77 | todo_ids: Vec, 78 | } 79 | 80 | async fn list_todos(State(app): State) -> impl IntoApiResponse { 81 | Json(TodoList { 82 | todo_ids: app.todos.lock().unwrap().keys().copied().collect(), 83 | }) 84 | } 85 | 86 | fn list_todos_docs(op: TransformOperation) -> TransformOperation { 87 | op.description("List all Todo items.") 88 | } 89 | 90 | #[derive(Deserialize, JsonSchema)] 91 | struct SelectTodo { 92 | /// The ID of the Todo. 93 | id: Uuid, 94 | } 95 | 96 | async fn get_todo( 97 | State(app): State, 98 | Path(todo): Path, 99 | ) -> impl IntoApiResponse { 100 | if let Some(todo) = app.todos.lock().unwrap().get(&todo.id) { 101 | Json(todo.clone()).into_response() 102 | } else { 103 | StatusCode::NOT_FOUND.into_response() 104 | } 105 | } 106 | 107 | fn get_todo_docs(op: TransformOperation) -> TransformOperation { 108 | op.description("Get a single Todo item.") 109 | .response_with::<200, Json, _>(|res| { 110 | res.example(TodoItem { 111 | complete: false, 112 | description: "fix bugs".into(), 113 | id: Uuid::nil(), 114 | }) 115 | }) 116 | .response_with::<404, (), _>(|res| res.description("todo was not found")) 117 | } 118 | 119 | async fn delete_todo( 120 | State(app): State, 121 | Path(todo): Path, 122 | ) -> impl IntoApiResponse { 123 | if app.todos.lock().unwrap().remove(&todo.id).is_some() { 124 | StatusCode::NO_CONTENT 125 | } else { 126 | StatusCode::NOT_FOUND 127 | } 128 | } 129 | 130 | fn delete_todo_docs(op: TransformOperation) -> TransformOperation { 131 | op.description("Delete a Todo item.") 132 | .response_with::<204, (), _>(|res| res.description("The Todo has been deleted.")) 133 | .response_with::<404, (), _>(|res| res.description("The todo was not found")) 134 | } 135 | 136 | async fn complete_todo( 137 | State(app): State, 138 | Path(todo): Path, 139 | ) -> impl IntoApiResponse { 140 | if let Some(todo) = app.todos.lock().unwrap().get_mut(&todo.id) { 141 | todo.complete = true; 142 | StatusCode::NO_CONTENT 143 | } else { 144 | StatusCode::NOT_FOUND 145 | } 146 | } 147 | 148 | fn complete_todo_docs(op: TransformOperation) -> TransformOperation { 149 | op.description("Complete a Todo.").response::<204, ()>() 150 | } 151 | --------------------------------------------------------------------------------