├── .editorconfig
├── .github
└── workflows
│ ├── ci.yml
│ └── publish.yml
├── .gitignore
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── acceptance
├── .editorconfig
├── Cargo.lock
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── crate_segment_path
│ ├── Cargo.toml
│ └── crate_folder
│ │ ├── lib.rs
│ │ └── sub_folder
│ │ ├── mod.rs
│ │ ├── paths.rs
│ │ └── test.rs
├── folder_in_src
│ ├── Cargo.toml
│ └── crate_folder
│ │ ├── lib.rs
│ │ └── new_sub_folder
│ │ ├── mod.rs
│ │ ├── paths.rs
│ │ └── test.rs
├── generics
│ ├── Cargo.toml
│ └── src
│ │ ├── main.rs
│ │ ├── open_api.expected.json
│ │ ├── routes.rs
│ │ └── schemas.rs
├── responses
│ ├── Cargo.toml
│ └── src
│ │ ├── main.rs
│ │ ├── open_api.expected.json
│ │ ├── response.rs
│ │ └── routes.rs
├── rustfmt.toml
└── utility
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── armory.toml
├── rustfmt.toml
├── utoipauto-core
├── Cargo.toml
└── src
│ ├── attribute_utils.rs
│ ├── discover.rs
│ ├── file_utils.rs
│ ├── lib.rs
│ ├── pair.rs
│ ├── string_utils.rs
│ └── token_utils.rs
├── utoipauto-macro
├── Cargo.toml
└── src
│ └── lib.rs
└── utoipauto
├── Cargo.toml
├── src
└── lib.rs
└── tests
├── default_features
├── controllers
│ ├── controller1.rs
│ ├── controller2.rs
│ ├── controller3.rs
│ └── mod.rs
├── mod.rs
├── models.rs
└── test.rs
└── test.rs
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | indent_style = space
13 | indent_size = 4
14 | max_line_length = 120
15 |
16 | [*.md]
17 | # double whitespace at end of line
18 | # denotes a line break in Markdown
19 | trim_trailing_whitespace = false
20 |
21 | [{*.yml,*.yaml}]
22 | indent_size = 2
23 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | # push:
5 | # branches: ["main"]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | test:
14 | name: Test
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | rust: [ "1.75.0", "stable", "nightly" ]
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: install toolchain
22 | uses: dtolnay/rust-toolchain@master # Needed to always have access to the latest releases. DO NOT REPLACE WITH STABLE!
23 | with:
24 | toolchain: ${{ matrix.rust }}
25 | - name: add cargo caching
26 | uses: Swatinem/rust-cache@v2
27 | - name: Build
28 | run: cargo build --all-features --verbose
29 | - name: Run tests
30 | run: cargo test --verbose
31 | - name: Run tests with all features
32 | run: cargo test --all-features --verbose
33 |
34 | acceptance-tests:
35 | name: Acceptance Tests
36 | runs-on: ubuntu-latest
37 | # Set default working directory
38 | defaults:
39 | run:
40 | working-directory: ./acceptance
41 | strategy:
42 | matrix:
43 | rust: [ "stable", "nightly" ]
44 | steps:
45 | - uses: actions/checkout@v3
46 | - name: install toolchain
47 | uses: dtolnay/rust-toolchain@master # Needed to always have access to the latest releases. DO NOT REPLACE WITH STABLE!
48 | with:
49 | toolchain: ${{ matrix.rust }}
50 | - name: add cargo caching
51 | uses: Swatinem/rust-cache@v2
52 | - name: Run acceptance tests (build)
53 | run: cargo build
54 | - name: Run acceptance tests (tests)
55 | run: cargo test --verbose
56 |
57 | format:
58 | name: Format
59 | runs-on: ubuntu-latest
60 | steps:
61 | - uses: actions/checkout@v4
62 | - name: install toolchain
63 | uses: dtolnay/rust-toolchain@stable
64 | with:
65 | components: rustfmt
66 | - name: add cargo caching
67 | uses: Swatinem/rust-cache@v2
68 | - name: run formater
69 | run: cargo fmt --check
70 |
71 | clippy:
72 | name: Lint
73 | runs-on: ubuntu-latest
74 | steps:
75 | - uses: actions/checkout@v4
76 | - name: install toolchain
77 | uses: dtolnay/rust-toolchain@stable
78 | with:
79 | components: clippy
80 | - name: add cargo caching
81 | uses: Swatinem/rust-cache@v2
82 | - name: run formater
83 | run: cargo clippy -- -D warnings
84 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | release:
5 | types: [ published ]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | utoipauto-core:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: dtolnay/rust-toolchain@stable
13 | with:
14 | components: rustfmt, clippy
15 |
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 |
19 | - name: Get releasing version
20 | working-directory: utoipauto-core
21 | run: echo NEXT_VERSION=$(sed -nE 's/^\s*version = "(.*?)"/\1/p' Cargo.toml) >> $GITHUB_ENV
22 |
23 | - name: Check published version
24 | run: echo PREV_VERSION=$(cargo search utoipauto-core --limit 1 | sed -nE 's/^[^"]*"//; s/".*//1p' -) >> $GITHUB_ENV
25 |
26 | - name: Cargo login
27 | if: env.NEXT_VERSION != env.PREV_VERSION
28 | run: cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }}
29 |
30 | - name: Cargo package
31 | if: env.NEXT_VERSION != env.PREV_VERSION
32 | working-directory: utoipauto-core
33 | run: |
34 | echo "Releasing version: $NEXT_VERSION"
35 | echo "Published version: $PREV_VERSION"
36 | echo "Cargo Packaging..."
37 | cargo package
38 |
39 | - name: Publish utoipauto-core
40 | if: env.NEXT_VERSION != env.PREV_VERSION
41 | working-directory: utoipauto-core
42 | run: |
43 | echo "Cargo Publishing..."
44 | cargo publish --no-verify -p utoipauto-core
45 | echo "New version $NEXT_VERSION has been published"
46 |
47 | utoipauto-macro:
48 | needs: utoipauto-core
49 | runs-on: ubuntu-latest
50 | steps:
51 | - uses: dtolnay/rust-toolchain@stable
52 | with:
53 | components: rustfmt, clippy
54 |
55 | - name: Checkout
56 | uses: actions/checkout@v4
57 |
58 | - name: Get releasing version
59 | working-directory: utoipauto-macro
60 | run: echo NEXT_VERSION=$(sed -nE 's/^\s*version = "(.*?)"/\1/p' Cargo.toml) >> $GITHUB_ENV
61 |
62 | - name: Check published version
63 | run: echo PREV_VERSION=$(cargo search utoipauto-macro --limit 1 | sed -nE 's/^[^"]*"//; s/".*//1p' -) >> $GITHUB_ENV
64 |
65 | - name: Cargo login
66 | if: env.NEXT_VERSION != env.PREV_VERSION
67 | run: cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }}
68 |
69 | - name: Cargo package
70 | if: env.NEXT_VERSION != env.PREV_VERSION
71 | working-directory: utoipauto-macro
72 | run: |
73 | echo "Releasing version: $NEXT_VERSION"
74 | echo "Published version: $PREV_VERSION"
75 | echo "Cargo Packaging..."
76 | cargo package
77 |
78 | - name: Publish utoipauto-macro
79 | if: env.NEXT_VERSION != env.PREV_VERSION
80 | working-directory: utoipauto-macro
81 | run: |
82 | echo "Cargo Publishing..."
83 | cargo publish --no-verify -p utoipauto-macro
84 | echo "New version $NEXT_VERSION has been published"
85 |
86 | utoipauto:
87 | needs: utoipauto-macro
88 | runs-on: ubuntu-latest
89 | steps:
90 | - uses: dtolnay/rust-toolchain@stable
91 | with:
92 | components: rustfmt, clippy
93 |
94 | - name: Checkout
95 | uses: actions/checkout@v4
96 |
97 | - name: Get releasing version
98 | run: echo NEXT_VERSION=$(sed -nE 's/^\s*version = "(.*?)"/\1/p' Cargo.toml) >> $GITHUB_ENV
99 |
100 | - name: Check published version
101 | run: echo PREV_VERSION=$(cargo search utoipauto --limit 1 | sed -nE 's/^[^"]*"//; s/".*//1p' -) >> $GITHUB_ENV
102 |
103 | - name: Cargo login
104 | if: env.NEXT_VERSION != env.PREV_VERSION
105 | run: cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }}
106 |
107 | - name: Cargo package
108 | if: env.NEXT_VERSION != env.PREV_VERSION
109 | run: |
110 | echo "Releasing version: $NEXT_VERSION"
111 | echo "Published version: $PREV_VERSION"
112 | echo "Cargo Packaging..."
113 | cargo package
114 |
115 | - name: Publish utoipauto
116 | if: env.NEXT_VERSION != env.PREV_VERSION
117 | run: |
118 | echo "Cargo Publishing..."
119 | cargo publish --no-verify -p utoipauto
120 | echo "New version $NEXT_VERSION has been published"
121 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDE
2 | /.idea
3 |
4 | *target
5 | /Cargo.lock
6 | git_cmd.md
7 | .vscode/settings.json
8 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["utoipauto", "utoipauto-core", "utoipauto-macro"]
3 | resolver = "2"
4 |
5 | [workspace.package]
6 | authors = ["ProbablyClem", "RxDiscovery", "DenuxPlays"]
7 | version = "0.3.0-alpha.2"
8 | edition = "2021"
9 | keywords = ["utoipa", "openapi", "swagger", "path", "auto"]
10 | description = "Rust Macros to automate the addition of Paths/Schemas to Utoipa crate, simulating Reflection during the compilation phase"
11 | categories = [
12 | "parsing",
13 | "development-tools::procedural-macro-helpers",
14 | "web-programming",
15 | ]
16 | license = "MIT OR Apache-2.0"
17 | readme = "README.md"
18 | repository = "https://github.com/ProbablyClem/utoipauto"
19 | homepage = "https://github.com/ProbablyClem/utoipauto"
20 | rust-version = "1.75.0"
21 |
22 | [workspace.dependencies]
23 | # Core dependencies
24 | utoipauto-core = { path = "utoipauto-core", version = "0.3.0-alpha.2" }
25 | utoipauto-macro = { path = "utoipauto-macro", version = "0.3.0-alpha.2" }
26 |
27 | # Utoipa
28 | utoipa = { version = "5.0.0", features = ["preserve_path_order"] }
29 |
30 | # Macro dependencies
31 | quote = "1.0.36"
32 | syn = { version = "2.0.74", features = ["full"] }
33 | proc-macro2 = "1.0.86"
34 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [utoipa_paths_discovery] [RxDiscovery]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017-NOW RxDiscovery Team
2 |
3 | Permission is hereby granted, free of charge, to any
4 | person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the
6 | Software without restriction, including without
7 | limitation the rights to use, copy, modify, merge,
8 | publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software
10 | is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice
14 | shall be included in all copies or substantial portions
15 | of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 | DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Utoipauto
3 |
4 | Rust Macros to automate the addition of Paths/Schemas to Utoipa crate, simulating Reflection during the compilation phase
5 |
6 |
7 |
8 | # Crate presentation
9 |
10 | Utoipa is a great crate for generating documentation (openapi/swagger) via source code.
11 |
12 | But since Rust is a static programming language, we don't have the possibility of automatically discovering paths and
13 | dto in runtime and adding them to the documentation,
14 |
15 | For APIs with just a few endpoints, it's not that much trouble to add controller functions one by one, and DTOs one by
16 | one.
17 |
18 | But, if you have hundreds or even thousands of endpoints, the code becomes very verbose and difficult to maintain.
19 |
20 | Ex :
21 |
22 | ```rust
23 |
24 | #[derive(OpenApi)]
25 | #[openapi(
26 | paths(
27 | // <================================ All functions 1 to N
28 | test_controller::service::func_get_1,
29 | test_controller::service::func_get_2,
30 | test_controller::service::func_get_3,
31 | test_controller::service::func_get_4,
32 | ....
33 | ....
34 | ....
35 | test_controller::service::func_get_N,
36 |
37 | ),
38 | components(
39 | // <====================== All DTO one by one
40 | schemas(TestDTO_1, TestDTO_2, ........ , TestDTO_N)
41 | ),
42 | tags(
43 | (name = "todo", description = "Todo management endpoints.")
44 | ),
45 | modifiers(&SecurityAddon)
46 | )]
47 | pub struct ApiDoc;
48 |
49 | ```
50 |
51 | The goal of this crate is to propose a macro that automates the detection of methods carrying Utoipa
52 | macros (`#[utoipa::path(...]`), and adds them automatically. (it also detects sub-modules.)
53 |
54 | It also detects struct that derive or implement `ToSchema` for the `components(schemas)` section, and the `ToResponse`
55 | for the `components(responses)` section.
56 |
57 | # Features
58 |
59 | - [x] Automatic recursive path detection
60 | - [x] Automatic import from module
61 | - [x] Automatic import from src folder
62 | - [x] Automatic model detection
63 | - [x] Automatic response detection
64 | - [x] Works with workspaces
65 | - [x] Exclude a method from automatic scanning
66 | - [x] Custom path detection
67 |
68 | # How to use it
69 |
70 | Simply add the crate `utoipauto` to the project
71 |
72 | ```
73 | cargo add utoipauto
74 | ```
75 |
76 | Import macro
77 |
78 | ```rust
79 | use utoipauto::utoipauto;
80 | ```
81 |
82 | Then add the `#[utoipauto]` macro just before the #[derive(OpenApi)] and `#[openapi]` macros.
83 |
84 | ## Important !!
85 |
86 | Put `#[utoipauto]` before `#[derive(OpenApi)] `and `#[openapi]` macros.
87 |
88 | ```rust
89 | #[utoipauto(paths = "MODULE_SRC_FILE_PATH, MODULE_SRC_FILE_PATH, ...")]
90 | ```
91 |
92 | The paths receives a String which must respect this structure :
93 |
94 | `"MODULE_SRC_FILE_PATH, MODULE_SRC_FILE_PATH, ..."`
95 |
96 | You can add several paths by separating them with a coma `","`.
97 |
98 | ## Usage with workspaces
99 |
100 | If you are using a workspace, you must specify the name of the crate in the path.
101 |
102 | This applies even if you are using `#[utoipauto]` in the same crate.
103 |
104 | ```rust
105 | #[utoipauto(paths = "./utoipauto/src")]
106 | ```
107 |
108 | You can specify that the specified paths are from another crate by using the from key work.
109 |
110 | ```rust
111 | #[utoipauto(paths = "./utoipauto/src from utoipauto")]
112 | ```
113 |
114 | ### Import from src folder
115 |
116 | If no path is specified, the macro will automatically scan the `src` folder and add all the methods carrying
117 | the `#[utoipa::path(...)]` macro, and all structs deriving `ToSchema` and `ToResponse`.
118 | Here's an example of how to add all the methods contained in the src code.
119 |
120 | ```rust
121 | ...
122 |
123 | use utoipauto::utoipauto;
124 |
125 | ...
126 | #[utoipauto]
127 | #[derive(OpenApi)]
128 | #[openapi(
129 | tags(
130 | (name = "todo", description = "Todo management endpoints.")
131 | ),
132 | modifiers(&SecurityAddon)
133 | )]
134 |
135 | pub struct ApiDoc;
136 |
137 | ...
138 |
139 | ```
140 |
141 | ### Import from module
142 |
143 | Here's an example of how to add all the methods and structs contained in the rest module.
144 |
145 | ```rust
146 |
147 | use utoipauto::utoipauto;
148 |
149 | #[utoipauto(
150 | paths = "./src/rest"
151 | )]
152 | #[derive(OpenApi)]
153 | #[openapi(
154 | tags(
155 | (name = "todo", description = "Todo management endpoints.")
156 | ),
157 | modifiers(&SecurityAddon)
158 | )]
159 |
160 | pub struct ApiDoc;
161 |
162 | ```
163 |
164 | Here's an example of how to add all methods and structs contained in the rest module if it is in a sub-folder:
165 |
166 | ```rust
167 |
168 | use utoipauto::utoipauto;
169 |
170 | #[utoipauto(
171 | paths = "(./src/lib/rest from crate::rest)"
172 | )]
173 | #[derive(OpenApi)]
174 | #[openapi(
175 | tags(
176 | (name = "todo", description = "Todo management endpoints.")
177 | ),
178 | modifiers(&SecurityAddon)
179 | )]
180 |
181 | pub struct ApiDoc;
182 |
183 | ```
184 |
185 | ### Import from filename
186 |
187 | Here's an example of how to add all the methods contained in the test_controller and test2_controller modules.
188 | you can also combine automatic and manual addition, as here we've added a method manually to the documentation "
189 | other_controller::get_users", and a schema "TestDTO".
190 |
191 | ```rust
192 |
193 | use utoipauto::utoipauto;
194 |
195 | #[utoipauto(
196 | paths = "./src/rest/test_controller.rs,./src/rest/test2_controller.rs "
197 | )]
198 | #[derive(OpenApi)]
199 | #[openapi(
200 | paths(
201 |
202 | crate::rest::other_controller::get_users,
203 | ),
204 | components(
205 | schemas(TestDTO)
206 | ),
207 | tags(
208 | (name = "todo", description = "Todo management endpoints.")
209 | ),
210 | modifiers(&SecurityAddon)
211 | )]
212 |
213 | pub struct ApiDoc;
214 |
215 |
216 |
217 | ```
218 |
219 | ## Exclude a method from automatic scanning
220 |
221 | you can exclude a function from the Doc Path list by adding the following macro `#[utoipa_ignore]` .
222 |
223 | ex:
224 |
225 | ```rust
226 | /// Get all pets from database
227 | ///
228 | #[utoipa_ignore] //<============== this Macro
229 | #[utoipa::path(
230 | responses(
231 | (status = 200, description = "List all Pets", body = [ListPetsDTO])
232 | )
233 | )]
234 | #[get("/pets")]
235 | async fn get_all_pets(req: HttpRequest, store: web::Data) -> impl Responder {
236 | // your CODE
237 | }
238 |
239 | ```
240 |
241 | ## Exclude a struct from automatic scanning
242 |
243 | you can also exclude a struct from the models and reponses list by adding the following macro `#[utoipa_ignore]` .
244 |
245 | ex:
246 |
247 | ```rust
248 | #[utoipa_ignore] //<============== this Macro
249 | #[derive(ToSchema)]
250 | struct ModelToIgnore {
251 | // your CODE
252 | }
253 |
254 | ```
255 |
256 | ### Custom path detection
257 |
258 | By default, this macro will look for function with the `#[utoipa::path(...)]` attribute, but you can also specify a
259 | custom attribute to look for.
260 |
261 | ```rust
262 | #[handler]
263 | pub fn custom_router() {
264 | // ...
265 | }
266 |
267 | #[utoipauto(function_attribute_name = "handler")] //Custom attribute
268 | #[derive(OpenApi)]
269 | #[openapi(tags()))]
270 | pub struct ApiDoc;
271 |
272 | ```
273 |
274 | You can also specify custom attributes for the model and response detection.
275 |
276 | ```rust
277 | #[derive(Schema, Response)]
278 | pub struct CustomModel {
279 | // ...
280 | }
281 |
282 | #[utoipauto(schema_attribute_name = "Schema", response_attribute_name = "Response")] //Custom derive
283 | #[derive(OpenApi)]
284 | #[openapi(tags())]
285 | pub struct ApiDoc;
286 |
287 | ```
288 |
289 | ## Note
290 |
291 | Sub-modules within a module containing methods tagged with utoipa::path are also automatically detected.
292 |
293 | ## Contributing
294 |
295 | Contributions are welcomed, feel free to submit a PR or an issue.
296 |
297 | ## Inspiration
298 |
299 | Inspired by [utoipa_auto_discovery](https://github.com/rxdiscovery/utoipa_auto_discovery)
300 |
--------------------------------------------------------------------------------
/acceptance/.editorconfig:
--------------------------------------------------------------------------------
1 | ../.editorconfig
--------------------------------------------------------------------------------
/acceptance/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "equivalent"
7 | version = "1.0.1"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
10 |
11 | [[package]]
12 | name = "folder-in-src"
13 | version = "0.1.0"
14 | dependencies = [
15 | "utoipa",
16 | "utoipauto",
17 | ]
18 |
19 | [[package]]
20 | name = "generics"
21 | version = "0.1.0"
22 | dependencies = [
23 | "utility",
24 | "utoipa",
25 | "utoipauto",
26 | ]
27 |
28 | [[package]]
29 | name = "hashbrown"
30 | version = "0.15.0"
31 | source = "registry+https://github.com/rust-lang/crates.io-index"
32 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
33 |
34 | [[package]]
35 | name = "indexmap"
36 | version = "2.6.0"
37 | source = "registry+https://github.com/rust-lang/crates.io-index"
38 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
39 | dependencies = [
40 | "equivalent",
41 | "hashbrown",
42 | "serde",
43 | ]
44 |
45 | [[package]]
46 | name = "itoa"
47 | version = "1.0.11"
48 | source = "registry+https://github.com/rust-lang/crates.io-index"
49 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
50 |
51 | [[package]]
52 | name = "memchr"
53 | version = "2.7.4"
54 | source = "registry+https://github.com/rust-lang/crates.io-index"
55 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
56 |
57 | [[package]]
58 | name = "proc-macro2"
59 | version = "1.0.87"
60 | source = "registry+https://github.com/rust-lang/crates.io-index"
61 | checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
62 | dependencies = [
63 | "unicode-ident",
64 | ]
65 |
66 | [[package]]
67 | name = "quote"
68 | version = "1.0.37"
69 | source = "registry+https://github.com/rust-lang/crates.io-index"
70 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
71 | dependencies = [
72 | "proc-macro2",
73 | ]
74 |
75 | [[package]]
76 | name = "responses"
77 | version = "0.1.0"
78 | dependencies = [
79 | "serde_json",
80 | "utility",
81 | "utoipa",
82 | "utoipauto",
83 | ]
84 |
85 | [[package]]
86 | name = "ryu"
87 | version = "1.0.18"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
90 |
91 | [[package]]
92 | name = "serde"
93 | version = "1.0.210"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
96 | dependencies = [
97 | "serde_derive",
98 | ]
99 |
100 | [[package]]
101 | name = "serde_derive"
102 | version = "1.0.210"
103 | source = "registry+https://github.com/rust-lang/crates.io-index"
104 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
105 | dependencies = [
106 | "proc-macro2",
107 | "quote",
108 | "syn",
109 | ]
110 |
111 | [[package]]
112 | name = "serde_json"
113 | version = "1.0.128"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
116 | dependencies = [
117 | "itoa",
118 | "memchr",
119 | "ryu",
120 | "serde",
121 | ]
122 |
123 | [[package]]
124 | name = "syn"
125 | version = "2.0.79"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
128 | dependencies = [
129 | "proc-macro2",
130 | "quote",
131 | "unicode-ident",
132 | ]
133 |
134 | [[package]]
135 | name = "unicode-ident"
136 | version = "1.0.13"
137 | source = "registry+https://github.com/rust-lang/crates.io-index"
138 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
139 |
140 | [[package]]
141 | name = "utility"
142 | version = "0.1.0"
143 | dependencies = [
144 | "serde_json",
145 | ]
146 |
147 | [[package]]
148 | name = "utoipa"
149 | version = "5.0.0"
150 | source = "registry+https://github.com/rust-lang/crates.io-index"
151 | checksum = "5e2b34fc58a72021914a5745832024b2baa638fe771df5a35f3d1b69266bd92c"
152 | dependencies = [
153 | "indexmap",
154 | "serde",
155 | "serde_json",
156 | "utoipa-gen",
157 | ]
158 |
159 | [[package]]
160 | name = "utoipa-gen"
161 | version = "5.0.0"
162 | source = "registry+https://github.com/rust-lang/crates.io-index"
163 | checksum = "866f11b33be38a747542f435578a164674b8922d958cc065d7f19319c19d4784"
164 | dependencies = [
165 | "proc-macro2",
166 | "quote",
167 | "syn",
168 | ]
169 |
170 | [[package]]
171 | name = "utoipauto"
172 | version = "0.3.0-alpha.2"
173 | dependencies = [
174 | "utoipauto-macro",
175 | ]
176 |
177 | [[package]]
178 | name = "utoipauto-core"
179 | version = "0.3.0-alpha.2"
180 | dependencies = [
181 | "proc-macro2",
182 | "quote",
183 | "syn",
184 | ]
185 |
186 | [[package]]
187 | name = "utoipauto-lib-test"
188 | version = "0.1.0"
189 | dependencies = [
190 | "folder-in-src",
191 | "utoipa",
192 | "utoipauto",
193 | ]
194 |
195 | [[package]]
196 | name = "utoipauto-macro"
197 | version = "0.3.0-alpha.2"
198 | dependencies = [
199 | "proc-macro2",
200 | "quote",
201 | "syn",
202 | "utoipauto-core",
203 | ]
204 |
--------------------------------------------------------------------------------
/acceptance/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["crate_segment_path", "folder_in_src", "generics", "responses", "utility"]
3 | resolver = "2"
4 |
5 | [workspace.package]
6 | authors = ["ProbablyClem", "DenuxPlays", "joaquin041"]
7 | version = "0.1.0"
8 | edition = "2021"
9 | publish = false
10 | description = "A collection of crates to test utoipauto."
11 | readme = "README.md"
12 | license = "MIT OR Apache-2.0"
13 | repository = "https://github.com/ProbablyClem/utoipauto"
14 | homepage = "https://github.com/ProbablyClem/utoipauto"
15 |
16 | [workspace.dependencies]
17 | # Utoipa
18 | utoipauto = { path = "../utoipauto", version = "0.3.0-alpha.2" }
19 | utoipa = { version = "5.0.0", features = ["preserve_path_order"] }
20 |
21 | # Serde
22 | serde_json = "1.0.128"
23 |
24 | # Utilities
25 | utility = { path = "utility" }
26 |
--------------------------------------------------------------------------------
/acceptance/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/acceptance/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/acceptance/README.md:
--------------------------------------------------------------------------------
1 | # acceptance tests for utoipauto
2 |
3 | This workspace contains multiple crates that tests different aspects about the utoipauto macro.
4 | These tests should represent how the user is expected to use the library.
5 |
6 | ## MSRV
7 |
8 | This workspace does not have a minimum supported rust version.
9 | It should work with the latest stable version of rust and can use all the features that are available in the latest
10 | stable version of rust.
11 |
--------------------------------------------------------------------------------
/acceptance/crate_segment_path/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "utoipauto-lib-test"
3 | description = "Test library that tests folders in the src directory."
4 | authors.workspace = true
5 | version.workspace = true
6 | edition.workspace = true
7 | publish.workspace = true
8 | readme.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | homepage.workspace = true
12 |
13 | [lib]
14 | path = "crate_folder/lib.rs"
15 |
16 | [dependencies]
17 | utoipa.workspace = true
18 | utoipauto.workspace = true
19 | folder-in-src = { path = "../folder_in_src" }
--------------------------------------------------------------------------------
/acceptance/crate_segment_path/crate_folder/lib.rs:
--------------------------------------------------------------------------------
1 | mod sub_folder;
2 |
--------------------------------------------------------------------------------
/acceptance/crate_segment_path/crate_folder/sub_folder/mod.rs:
--------------------------------------------------------------------------------
1 | mod paths;
2 | mod test;
3 |
--------------------------------------------------------------------------------
/acceptance/crate_segment_path/crate_folder/sub_folder/paths.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)] // This code is used in the tests
2 |
3 | #[utoipa::path(post, path = "/route1")]
4 | pub fn route1() {}
5 |
6 | #[utoipa::path(post, path = "/route2")]
7 | pub fn route2() {}
8 |
--------------------------------------------------------------------------------
/acceptance/crate_segment_path/crate_folder/sub_folder/test.rs:
--------------------------------------------------------------------------------
1 | use utoipa::OpenApi;
2 | use utoipauto::utoipauto;
3 |
4 | #[utoipauto(paths = "( ./crate_segment_path/crate_folder/sub_folder/paths.rs from crate::sub_folder )")]
5 | #[derive(OpenApi)]
6 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
7 | #[allow(dead_code)]
8 | pub struct CrateInAnotherPath {}
9 |
10 | #[utoipauto(paths = "( ./folder_in_src/crate_folder/new_sub_folder/paths.rs from folder-in-src::new_sub_folder )")]
11 | #[derive(OpenApi)]
12 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
13 | #[allow(dead_code)]
14 | pub struct CrateInAnotherCrate {}
15 |
16 | #[test]
17 | fn test_crate_in_another_path() {
18 | assert_eq!(CrateInAnotherPath::openapi().paths.paths.len(), 2)
19 | }
20 |
21 | #[test]
22 | fn test_crate_in_another_crate() {
23 | assert_eq!(CrateInAnotherCrate::openapi().paths.paths.len(), 2)
24 | }
25 |
--------------------------------------------------------------------------------
/acceptance/folder_in_src/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "folder-in-src"
3 | description = "Test library that tests folders in the src directory."
4 | authors.workspace = true
5 | version.workspace = true
6 | edition.workspace = true
7 | publish.workspace = true
8 | readme.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | homepage.workspace = true
12 |
13 | [lib]
14 | path = "crate_folder/lib.rs"
15 |
16 | [dependencies]
17 | utoipa.workspace = true
18 | utoipauto.workspace = true
--------------------------------------------------------------------------------
/acceptance/folder_in_src/crate_folder/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod new_sub_folder;
2 |
--------------------------------------------------------------------------------
/acceptance/folder_in_src/crate_folder/new_sub_folder/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod paths;
2 | mod test;
3 |
--------------------------------------------------------------------------------
/acceptance/folder_in_src/crate_folder/new_sub_folder/paths.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)] // This code is used in the tests
2 |
3 | #[utoipa::path(post, path = "/route1new")]
4 | pub fn route1new() {}
5 |
6 | #[utoipa::path(post, path = "/route2new")]
7 | pub fn route2new() {}
8 |
--------------------------------------------------------------------------------
/acceptance/folder_in_src/crate_folder/new_sub_folder/test.rs:
--------------------------------------------------------------------------------
1 | use utoipa::OpenApi;
2 | use utoipauto::utoipauto;
3 |
4 | #[utoipauto(paths = "( ./folder_in_src/crate_folder/new_sub_folder/paths.rs from crate::new_sub_folder )")]
5 | #[derive(OpenApi)]
6 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
7 | #[allow(dead_code)]
8 | pub struct CrateInAnotherPath {}
9 |
10 | #[test]
11 | fn test_crate_in_another_path() {
12 | assert_eq!(CrateInAnotherPath::openapi().paths.paths.len(), 2)
13 | }
14 |
--------------------------------------------------------------------------------
/acceptance/generics/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "generics"
3 | authors.workspace = true
4 | version.workspace = true
5 | edition.workspace = true
6 | publish.workspace = true
7 | description.workspace = true
8 | readme.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | homepage.workspace = true
12 |
13 | [lints.rust]
14 | unused = "allow"
15 |
16 | [dependencies]
17 | utoipa.workspace = true
18 | utoipauto = { workspace = true }
19 |
20 | [dev-dependencies]
21 | utility.workspace = true
22 |
--------------------------------------------------------------------------------
/acceptance/generics/src/main.rs:
--------------------------------------------------------------------------------
1 | pub mod routes;
2 | pub mod schemas;
3 |
4 | use routes::*;
5 |
6 | use utoipa::OpenApi;
7 | use utoipauto::utoipauto;
8 |
9 | #[utoipauto(paths = "./generics/src")]
10 | #[derive(Debug, OpenApi)]
11 | #[openapi(info(title = "Generic Test Api"))]
12 | pub(crate) struct ApiDoc;
13 |
14 | fn main() {
15 | println!(
16 | "Our OpenApi documentation {}",
17 | ApiDoc::openapi().to_pretty_json().unwrap()
18 | );
19 | }
20 |
21 | #[cfg(test)]
22 | mod tests {
23 | use super::*;
24 | use utility::assert_json_eq;
25 |
26 | pub(crate) const EXPECTED_OPEN_API: &str = include_str!("open_api.expected.json");
27 |
28 | #[test]
29 | fn test_openapi() {
30 | let open_api = ApiDoc::openapi().to_json().unwrap();
31 |
32 | assert_json_eq(&open_api, EXPECTED_OPEN_API);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/acceptance/generics/src/open_api.expected.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "Generic Test Api",
5 | "description": "A collection of crates to test utoipauto.",
6 | "contact": {
7 | "name": "ProbablyClem"
8 | },
9 | "license": {
10 | "name": "MIT OR Apache-2.0"
11 | },
12 | "version": "0.1.0"
13 | },
14 | "paths": {
15 | "/persons": {
16 | "get": {
17 | "tags": [
18 | "crate::routes"
19 | ],
20 | "operationId": "get_persons",
21 | "responses": {
22 | "200": {
23 | "description": "A Response",
24 | "content": {
25 | "application/json": {
26 | "schema": {
27 | "$ref": "#/components/schemas/Response_Person"
28 | }
29 | }
30 | }
31 | }
32 | }
33 | }
34 | },
35 | "/nested_persons": {
36 | "get": {
37 | "tags": [
38 | "crate::routes"
39 | ],
40 | "operationId": "get_nested_persons",
41 | "responses": {
42 | "200": {
43 | "description": "A NestedResponse",
44 | "content": {
45 | "application/json": {
46 | "schema": {
47 | "$ref": "#/components/schemas/NestedResponse_Person"
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | },
55 | "/borrowed_persons": {
56 | "get": {
57 | "tags": [
58 | "crate::routes"
59 | ],
60 | "operationId": "get_borrowed_persons",
61 | "responses": {
62 | "200": {
63 | "description": "A BorrowedResponse<'static>",
64 | "content": {
65 | "application/json": {
66 | "schema": {
67 | "$ref": "#/components/schemas/BorrowedResponse"
68 | }
69 | }
70 | }
71 | }
72 | }
73 | }
74 | },
75 | "/nested_borrowed_persons": {
76 | "get": {
77 | "tags": [
78 | "crate::routes"
79 | ],
80 | "operationId": "get_nested_borrowed_persons",
81 | "responses": {
82 | "200": {
83 | "description": "A NestedBorrowedResponse<'static, Person>",
84 | "content": {
85 | "application/json": {
86 | "schema": {
87 | "$ref": "#/components/schemas/NestedBorrowedResponse_Person"
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 | },
95 | "/combined_persons": {
96 | "get": {
97 | "tags": [
98 | "crate::routes"
99 | ],
100 | "operationId": "get_combined_persons",
101 | "responses": {
102 | "200": {
103 | "description": "A CombinedResponse<'static, Person>",
104 | "content": {
105 | "application/json": {
106 | "schema": {
107 | "$ref": "#/components/schemas/CombinedResponse_Person"
108 | }
109 | }
110 | }
111 | }
112 | }
113 | }
114 | }
115 | },
116 | "components": {
117 | "schemas": {
118 | "BorrowedResponse": {
119 | "type": "object",
120 | "required": [
121 | "data",
122 | "additional"
123 | ],
124 | "properties": {
125 | "additional": {
126 | "type": "object",
127 | "additionalProperties": {
128 | "type": "integer",
129 | "format": "int32"
130 | },
131 | "propertyNames": {
132 | "type": "string"
133 | }
134 | },
135 | "data": {
136 | "type": "string"
137 | }
138 | }
139 | },
140 | "CombinedResponse_Person": {
141 | "type": "object",
142 | "required": [
143 | "nested_response",
144 | "borrowed_response"
145 | ],
146 | "properties": {
147 | "borrowed_response": {
148 | "$ref": "#/components/schemas/NestedBorrowedResponse_Person"
149 | },
150 | "nested_response": {
151 | "$ref": "#/components/schemas/NestedResponse_Person"
152 | }
153 | }
154 | },
155 | "NestedBorrowedResponse_Person": {
156 | "type": "object",
157 | "required": [
158 | "status",
159 | "data"
160 | ],
161 | "properties": {
162 | "data": {
163 | "type": "object",
164 | "required": [
165 | "name",
166 | "age"
167 | ],
168 | "properties": {
169 | "age": {
170 | "type": "integer",
171 | "format": "int32",
172 | "minimum": 0
173 | },
174 | "name": {
175 | "type": "string"
176 | }
177 | }
178 | },
179 | "status": {
180 | "type": "integer",
181 | "format": "int32",
182 | "minimum": 0
183 | }
184 | }
185 | },
186 | "NestedResponse_Person": {
187 | "type": "object",
188 | "required": [
189 | "response"
190 | ],
191 | "properties": {
192 | "response": {
193 | "$ref": "#/components/schemas/Response_Person"
194 | }
195 | }
196 | },
197 | "Person": {
198 | "type": "object",
199 | "required": [
200 | "name",
201 | "age"
202 | ],
203 | "properties": {
204 | "age": {
205 | "type": "integer",
206 | "format": "int32",
207 | "minimum": 0
208 | },
209 | "name": {
210 | "type": "string"
211 | }
212 | }
213 | },
214 | "Response_Person": {
215 | "type": "object",
216 | "required": [
217 | "status",
218 | "data"
219 | ],
220 | "properties": {
221 | "data": {
222 | "type": "object",
223 | "required": [
224 | "name",
225 | "age"
226 | ],
227 | "properties": {
228 | "age": {
229 | "type": "integer",
230 | "format": "int32",
231 | "minimum": 0
232 | },
233 | "name": {
234 | "type": "string"
235 | }
236 | }
237 | },
238 | "status": {
239 | "type": "integer",
240 | "format": "int32",
241 | "minimum": 0
242 | }
243 | }
244 | }
245 | },
246 | "responses": {
247 | "BorrowedResponse": {
248 | "description": "",
249 | "content": {
250 | "application/json": {
251 | "schema": {
252 | "type": "object",
253 | "required": [
254 | "data",
255 | "additional"
256 | ],
257 | "properties": {
258 | "additional": {
259 | "type": "object",
260 | "additionalProperties": {
261 | "type": "integer",
262 | "format": "int32"
263 | },
264 | "propertyNames": {
265 | "type": "string"
266 | }
267 | },
268 | "data": {
269 | "type": "string"
270 | }
271 | }
272 | }
273 | }
274 | }
275 | },
276 | "Person": {
277 | "description": "",
278 | "content": {
279 | "application/json": {
280 | "schema": {
281 | "type": "object",
282 | "required": [
283 | "name",
284 | "age"
285 | ],
286 | "properties": {
287 | "age": {
288 | "type": "integer",
289 | "format": "int32",
290 | "minimum": 0
291 | },
292 | "name": {
293 | "type": "string"
294 | }
295 | }
296 | }
297 | }
298 | }
299 | }
300 | }
301 | }
302 | }
--------------------------------------------------------------------------------
/acceptance/generics/src/routes.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use crate::schemas::{BorrowedResponse, CombinedResponse, NestedBorrowedResponse, NestedResponse, Person, Response};
4 |
5 | #[utoipa::path(get,
6 | path = "/persons",
7 | responses(
8 | (status = 200, description = "A Response", content_type = "application/json", body = Response),
9 | )
10 | )]
11 | pub fn get_persons() -> Response {
12 | Response {
13 | status: 200,
14 | data: Person {
15 | name: "John Doe".to_string(),
16 | age: 30,
17 | },
18 | }
19 | }
20 |
21 | #[utoipa::path(get,
22 | path = "/nested_persons",
23 | responses(
24 | (status = 200, description = "A NestedResponse", content_type = "application/json", body = NestedResponse),
25 | )
26 | )]
27 | pub fn get_nested_persons() -> NestedResponse {
28 | NestedResponse {
29 | response: Response {
30 | status: 200,
31 | data: Person {
32 | name: "John Doe".to_string(),
33 | age: 30,
34 | },
35 | },
36 | }
37 | }
38 |
39 | #[utoipa::path(get,
40 | path = "/borrowed_persons",
41 | responses(
42 | (status = 200, description = "A BorrowedResponse<'static>", content_type = "application/json", body = BorrowedResponse<'static>),
43 | )
44 | )]
45 | pub fn get_borrowed_persons() -> BorrowedResponse<'static> {
46 | let additional = HashMap::from([("first", &42), ("second", &-3)]);
47 | BorrowedResponse {
48 | data: "Test",
49 | additional,
50 | }
51 | }
52 |
53 | #[utoipa::path(get,
54 | path = "/nested_borrowed_persons",
55 | responses(
56 | (status = 200, description = "A NestedBorrowedResponse<'static, Person>", content_type = "application/json", body = NestedBorrowedResponse<'static, Person>),
57 | )
58 | )]
59 | pub fn get_nested_borrowed_persons() -> NestedBorrowedResponse<'static, Person> {
60 | let person = Box::new(Person {
61 | name: "John Doe".to_string(),
62 | age: 30,
63 | });
64 | NestedBorrowedResponse {
65 | status: 200,
66 | data: Box::leak(person),
67 | }
68 | }
69 |
70 | #[utoipa::path(get,
71 | path = "/combined_persons",
72 | responses(
73 | (status = 200, description = "A CombinedResponse<'static, Person>", content_type = "application/json", body = CombinedResponse<'static, Person>),
74 | )
75 | )]
76 | pub fn get_combined_persons() -> CombinedResponse<'static, Person> {
77 | let person = Box::new(Person {
78 | name: "John Doe".to_string(),
79 | age: 30,
80 | });
81 | let person_ref = Box::leak(person);
82 | CombinedResponse {
83 | nested_response: NestedResponse {
84 | response: Response {
85 | status: 200,
86 | data: person_ref.clone(),
87 | },
88 | },
89 | borrowed_response: NestedBorrowedResponse {
90 | status: 200,
91 | data: person_ref,
92 | },
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/acceptance/generics/src/schemas.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use utoipa::{ToResponse, ToSchema};
4 |
5 | #[derive(Debug, ToSchema, ToResponse)]
6 | pub struct Response {
7 | pub status: u16,
8 | pub data: T,
9 | }
10 |
11 | #[derive(Debug, Clone, ToSchema, ToResponse)]
12 | pub struct Person {
13 | pub name: String,
14 | pub age: u8,
15 | }
16 |
17 | // Nested Generics
18 | #[derive(Debug, ToSchema, ToResponse)]
19 | pub struct NestedResponse {
20 | pub response: Response,
21 | }
22 |
23 | // Lifetime Generics
24 | #[derive(Debug, ToSchema, ToResponse)]
25 | pub struct BorrowedResponse<'a> {
26 | pub data: &'a str,
27 | pub additional: HashMap<&'a str, &'a i32>,
28 | }
29 |
30 | // Lifetime + nested Generics
31 | #[derive(Debug, ToSchema, ToResponse)]
32 | pub struct NestedBorrowedResponse<'a, T: ToSchema> {
33 | pub status: u16,
34 | pub data: &'a T,
35 | }
36 |
37 | // Combined Generics
38 | #[derive(Debug, ToSchema, ToResponse)]
39 | pub struct CombinedResponse<'a, T: ToSchema> {
40 | pub nested_response: NestedResponse,
41 | pub borrowed_response: NestedBorrowedResponse<'a, T>,
42 | }
43 |
--------------------------------------------------------------------------------
/acceptance/responses/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "responses"
3 | authors.workspace = true
4 | version.workspace = true
5 | edition.workspace = true
6 | publish.workspace = true
7 | description.workspace = true
8 | readme.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | homepage.workspace = true
12 |
13 | [lints.rust]
14 | unused = "allow"
15 |
16 | [dependencies]
17 | # Utoipa
18 | utoipa.workspace = true
19 | utoipauto.workspace = true
20 |
21 | # Serde
22 | serde_json.workspace = true
23 |
24 | # Utility
25 | utility.workspace = true
26 |
--------------------------------------------------------------------------------
/acceptance/responses/src/main.rs:
--------------------------------------------------------------------------------
1 | pub mod response;
2 | mod routes;
3 |
4 | use utoipa::OpenApi;
5 | use utoipauto::utoipauto;
6 |
7 | #[utoipauto(paths = "./responses/src")]
8 | #[derive(Debug, OpenApi)]
9 | #[openapi(info(title = "Responses Test Api"))]
10 | pub(crate) struct ApiDoc;
11 |
12 | fn main() {
13 | println!(
14 | "Our OpenApi documentation {}",
15 | ApiDoc::openapi().to_pretty_json().unwrap()
16 | );
17 | }
18 |
19 | #[cfg(test)]
20 | mod tests {
21 | use crate::ApiDoc;
22 | use utoipa::OpenApi;
23 | use utility::assert_json_eq;
24 |
25 | pub(crate) const EXPECTED_OPEN_API: &str = include_str!("open_api.expected.json");
26 | #[test]
27 | fn test_open_api() {
28 | let open_api = ApiDoc::openapi().to_json().unwrap();
29 | let expected_value = EXPECTED_OPEN_API;
30 |
31 | assert_json_eq(&open_api, expected_value);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/acceptance/responses/src/open_api.expected.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "Responses Test Api",
5 | "description": "A collection of crates to test utoipauto.",
6 | "contact": {
7 | "name": "ProbablyClem"
8 | },
9 | "license": {
10 | "name": "MIT OR Apache-2.0"
11 | },
12 | "version": "0.1.0"
13 | },
14 | "paths": {
15 | "/api/user": {
16 | "get": {
17 | "tags": [
18 | "crate::routes"
19 | ],
20 | "operationId": "get_user",
21 | "responses": {
22 | "200": {
23 | "description": "Success response",
24 | "content": {
25 | "application/json": {
26 | "schema": {
27 | "type": "object",
28 | "description": "Success response",
29 | "required": [
30 | "value"
31 | ],
32 | "properties": {
33 | "value": {
34 | "type": "string"
35 | }
36 | }
37 | }
38 | }
39 | }
40 | },
41 | "400": {
42 | "description": "",
43 | "content": {
44 | "application/json": {
45 | "schema": {
46 | "$ref": "#/components/schemas/BadRequest"
47 | }
48 | }
49 | }
50 | },
51 | "404": {
52 | "description": ""
53 | }
54 | }
55 | }
56 | },
57 | "/api/person": {
58 | "get": {
59 | "tags": [
60 | "crate::routes"
61 | ],
62 | "operationId": "get_person",
63 | "responses": {
64 | "200": {
65 | "$ref": "#/components/responses/Person"
66 | }
67 | }
68 | }
69 | }
70 | },
71 | "components": {
72 | "schemas": {
73 | "Admin": {
74 | "type": "object",
75 | "required": [
76 | "name"
77 | ],
78 | "properties": {
79 | "name": {
80 | "type": "string"
81 | }
82 | }
83 | },
84 | "Admin2": {
85 | "type": "object",
86 | "required": [
87 | "name",
88 | "id"
89 | ],
90 | "properties": {
91 | "id": {
92 | "type": "integer",
93 | "format": "int32"
94 | },
95 | "name": {
96 | "type": "string"
97 | }
98 | }
99 | },
100 | "BadRequest": {
101 | "type": "object",
102 | "required": [
103 | "message"
104 | ],
105 | "properties": {
106 | "message": {
107 | "type": "string"
108 | }
109 | }
110 | }
111 | },
112 | "responses": {
113 | "Person": {
114 | "description": "",
115 | "content": {
116 | "application/vnd-custom-v1+json": {
117 | "schema": {
118 | "$ref": "#/components/schemas/Admin"
119 | },
120 | "examples": {
121 | "Person1": {
122 | "value": {
123 | "name": "name1"
124 | }
125 | },
126 | "Person2": {
127 | "value": {
128 | "name": "name2"
129 | }
130 | }
131 | }
132 | },
133 | "application/vnd-custom-v2+json": {
134 | "schema": {
135 | "type": "object",
136 | "required": [
137 | "name",
138 | "id"
139 | ],
140 | "properties": {
141 | "id": {
142 | "type": "integer",
143 | "format": "int32"
144 | },
145 | "name": {
146 | "type": "string"
147 | }
148 | }
149 | },
150 | "example": {
151 | "id": 1,
152 | "name": "name3"
153 | }
154 | }
155 | }
156 | }
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/acceptance/responses/src/response.rs:
--------------------------------------------------------------------------------
1 | #[derive(utoipa::ToSchema)]
2 | pub struct Admin {
3 | pub name: String,
4 | }
5 | #[derive(utoipa::ToSchema)]
6 | pub struct Admin2 {
7 | pub name: String,
8 | pub id: i32,
9 | }
10 |
11 | #[derive(utoipa::ToResponse)]
12 | pub enum Person {
13 | #[response(examples(
14 | ("Person1" = (value = json!({"name": "name1"}))),
15 | ("Person2" = (value = json!({"name": "name2"})))
16 | ))]
17 | Admin(#[content("application/vnd-custom-v1+json")] Admin),
18 |
19 | #[response(example = json!({"name": "name3", "id": 1}))]
20 | Admin2(
21 | #[content("application/vnd-custom-v2+json")]
22 | #[to_schema]
23 | Admin2,
24 | ),
25 | }
26 |
27 | #[derive(utoipa::ToSchema)]
28 | pub struct BadRequest {
29 | message: String,
30 | }
31 |
32 | #[derive(utoipa::IntoResponses)]
33 | pub enum UserResponses {
34 | /// Success response
35 | #[response(status = 200)]
36 | Success { value: String },
37 |
38 | #[response(status = 404)]
39 | NotFound,
40 |
41 | #[response(status = 400)]
42 | BadRequest(BadRequest),
43 | }
44 |
--------------------------------------------------------------------------------
/acceptance/responses/src/routes.rs:
--------------------------------------------------------------------------------
1 | use crate::response::{Admin, Person, UserResponses};
2 |
3 | #[utoipa::path(get, path = "/api/user", responses(UserResponses))]
4 | fn get_user() -> UserResponses {
5 | UserResponses::NotFound
6 | }
7 |
8 | #[utoipa::path(
9 | get,
10 | path = "/api/person",
11 | responses(
12 | (status = 200, response = Person)
13 | )
14 | )]
15 | fn get_person() -> Person {
16 | Person::Admin(Admin {
17 | name: "name1".to_string(),
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/acceptance/rustfmt.toml:
--------------------------------------------------------------------------------
1 | ../rustfmt.toml
--------------------------------------------------------------------------------
/acceptance/utility/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "utility"
3 | description = "Utility functions for the acceptance tests"
4 | authors.workspace = true
5 | version.workspace = true
6 | edition.workspace = true
7 | publish.workspace = true
8 | readme.workspace = true
9 | license.workspace = true
10 | repository.workspace = true
11 | homepage.workspace = true
12 |
13 | [dependencies]
14 | serde_json.workspace = true
15 |
--------------------------------------------------------------------------------
/acceptance/utility/src/lib.rs:
--------------------------------------------------------------------------------
1 | use serde_json::Value;
2 |
3 | pub fn assert_json_eq(actual: &str, expected: &str) {
4 | let actual_value: Value = serde_json::from_str(actual).expect("Invalid JSON in actual");
5 | let expected_value: Value = serde_json::from_str(expected).expect("Invalid JSON in expected");
6 |
7 | assert_eq!(actual_value, expected_value, "JSON objects are not equal");
8 | }
9 |
--------------------------------------------------------------------------------
/armory.toml:
--------------------------------------------------------------------------------
1 | version = "0.1.0"
2 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | edition = "2021"
2 | max_width = 120
3 | newline_style = "Unix"
4 | hard_tabs = false
5 | tab_spaces = 4
6 | #group_imports = "StdExternalCrate" # currently a nightly feature
--------------------------------------------------------------------------------
/utoipauto-core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "utoipauto-core"
3 | authors.workspace = true
4 | version.workspace = true
5 | edition.workspace = true
6 | keywords.workspace = true
7 | description.workspace = true
8 | categories.workspace = true
9 | license.workspace = true
10 | readme.workspace = true
11 | repository.workspace = true
12 | homepage.workspace = true
13 | rust-version.workspace = true
14 |
15 | [dependencies]
16 | quote.workspace = true
17 | syn.workspace = true
18 | proc-macro2.workspace = true
19 |
20 | [dev-dependencies]
21 | utoipa.workspace = true
22 |
--------------------------------------------------------------------------------
/utoipauto-core/src/attribute_utils.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::TokenStream;
2 | use syn::{punctuated::Punctuated, Attribute, Meta, Token};
3 |
4 | pub fn update_openapi_macro_attributes(
5 | macro_attibutes: &mut Vec,
6 | uto_paths: &TokenStream,
7 | uto_models: &TokenStream,
8 | uto_responses: &TokenStream,
9 | ) {
10 | let mut is_ok = false;
11 | for attr in macro_attibutes {
12 | if !attr.path().is_ident("openapi") {
13 | continue;
14 | }
15 | is_ok = true;
16 | match &attr.meta {
17 | // #[openapi]
18 | Meta::Path(_path) => {
19 | *attr = build_new_openapi_attributes(Punctuated::new(), uto_paths, uto_models, uto_responses);
20 | }
21 | // #[openapi()] or #[openapi(attribute(...))]
22 | Meta::List(meta_list) => {
23 | let nested = meta_list
24 | .parse_args_with(Punctuated::::parse_terminated)
25 | .expect("Expected a list of attributes inside #[openapi(...)]!");
26 | *attr = build_new_openapi_attributes(nested, uto_paths, uto_models, uto_responses);
27 | }
28 | // This would be #[openapi = "foo"], which is not valid
29 | Meta::NameValue(_) => panic!("Expected #[openapi(...)], but found #[openapi = value]!"),
30 | }
31 | }
32 | if !is_ok {
33 | panic!("No utoipa::openapi Macro found !");
34 | }
35 | }
36 |
37 | /// Build the new openapi macro attribute with the newly discovered paths
38 | pub fn build_new_openapi_attributes(
39 | nested_attributes: Punctuated,
40 | uto_paths: &TokenStream,
41 | uto_models: &TokenStream,
42 | uto_responses: &TokenStream,
43 | ) -> Attribute {
44 | let paths = extract_paths(&nested_attributes);
45 | let schemas = extract_components(&nested_attributes, "schemas");
46 | let responses = extract_components(&nested_attributes, "responses");
47 | let remaining_nested_attributes = remove_paths_and_components(nested_attributes);
48 |
49 | let uto_paths = match uto_paths.is_empty() {
50 | true => TokenStream::new(),
51 | false => quote::quote!(#uto_paths,),
52 | };
53 | let uto_models = match uto_models.is_empty() {
54 | true => TokenStream::new(),
55 | false => quote::quote!(#uto_models,),
56 | };
57 | let uto_responses = match uto_responses.is_empty() {
58 | true => TokenStream::new(),
59 | false => quote::quote!(#uto_responses,),
60 | };
61 | let uto_macro = quote::quote!(
62 | paths(#uto_paths #paths),components(schemas(#uto_models #schemas),responses(#uto_responses #responses)),
63 | #remaining_nested_attributes
64 | );
65 |
66 | syn::parse_quote! { #[openapi( #uto_macro )] }
67 | }
68 |
69 | fn remove_paths_and_components(nested_attributes: Punctuated) -> TokenStream {
70 | let mut remaining = Vec::new();
71 | for meta in nested_attributes {
72 | match meta {
73 | Meta::List(list) if list.path.is_ident("paths") => (),
74 | Meta::List(list) if list.path.is_ident("components") => (),
75 | // These should be handled by removing `components`, this is just in case they occur outside of `components` for some reason.
76 | Meta::List(list) if list.path.is_ident("schemas") => (),
77 | Meta::List(list) if list.path.is_ident("responses") => (),
78 | _ => remaining.push(meta),
79 | }
80 | }
81 | quote::quote!( #(#remaining),* )
82 | }
83 |
84 | fn extract_paths(nested_attributes: &Punctuated) -> TokenStream {
85 | nested_attributes
86 | .iter()
87 | .find_map(|meta| {
88 | let Meta::List(list) = meta else { return None };
89 | list.path.is_ident("paths").then(|| list.tokens.clone())
90 | })
91 | .unwrap_or_else(TokenStream::new)
92 | }
93 |
94 | fn extract_components(nested_attributes: &Punctuated, component_kind: &str) -> TokenStream {
95 | nested_attributes
96 | .iter()
97 | .find_map(|meta| {
98 | let Meta::List(list) = meta else { return None };
99 | if !list.path.is_ident("components") {
100 | return None;
101 | }
102 |
103 | let nested = list
104 | .parse_args_with(Punctuated::::parse_terminated)
105 | .expect("Expected a list of attributes inside components(...)!");
106 |
107 | nested.iter().find_map(|meta| {
108 | let Meta::List(list) = meta else { return None };
109 | list.path.is_ident(component_kind).then(|| list.tokens.clone())
110 | })
111 | })
112 | .unwrap_or_else(TokenStream::new)
113 | }
114 |
115 | #[cfg(test)]
116 | mod test {
117 | use proc_macro2::TokenStream;
118 | use quote::ToTokens;
119 | use syn::punctuated::Punctuated;
120 |
121 | #[test]
122 | fn test_extract_paths() {
123 | assert_eq!(
124 | super::extract_paths(&syn::parse_quote!(paths(p1))).to_string(),
125 | "p1".to_string()
126 | );
127 | }
128 |
129 | #[test]
130 | fn test_extract_paths_empty() {
131 | assert_eq!(super::extract_paths(&Punctuated::new()).to_string(), "".to_string());
132 | }
133 |
134 | #[test]
135 | fn test_build_new_openapi_attributes() {
136 | assert_eq!(
137 | super::build_new_openapi_attributes(
138 | Punctuated::new(),
139 | "e::quote!(crate::api::test),
140 | &TokenStream::new(),
141 | &TokenStream::new(),
142 | )
143 | .to_token_stream()
144 | .to_string()
145 | .replace(' ', ""),
146 | "#[openapi(paths(crate::api::test,),components(schemas(),responses()),)]".to_string()
147 | );
148 | }
149 |
150 | #[test]
151 | fn test_build_new_openapi_attributes_path_replace() {
152 | assert_eq!(
153 | super::build_new_openapi_attributes(
154 | syn::parse_quote!(paths(p1)),
155 | "e::quote!(crate::api::test),
156 | &TokenStream::new(),
157 | &TokenStream::new(),
158 | )
159 | .to_token_stream()
160 | .to_string()
161 | .replace(' ', ""),
162 | "#[openapi(paths(crate::api::test,p1),components(schemas(),responses()),)]".to_string()
163 | );
164 | }
165 |
166 | #[test]
167 | fn test_build_new_openapi_attributes_components() {
168 | assert_eq!(
169 | super::build_new_openapi_attributes(
170 | syn::parse_quote!(paths(p1)),
171 | "e::quote!(crate::api::test),
172 | "e::quote!(model),
173 | &TokenStream::new(),
174 | )
175 | .to_token_stream()
176 | .to_string()
177 | .replace(' ', ""),
178 | "#[openapi(paths(crate::api::test,p1),components(schemas(model,),responses()),)]".to_string()
179 | );
180 | }
181 |
182 | #[test]
183 | fn test_build_new_openapi_attributes_components_schemas_replace() {
184 | assert_eq!(
185 | super::build_new_openapi_attributes(
186 | syn::parse_quote!(paths(p1), components(schemas(m1))),
187 | "e::quote!(crate::api::test),
188 | "e::quote!(model),
189 | &TokenStream::new(),
190 | )
191 | .to_token_stream()
192 | .to_string()
193 | .replace(' ', ""),
194 | "#[openapi(paths(crate::api::test,p1),components(schemas(model,m1),responses()),)]".to_string()
195 | );
196 | }
197 |
198 | #[test]
199 | fn test_build_new_openapi_attributes_components_responses_replace() {
200 | assert_eq!(
201 | super::build_new_openapi_attributes(
202 | syn::parse_quote!(paths(p1), components(responses(r1))),
203 | "e::quote!(crate::api::test),
204 | &TokenStream::new(),
205 | "e::quote!(response),
206 | )
207 | .to_token_stream()
208 | .to_string()
209 | .replace(' ', ""),
210 | "#[openapi(paths(crate::api::test,p1),components(schemas(),responses(response,r1)),)]".to_string()
211 | );
212 | }
213 |
214 | #[test]
215 | fn test_build_new_openapi_attributes_components_responses_schemas_replace() {
216 | assert_eq!(
217 | super::build_new_openapi_attributes(
218 | syn::parse_quote!(paths(p1), components(responses(r1), schemas(m1))),
219 | "e::quote!(crate::api::test),
220 | "e::quote!(model),
221 | "e::quote!(response),
222 | )
223 | .to_token_stream()
224 | .to_string()
225 | .replace(' ', ""),
226 | "#[openapi(paths(crate::api::test,p1),components(schemas(model,m1),responses(response,r1)),)]".to_string()
227 | );
228 | }
229 |
230 | #[test]
231 | fn test_build_new_openapi_attributes_components_responses_schemas() {
232 | assert_eq!(
233 | super::build_new_openapi_attributes(
234 | syn::parse_quote!(paths(p1), components(responses(r1), schemas(m1))),
235 | "e::quote!(crate::api::test),
236 | &TokenStream::new(),
237 | "e::quote!(response),
238 | )
239 | .to_token_stream()
240 | .to_string()
241 | .replace(' ', ""),
242 | "#[openapi(paths(crate::api::test,p1),components(schemas(m1),responses(response,r1)),)]".to_string()
243 | );
244 | }
245 |
246 | #[test]
247 | fn test_build_new_openapi_attributes_components_schemas_reponses() {
248 | assert_eq!(
249 | super::build_new_openapi_attributes(
250 | syn::parse_quote!(paths(p1), components(schemas(m1), responses(r1))),
251 | "e::quote!(crate::api::test),
252 | "e::quote!(model),
253 | &TokenStream::new(),
254 | )
255 | .to_token_stream()
256 | .to_string()
257 | .replace(' ', ""),
258 | "#[openapi(paths(crate::api::test,p1),components(schemas(model,m1),responses(r1)),)]".to_string()
259 | );
260 | }
261 |
262 | #[test]
263 | fn test_update_openapi_attributes_empty() {
264 | let mut attrs = vec![syn::parse_quote!(#[openapi])];
265 | super::update_openapi_macro_attributes(
266 | &mut attrs,
267 | "e::quote!(crate::api::test),
268 | "e::quote!(model),
269 | &TokenStream::new(),
270 | );
271 | assert_eq!(
272 | attrs[0].to_token_stream().to_string().replace(' ', ""),
273 | "#[openapi(paths(crate::api::test,),components(schemas(model,),responses()),)]".to_string()
274 | );
275 | }
276 |
277 | #[test]
278 | fn test_update_openapi_attributes_components_schemas_reponses() {
279 | let mut attrs = vec![syn::parse_quote!(#[openapi(paths(p1), components(schemas(m1), responses(r1)))])];
280 | super::update_openapi_macro_attributes(
281 | &mut attrs,
282 | "e::quote!(crate::api::test),
283 | "e::quote!(model),
284 | &TokenStream::new(),
285 | );
286 | assert_eq!(
287 | attrs[0].to_token_stream().to_string().replace(' ', ""),
288 | "#[openapi(paths(crate::api::test,p1),components(schemas(model,m1),responses(r1)),)]".to_string()
289 | );
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/utoipauto-core/src/discover.rs:
--------------------------------------------------------------------------------
1 | use std::vec;
2 |
3 | use crate::file_utils::{extract_module_name_from_path, parse_files};
4 | use crate::token_utils::Parameters;
5 | use quote::ToTokens;
6 | use syn::token::Comma;
7 | use syn::Ident;
8 | use syn::{punctuated::Punctuated, Attribute, GenericParam, Item, ItemFn, ItemImpl, Meta, Token};
9 |
10 | /// Discover everything from a file, will explore folder recursively
11 | pub fn discover_from_file(
12 | src_path: String,
13 | crate_name: String,
14 | params: &Parameters,
15 | ) -> (Vec, Vec, Vec) {
16 | let files = parse_files(&src_path).unwrap_or_else(|_| panic!("Failed to parse file {}", src_path));
17 |
18 | files
19 | .into_iter()
20 | .map(|e| parse_module_items(extract_module_name_from_path(&e.0, &crate_name), e.1.items, params))
21 | .fold(Vec::::new(), |mut acc, mut v| {
22 | acc.append(&mut v);
23 | acc
24 | })
25 | .into_iter()
26 | .fold(
27 | (
28 | Vec::::new(),
29 | Vec::::new(),
30 | Vec::::new(),
31 | ),
32 | |mut acc, v| {
33 | match v {
34 | DiscoverType::Fn(n) => acc.0.push(n),
35 | DiscoverType::Model(n) => acc.1.push(n),
36 | DiscoverType::Response(n) => acc.2.push(n),
37 | DiscoverType::CustomModelImpl(n) => acc.1.push(n),
38 | DiscoverType::CustomResponseImpl(n) => acc.2.push(n),
39 | };
40 |
41 | acc
42 | },
43 | )
44 | }
45 |
46 | #[allow(unused)]
47 | enum DiscoverType {
48 | Fn(syn::Path),
49 | Model(syn::Path),
50 | Response(syn::Path),
51 | CustomModelImpl(syn::Path),
52 | CustomResponseImpl(syn::Path),
53 | }
54 |
55 | fn parse_module_items(module_path: syn::Path, items: Vec- , params: &Parameters) -> Vec {
56 | items
57 | .into_iter()
58 | .filter(|e| {
59 | matches!(
60 | e,
61 | Item::Mod(_) | Item::Fn(_) | Item::Struct(_) | Item::Enum(_) | Item::Impl(_)
62 | )
63 | })
64 | .map(|v| match v {
65 | Item::Mod(m) => m.content.map_or(Vec::::new(), |cs| {
66 | parse_module_items(build_path(&module_path, &m.ident), cs.1, params)
67 | }),
68 | Item::Fn(f) => parse_function(&f, ¶ms.fn_attribute_name)
69 | .into_iter()
70 | .map(|item| DiscoverType::Fn(build_path(&module_path, &item)))
71 | .collect(),
72 | Item::Struct(s) => parse_from_attr(&s.attrs, build_path(&module_path, &s.ident), s.generics.params, params),
73 | Item::Enum(e) => parse_from_attr(&e.attrs, build_path(&module_path, &e.ident), e.generics.params, params),
74 | Item::Impl(im) => parse_from_impl(&im, &module_path, params),
75 | _ => vec![],
76 | })
77 | .fold(Vec::::new(), |mut acc, mut v| {
78 | acc.append(&mut v);
79 | acc
80 | })
81 | }
82 |
83 | /// Search for ToSchema and ToResponse implementations in attr
84 | fn parse_from_attr(
85 | a: &Vec,
86 | name: syn::Path,
87 | generic_params: Punctuated,
88 | params: &Parameters,
89 | ) -> Vec {
90 | let mut out: Vec = vec![];
91 | if !generic_params.iter().all(|p| matches!(p, GenericParam::Lifetime(_))) {
92 | return out;
93 | }
94 |
95 | for attr in a {
96 | let meta = &attr.meta;
97 | if meta.path().is_ident("utoipa_ignore") {
98 | return vec![];
99 | }
100 | if meta.path().is_ident("derive") {
101 | let nested = attr
102 | .parse_args_with(Punctuated::::parse_terminated)
103 | .expect("Failed to parse derive attribute");
104 | for nested_meta in nested {
105 | if nested_meta.path().segments.len() == 2 && nested_meta.path().segments[0].ident == "utoipa" {
106 | match nested_meta.path().segments[1].ident.to_string().as_str() {
107 | "ToSchema" => out.push(DiscoverType::Model(name.clone())),
108 | "ToResponse" => out.push(DiscoverType::Response(name.clone())),
109 | _ => {}
110 | }
111 | } else {
112 | if nested_meta.path().is_ident(¶ms.schema_attribute_name) {
113 | out.push(DiscoverType::Model(name.clone()));
114 | }
115 | if nested_meta.path().is_ident(¶ms.response_attribute_name) {
116 | out.push(DiscoverType::Response(name.clone()));
117 | }
118 | }
119 | }
120 | }
121 | }
122 |
123 | out
124 | }
125 |
126 | fn parse_from_impl(im: &ItemImpl, module_base_path: &syn::Path, params: &Parameters) -> Vec {
127 | im.trait_
128 | .as_ref()
129 | .and_then(|trt| trt.1.segments.last().map(|p| p.ident.to_string()))
130 | .and_then(|impl_name| {
131 | if impl_name.eq(params.schema_attribute_name.as_str()) {
132 | Some(vec![DiscoverType::CustomModelImpl(build_path(
133 | module_base_path,
134 | &im.self_ty,
135 | ))])
136 | } else if impl_name.eq(params.response_attribute_name.as_str()) {
137 | Some(vec![DiscoverType::CustomResponseImpl(build_path(
138 | module_base_path,
139 | &im.self_ty,
140 | ))])
141 | } else {
142 | None
143 | }
144 | })
145 | .unwrap_or_default()
146 | }
147 |
148 | fn parse_function(f: &ItemFn, fn_attributes_name: &str) -> Vec {
149 | let mut fns_name: Vec = vec![];
150 | if should_parse_fn(f) {
151 | for i in 0..f.attrs.len() {
152 | if f.attrs[i]
153 | .meta
154 | .path()
155 | .segments
156 | .iter()
157 | .any(|item| item.ident.eq(fn_attributes_name))
158 | {
159 | fns_name.push(f.sig.ident.clone());
160 | }
161 | }
162 | }
163 | fns_name
164 | }
165 |
166 | fn should_parse_fn(f: &ItemFn) -> bool {
167 | !f.attrs.is_empty() && !is_ignored(f)
168 | }
169 |
170 | fn is_ignored(f: &ItemFn) -> bool {
171 | f.attrs.iter().any(|attr| {
172 | if let Some(name) = attr.path().get_ident() {
173 | name.eq("utoipa_ignore")
174 | } else {
175 | false
176 | }
177 | })
178 | }
179 |
180 | fn build_path(file_path: &syn::Path, fn_name: impl ToTokens) -> syn::Path {
181 | syn::parse_quote!(#file_path::#fn_name)
182 | }
183 |
184 | #[cfg(test)]
185 | mod test {
186 | use quote::quote;
187 | use syn::ItemFn;
188 |
189 | #[test]
190 | fn test_parse_function() {
191 | let quoted = quote! {
192 | #[utoipa]
193 | pub fn route_custom() {}
194 | };
195 |
196 | let item_fn: ItemFn = syn::parse2(quoted).unwrap();
197 | let fn_name = super::parse_function(&item_fn, "utoipa");
198 | assert_eq!(fn_name, vec!["route_custom"]);
199 |
200 | let quoted = quote! {
201 | #[handler]
202 | pub fn route_custom() {}
203 | };
204 |
205 | let item_fn: ItemFn = syn::parse2(quoted).unwrap();
206 | let fn_name = super::parse_function(&item_fn, "handler");
207 | assert_eq!(fn_name, vec!["route_custom"]);
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/utoipauto-core/src/file_utils.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::{self, File},
3 | io::{self, Read},
4 | iter,
5 | path::{Path, PathBuf},
6 | };
7 |
8 | use proc_macro2::Span;
9 |
10 | pub fn parse_file>(filepath: T) -> Result {
11 | let pb: PathBuf = filepath.into();
12 |
13 | if !pb.is_file() {
14 | panic!("File not found: {:?}", pb);
15 | }
16 |
17 | let mut file = File::open(&pb)?;
18 | let mut content = String::new();
19 | file.read_to_string(&mut content)?;
20 |
21 | Ok(syn::parse_file(&content).unwrap_or_else(move |_| panic!("Failed to parse file {:?}", pb)))
22 | }
23 |
24 | /// Parse all the files in the given path
25 | pub fn parse_files>(path: T) -> Result, io::Error> {
26 | let mut files: Vec<(String, syn::File)> = vec![];
27 |
28 | let pb: PathBuf = path.into();
29 | if pb.is_file() {
30 | // we only parse rust files
31 | if is_rust_file(&pb) {
32 | files.push((pb.to_str().unwrap().to_string(), parse_file(pb)?));
33 | }
34 | } else {
35 | for entry in fs::read_dir(pb)? {
36 | let entry = entry?;
37 | let path = entry.path();
38 | if path.is_file() && is_rust_file(&path) {
39 | files.push((path.to_str().unwrap().to_string(), parse_file(path)?));
40 | } else {
41 | files.append(&mut parse_files(path)?);
42 | }
43 | }
44 | }
45 | Ok(files)
46 | }
47 |
48 | fn is_rust_file(path: &Path) -> bool {
49 | path.is_file()
50 | && match path.extension() {
51 | Some(ext) => match ext.to_str() {
52 | Some(ext) => ext.eq("rs"),
53 | None => false,
54 | },
55 | None => false,
56 | }
57 | }
58 |
59 | /// Extract the module name from the file path
60 | /// # Example
61 | /// ```
62 | /// # use quote::ToTokens as _;
63 | /// use utoipauto_core::file_utils::extract_module_name_from_path;
64 | /// let module_name = extract_module_name_from_path(
65 | /// &"./utoipa-auto-macro/tests/controllers/controller1.rs".to_string(),
66 | /// "crate"
67 | /// );
68 | /// assert_eq!(
69 | /// module_name.to_token_stream().to_string().replace(' ', ""),
70 | /// "crate::controllers::controller1".to_string()
71 | /// );
72 | /// ```
73 | pub fn extract_module_name_from_path(path: &str, crate_name: &str) -> syn::Path {
74 | let path = path.replace('\\', "/");
75 | let path = path
76 | .trim_end_matches(".rs")
77 | .trim_end_matches("/mod")
78 | .trim_end_matches("/lib")
79 | .trim_end_matches("/main")
80 | .trim_start_matches("./");
81 | let segments: Vec<_> = path.split('/').collect();
82 |
83 | // In general, paths will look like `./src/my/module`, which should turn into `crate::my::module`.
84 | // When using cargo workspaces, paths may look like `./subcrate/src/my/module`,
85 | // `./crates/subcrate/src/my/module`, etc., so we need to remove anything up to `src`
86 | // (or `tests`) to still produce `crate::my::module`.
87 | // So we split the segments by the last occurrence of `src` or `tests` and take the last part.
88 | let segments_inside_crate = find_segment_and_skip(&segments, &["src", "tests"], 1);
89 |
90 | // Also skip fragments that are already out of the crate name. For example,
91 | // `./src/lib/my/module/name from crate::my::module` should turn into `crate::my::module:name`,
92 | // and not into `crate::lib::my::module::name`.
93 | let crate_name = crate_name.replace("-", "_");
94 | let mut crate_segments = crate_name.split("::");
95 | let first_crate_fragment = crate_segments.next().expect("Crate should not be empty");
96 | let segments_inside_crate = match crate_segments.next() {
97 | Some(crate_fragment) => find_segment_and_skip(segments_inside_crate, &[crate_fragment], 0),
98 | None => segments_inside_crate,
99 | };
100 |
101 | let full_crate_path = iter::once(first_crate_fragment)
102 | .chain(segments_inside_crate.iter().copied())
103 | .map(|segment| syn::PathSegment::from(syn::Ident::new(&segment.replace('-', "_"), Span::mixed_site())));
104 | syn::Path {
105 | leading_colon: None,
106 | segments: full_crate_path.collect(),
107 | }
108 | }
109 |
110 | fn find_segment_and_skip<'a>(segments: &'a [&str], to_find: &[&str], to_skip: usize) -> &'a [&'a str] {
111 | match segments.iter().rposition(|segment| to_find.contains(segment)) {
112 | Some(idx) => &segments[(idx + to_skip)..],
113 | None => segments,
114 | }
115 | }
116 |
117 | #[cfg(test)]
118 | mod tests {
119 | use quote::ToTokens;
120 |
121 | use super::*;
122 |
123 | #[test]
124 | fn test_extract_module_name_from_path() {
125 | assert_eq!(
126 | extract_module_name_from_path("./utoipa-auto-macro/tests/controllers/controller1.rs", "crate")
127 | .to_token_stream()
128 | .to_string()
129 | .replace(" ", ""),
130 | "crate::controllers::controller1"
131 | );
132 | }
133 |
134 | #[test]
135 | fn test_extract_module_name_from_path_windows() {
136 | assert_eq!(
137 | extract_module_name_from_path(".\\utoipa-auto-macro\\tests\\controllers\\controller1.rs", "crate")
138 | .to_token_stream()
139 | .to_string()
140 | .replace(" ", ""),
141 | "crate::controllers::controller1"
142 | );
143 | }
144 |
145 | #[test]
146 | fn test_extract_module_name_from_mod() {
147 | assert_eq!(
148 | extract_module_name_from_path("./utoipa-auto-macro/tests/controllers/mod.rs", "crate")
149 | .to_token_stream()
150 | .to_string()
151 | .replace(" ", ""),
152 | "crate::controllers"
153 | );
154 | }
155 |
156 | #[test]
157 | fn test_extract_module_name_from_lib() {
158 | assert_eq!(
159 | extract_module_name_from_path("./src/lib.rs", "crate")
160 | .to_token_stream()
161 | .to_string()
162 | .replace(" ", ""),
163 | "crate"
164 | );
165 | }
166 |
167 | #[test]
168 | fn test_extract_module_name_from_main() {
169 | assert_eq!(
170 | extract_module_name_from_path("./src/main.rs", "crate")
171 | .to_token_stream()
172 | .to_string()
173 | .replace(" ", ""),
174 | "crate"
175 | );
176 | }
177 |
178 | #[test]
179 | fn test_extract_module_name_from_workspace() {
180 | assert_eq!(
181 | extract_module_name_from_path("./server/src/routes/asset.rs", "crate")
182 | .to_token_stream()
183 | .to_string()
184 | .replace(" ", ""),
185 | "crate::routes::asset"
186 | );
187 | }
188 |
189 | #[test]
190 | fn test_extract_module_name_from_workspace_nested() {
191 | assert_eq!(
192 | extract_module_name_from_path("./crates/server/src/routes/asset.rs", "crate")
193 | .to_token_stream()
194 | .to_string()
195 | .replace(" ", ""),
196 | "crate::routes::asset"
197 | );
198 | }
199 |
200 | #[test]
201 | fn test_extract_module_name_from_folders() {
202 | assert_eq!(
203 | extract_module_name_from_path("./src/routing/api/audio.rs", "crate")
204 | .to_token_stream()
205 | .to_string()
206 | .replace(" ", ""),
207 | "crate::routing::api::audio"
208 | );
209 | }
210 |
211 | #[test]
212 | fn test_extract_module_name_from_folders_nested() {
213 | assert_eq!(
214 | extract_module_name_from_path("./src/applications/src/retail_api/controllers/mod.rs", "crate")
215 | .to_token_stream()
216 | .to_string()
217 | .replace(" ", ""),
218 | "crate::retail_api::controllers"
219 | );
220 | }
221 |
222 | #[test]
223 | fn test_extract_module_name_from_folders_nested_external_crate() {
224 | assert_eq!(
225 | extract_module_name_from_path("./src/applications/src/retail_api/controllers/mod.rs", "other_crate")
226 | .to_token_stream()
227 | .to_string()
228 | .replace(" ", ""),
229 | "other_crate::retail_api::controllers"
230 | );
231 | }
232 |
233 | #[test]
234 | fn test_extract_module_name_from_workspace_with_prefix_path() {
235 | assert_eq!(
236 | extract_module_name_from_path("./crates/server/src/routes_lib/routes/asset.rs", "crate::routes")
237 | .to_token_stream()
238 | .to_string()
239 | .replace(" ", ""),
240 | "crate::routes::asset"
241 | );
242 | }
243 |
244 | #[test]
245 | fn test_extract_module_name_from_workspace_with_external_crate_and_underscore() {
246 | assert_eq!(
247 | extract_module_name_from_path("./src/applications/src/retail-api/controllers/mod.rs", "other-crate")
248 | .to_token_stream()
249 | .to_string()
250 | .replace(" ", ""),
251 | "other_crate::retail_api::controllers"
252 | );
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/utoipauto-core/src/lib.rs:
--------------------------------------------------------------------------------
1 | extern crate proc_macro;
2 | extern crate proc_macro2;
3 | extern crate quote;
4 | extern crate syn;
5 | pub mod attribute_utils;
6 | pub mod discover;
7 | pub mod file_utils;
8 | pub mod string_utils;
9 | pub mod token_utils;
10 |
--------------------------------------------------------------------------------
/utoipauto-core/src/pair.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/utoipauto-core/src/string_utils.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::TokenStream;
2 |
3 | use crate::{discover::discover_from_file, token_utils::Parameters};
4 |
5 | pub fn rem_first_and_last(value: &str) -> &str {
6 | let mut chars = value.chars();
7 | chars.next();
8 | chars.next_back();
9 | chars.as_str()
10 | }
11 |
12 | pub fn trim_whites(str: &str) -> String {
13 | let s = str.trim();
14 |
15 | let s: String = s.replace('\n', "");
16 |
17 | s
18 | }
19 |
20 | pub fn trim_parentheses(str: &str) -> String {
21 | let s = str.trim();
22 |
23 | let s: String = s.replace(['(', ')'], "");
24 |
25 | s
26 | }
27 |
28 | /// Extract the file paths from the attributes
29 | /// Support the old syntax (MODULE_TREE_PATH => MODULE_SRC_PATH) ; (MODULE_TREE_PATH => MODULE_SRC_PATH) ;
30 | /// and the new syntax MODULE_SRC_PATH, MODULE_SRC_PATH
31 | ///
32 | /// # Example
33 | /// ```
34 | /// use utoipauto_core::string_utils::extract_paths;
35 | /// let paths = extract_paths(
36 | /// "(utoipa_auto_macro::tests::controllers::controller1 => ./utoipa-auto-macro/tests/controllers/controller1.rs) ; (utoipa_auto_macro::tests::controllers::controller2 => ./utoipa-auto-macro/tests/controllers/controller2.rs)"
37 | /// );
38 | /// assert_eq!(
39 | /// paths,
40 | /// vec![
41 | /// "./utoipa-auto-macro/tests/controllers/controller1.rs".to_string(),
42 | /// "./utoipa-auto-macro/tests/controllers/controller2.rs".to_string(),
43 | /// ]
44 | /// );
45 | /// ```
46 | pub fn extract_paths(attributes: &str) -> Vec {
47 | let attributes = trim_parentheses(attributes);
48 |
49 | if attributes.contains('|') {
50 | panic!("Please use the new syntax ! paths=\"(MODULE_TREE_PATH => MODULE_SRC_PATH) ;\"")
51 | }
52 | let paths = if attributes.contains("=>") {
53 | extract_paths_arrow(attributes)
54 | } else {
55 | extract_paths_coma(attributes)
56 | };
57 | if paths.is_empty() {
58 | panic!("utoipauto: No paths specified !")
59 | }
60 | paths
61 | }
62 |
63 | // (MODULE_TREE_PATH => MODULE_SRC_PATH) ; (MODULE_TREE_PATH => MODULE_SRC_PATH) ;
64 | // extract the paths from the string
65 | // Here for legacy support
66 | fn extract_paths_arrow(attributes: String) -> Vec {
67 | let mut paths: Vec = vec![];
68 | let attributes = attributes.split(';');
69 |
70 | for p in attributes {
71 | let pair = p.split_once("=>");
72 |
73 | if let Some(pair) = pair {
74 | paths.push(trim_whites(pair.1));
75 | }
76 | }
77 | paths
78 | }
79 |
80 | // MODULE_SRC_PATH, MODULE_SRC_PATH
81 | fn extract_paths_coma(attributes: String) -> Vec {
82 | let mut paths: Vec = vec![];
83 | let attributes = attributes.split(',');
84 |
85 | for p in attributes {
86 | paths.push(trim_whites(p));
87 | }
88 | paths
89 | }
90 |
91 | /// Return the list of all the functions with the #[utoipa] attribute
92 | /// and the list of all the structs with the #[derive(ToSchema)] attribute
93 | /// and the list of all the structs with the #[derive(ToResponse)] attribute
94 | pub fn discover(paths: Vec, params: &Parameters) -> (TokenStream, TokenStream, TokenStream) {
95 | let mut uto_paths = Vec::new();
96 | let mut uto_models = Vec::new();
97 | let mut uto_responses = Vec::new();
98 | for p in paths {
99 | let path = extract_crate_name(p);
100 | let (list_fn, list_model, list_reponse) = discover_from_file(path.paths, path.crate_name, params);
101 | uto_paths.extend(list_fn);
102 | uto_models.extend(list_model);
103 | uto_responses.extend(list_reponse);
104 | }
105 | // We need to add a coma after each path
106 | (
107 | quote::quote!(#(#uto_paths),*),
108 | quote::quote!(#(#uto_models),*),
109 | quote::quote!(#(#uto_responses),*),
110 | )
111 | }
112 |
113 | #[derive(Debug, PartialEq)]
114 | struct Path {
115 | paths: String,
116 | crate_name: String,
117 | }
118 |
119 | fn extract_crate_name(path: String) -> Path {
120 | let mut path = path.split(" from ");
121 | let paths = path.next().unwrap();
122 | let crate_name = path.next().unwrap_or("crate").to_string();
123 | Path {
124 | paths: paths.to_string(),
125 | crate_name,
126 | }
127 | }
128 |
129 | #[cfg(test)]
130 | mod test {
131 | use crate::string_utils::extract_paths;
132 |
133 | #[test]
134 | fn test_extract_path() {
135 | let paths = "./src";
136 | let extracted = extract_paths(paths);
137 | assert_eq!(extracted, vec!["./src".to_string()]);
138 | }
139 |
140 | #[test]
141 | fn test_extract_crate_name() {
142 | assert_eq!(
143 | super::extract_crate_name(
144 | "utoipa_auto_macro::from::controllers::controller1 from utoipa_auto_macro".to_string(),
145 | ),
146 | super::Path {
147 | paths: "utoipa_auto_macro::from::controllers::controller1".to_string(),
148 | crate_name: "utoipa_auto_macro".to_string()
149 | }
150 | );
151 | }
152 |
153 | #[test]
154 | fn test_extract_crate_name_default() {
155 | assert_eq!(
156 | super::extract_crate_name("utoipa_auto_macro::from::controllers::controller1".to_string()),
157 | super::Path {
158 | paths: "utoipa_auto_macro::from::controllers::controller1".to_string(),
159 | crate_name: "crate".to_string()
160 | }
161 | );
162 | }
163 |
164 | #[test]
165 | fn test_extract_paths_arrow() {
166 | assert_eq!(
167 | super::extract_paths(
168 | "(utoipa_auto_macro::tests::controllers::controller1 => ./utoipa-auto-macro/tests/controllers/controller1.rs) ; (utoipa_auto_macro::tests::controllers::controller2 => ./utoipa-auto-macro/tests/controllers/controller2.rs)"
169 | ),
170 | vec![
171 | "./utoipa-auto-macro/tests/controllers/controller1.rs".to_string(),
172 | "./utoipa-auto-macro/tests/controllers/controller2.rs".to_string()
173 | ]
174 | );
175 | }
176 |
177 | #[test]
178 | fn test_extract_paths_coma() {
179 | assert_eq!(
180 | super::extract_paths(
181 | "./utoipa-auto-macro/tests/controllers/controller1.rs, ./utoipa-auto-macro/tests/controllers/controller2.rs"
182 | ),
183 | vec![
184 | "./utoipa-auto-macro/tests/controllers/controller1.rs".to_string(),
185 | "./utoipa-auto-macro/tests/controllers/controller2.rs".to_string()
186 | ]
187 | );
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/utoipauto-core/src/token_utils.rs:
--------------------------------------------------------------------------------
1 | use proc_macro::TokenStream;
2 | use proc_macro2::Literal;
3 | use quote::quote;
4 | use syn::Attribute;
5 |
6 | pub struct Parameters {
7 | pub paths: String,
8 | pub fn_attribute_name: String,
9 | pub schema_attribute_name: String,
10 | pub response_attribute_name: String,
11 | }
12 |
13 | /// Extract the paths string attribute from the proc_macro::TokenStream
14 | ///
15 | /// If none is specified, we use the default path "./src"
16 | pub fn extract_attributes(stream: proc_macro2::TokenStream) -> Parameters {
17 | let paths = extract_attribute("paths", stream.clone());
18 | let fn_attribute_name = extract_attribute("function_attribute_name", stream.clone());
19 | let schema_attribute_name = extract_attribute("schema_attribute_name", stream.clone());
20 | let response_attribute_name = extract_attribute("response_attribute_name", stream);
21 | // if no paths specified, we use the default path "./src"
22 | Parameters {
23 | paths: paths.unwrap_or("./src".to_string()),
24 | fn_attribute_name: fn_attribute_name.unwrap_or("utoipa".to_string()),
25 | schema_attribute_name: schema_attribute_name.unwrap_or("ToSchema".to_string()),
26 | response_attribute_name: response_attribute_name.unwrap_or("ToResponse".to_string()),
27 | }
28 | }
29 |
30 | // extract the name = "" attributes from the proc_macro::TokenStream
31 | fn extract_attribute(name: &str, stream: proc_macro2::TokenStream) -> Option {
32 | let mut has_value = false;
33 |
34 | for token in stream {
35 | if has_value {
36 | if let proc_macro2::TokenTree::Literal(lit) = token {
37 | return Some(get_content(lit));
38 | }
39 | }
40 | if let proc_macro2::TokenTree::Ident(ident) = token {
41 | if ident.to_string().eq(name) {
42 | has_value = true;
43 | }
44 | }
45 | }
46 | None
47 | }
48 |
49 | fn get_content(lit: Literal) -> String {
50 | let content = lit.to_string();
51 | content[1..content.len() - 1].to_string()
52 | }
53 |
54 | /// Check if the macro is placed before the #[derive] and #[openapi] attributes
55 | /// Otherwise, panic!
56 | pub fn check_macro_placement(attrs: Vec) {
57 | if !attrs.iter().any(|elm| elm.path().is_ident("derive")) {
58 | panic!("Please put utoipauto before #[derive] and #[openapi]");
59 | }
60 |
61 | if !attrs.iter().any(|elm| elm.path().is_ident("openapi")) {
62 | panic!("Please put utoipauto before #[derive] and #[openapi]");
63 | }
64 | }
65 |
66 | // Output the macro back to the compiler
67 | pub fn output_macro(openapi_macro: syn::ItemStruct) -> proc_macro::TokenStream {
68 | let code = quote!(
69 | #openapi_macro
70 | );
71 |
72 | TokenStream::from(code)
73 | }
74 |
75 | #[cfg(test)]
76 | mod tests {
77 | use super::*;
78 |
79 | #[test]
80 | fn test_get_content() {
81 | let lit = Literal::string("p1");
82 | let content = get_content(lit);
83 | assert_eq!(content, "p1");
84 | }
85 |
86 | #[test]
87 | fn test_extract_attributes() {
88 | let tokens = quote! {
89 | paths = "p1"
90 | };
91 |
92 | let attributes = extract_attributes(tokens);
93 | assert_eq!(attributes.paths, "p1")
94 | }
95 |
96 | #[test]
97 | fn test_extract_attribute() {
98 | let quote = quote! {
99 | paths = "p1", thing = "thing", other = "other"
100 | };
101 |
102 | let attributes = extract_attribute("thing", quote).unwrap();
103 | assert_eq!(attributes, "thing");
104 | }
105 |
106 | #[test]
107 | fn test_extract_attribute_none() {
108 | let quote = quote! {
109 | paths = "p1", thing = "thing", other = "other"
110 | };
111 |
112 | let attributes = extract_attribute("not_found", quote);
113 | assert_eq!(attributes, None);
114 | }
115 |
116 | #[test]
117 | fn test_extract_attribute_empty() {
118 | let quote = quote! {};
119 |
120 | let attributes = extract_attribute("thing", quote);
121 | assert_eq!(attributes, None);
122 | }
123 |
124 | #[test]
125 | fn test_extract_attributes_empty() {
126 | let tokens = quote! {};
127 |
128 | let attributes = extract_attributes(tokens);
129 | assert_eq!(attributes.paths, "./src");
130 | assert_eq!(attributes.fn_attribute_name, "utoipa");
131 | assert_eq!(attributes.schema_attribute_name, "ToSchema");
132 | assert_eq!(attributes.response_attribute_name, "ToResponse");
133 | }
134 |
135 | #[test]
136 | fn test_extract_attributes_custom_name() {
137 | let tokens = quote! {
138 | paths = "p1", function_attribute_name = "handler", schema_attribute_name = "Schema", response_attribute_name = "Response"
139 | };
140 |
141 | let attributes = extract_attributes(tokens);
142 | assert_eq!(attributes.paths, "p1");
143 | assert_eq!(attributes.fn_attribute_name, "handler");
144 | assert_eq!(attributes.schema_attribute_name, "Schema");
145 | assert_eq!(attributes.response_attribute_name, "Response");
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/utoipauto-macro/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "utoipauto-macro"
3 | authors.workspace = true
4 | version.workspace = true
5 | edition.workspace = true
6 | keywords.workspace = true
7 | description.workspace = true
8 | categories.workspace = true
9 | license.workspace = true
10 | readme.workspace = true
11 | repository.workspace = true
12 | homepage.workspace = true
13 | rust-version.workspace = true
14 |
15 | [lib]
16 | proc-macro = true
17 |
18 | [dependencies]
19 | utoipauto-core.workspace = true
20 |
21 | quote.workspace = true
22 | syn.workspace = true
23 | proc-macro2.workspace = true
24 |
--------------------------------------------------------------------------------
/utoipauto-macro/src/lib.rs:
--------------------------------------------------------------------------------
1 | use attribute_utils::update_openapi_macro_attributes;
2 | use proc_macro::TokenStream;
3 |
4 | use quote::quote;
5 | use string_utils::{discover, extract_paths};
6 | use syn::parse_macro_input;
7 | use token_utils::{check_macro_placement, extract_attributes, output_macro};
8 | use utoipauto_core::{attribute_utils, string_utils, token_utils};
9 |
10 | /// Macro to automatically discover all the functions with the #[utoipa] attribute
11 | /// And the struct deriving ToSchema and ToResponse
12 | #[proc_macro_attribute]
13 | pub fn utoipauto(
14 | attributes: proc_macro::TokenStream, // #[utoipauto(paths = "(MODULE_TREE_PATH => MODULE_SRC_PATH) ;")]
15 | item: proc_macro::TokenStream, // #[openapi(paths = "")]
16 | ) -> proc_macro::TokenStream {
17 | // (MODULE_TREE_PATH => MODULE_SRC_PATH) ; (MODULE_TREE_PATH => MODULE_SRC_PATH) ; ...
18 | let params = extract_attributes(attributes.into());
19 | // [(MODULE_TREE_PATH, MODULE_SRC_PATH)]
20 | let paths: Vec = extract_paths(¶ms.paths);
21 |
22 | // #[openapi(...)]
23 | let mut openapi_macro = parse_macro_input!(item as syn::ItemStruct);
24 |
25 | // Discover all the functions with the #[utoipa] attribute
26 | let (uto_paths, uto_models, uto_responses) = discover(paths, ¶ms);
27 |
28 | // extract the openapi macro attributes : #[openapi(openapi_macro_attibutes)]
29 | let openapi_macro_attibutes = &mut openapi_macro.attrs;
30 |
31 | // Check if the macro is placed before the #[derive] and #[openapi] attributes
32 | check_macro_placement(openapi_macro_attibutes.clone());
33 |
34 | // Update the openapi macro attributes with the newly discovered paths
35 | update_openapi_macro_attributes(openapi_macro_attibutes, &uto_paths, &uto_models, &uto_responses);
36 |
37 | // Output the macro back to the compiler
38 | output_macro(openapi_macro)
39 | }
40 |
41 | /// Ignore the function from the auto discovery
42 | #[proc_macro_attribute]
43 | pub fn utoipa_ignore(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
44 | let input = parse_macro_input!(item as syn::Item);
45 | let code = quote!(
46 | #input
47 | );
48 |
49 | TokenStream::from(code)
50 | }
51 |
52 | /// Useless macro to test custom function attributes
53 | #[proc_macro_attribute]
54 | pub fn test_handler(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
55 | let input = parse_macro_input!(item as syn::ItemFn);
56 |
57 | let code = quote!(
58 | #[utoipa::path(get, path = "/")]
59 | #input
60 | );
61 |
62 | TokenStream::from(code)
63 | }
64 |
--------------------------------------------------------------------------------
/utoipauto/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "utoipauto"
3 | authors.workspace = true
4 | version.workspace = true
5 | edition.workspace = true
6 | keywords.workspace = true
7 | description.workspace = true
8 | categories.workspace = true
9 | license.workspace = true
10 | readme.workspace = true
11 | repository.workspace = true
12 | homepage.workspace = true
13 | rust-version.workspace = true
14 |
15 | [dependencies]
16 | utoipauto-macro.workspace = true
17 |
18 | [dev-dependencies]
19 | utoipa.workspace = true
20 | serde_json = "1.0.128"
21 | syn = { version = "2.0.74", features = ["extra-traits", "full"] }
22 |
--------------------------------------------------------------------------------
/utoipauto/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)] // This code is used in the tests
2 |
3 | pub use utoipauto_macro::*;
4 |
5 | #[cfg(test)]
6 | mod test {
7 | use utoipa::OpenApi;
8 | use utoipauto_macro::utoipauto;
9 |
10 | #[utoipa::path(post, path = "/route1")]
11 | pub fn route1() {}
12 |
13 | #[utoipa::path(post, path = "/route2")]
14 | pub fn route2() {}
15 |
16 | #[utoipa::path(post, path = "/route3")]
17 | pub fn route3() {}
18 |
19 | /// Discover from the crate root auto
20 | #[utoipauto(paths = "./utoipauto/src")]
21 | #[derive(OpenApi)]
22 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
23 | pub struct CrateAutoApiDocs {}
24 |
25 | #[test]
26 | fn test_crate_auto_import_path() {
27 | assert_eq!(CrateAutoApiDocs::openapi().paths.paths.len(), 3)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/utoipauto/tests/default_features/controllers/controller1.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)] // This code is used in the tests
2 | use utoipauto_macro::utoipa_ignore;
3 |
4 | #[utoipa::path(post, path = "/route1")]
5 | pub fn route1() {}
6 |
7 | #[utoipa_ignore]
8 | #[utoipa::path(post, path = "/route-ignored")]
9 | pub fn route_ignored() {}
10 |
--------------------------------------------------------------------------------
/utoipauto/tests/default_features/controllers/controller2.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)] // This code is used in the tests
2 |
3 | #[utoipa::path(post, path = "/route3")]
4 | pub fn route3() {}
5 |
--------------------------------------------------------------------------------
/utoipauto/tests/default_features/controllers/controller3.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)]
2 | use utoipauto_macro::test_handler;
3 |
4 | #[test_handler]
5 | pub fn route_custom() {}
6 |
--------------------------------------------------------------------------------
/utoipauto/tests/default_features/controllers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod controller1;
2 | pub mod controller2;
3 | pub mod controller3;
4 |
--------------------------------------------------------------------------------
/utoipauto/tests/default_features/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod controllers;
2 | pub mod models;
3 | pub mod test;
4 |
--------------------------------------------------------------------------------
/utoipauto/tests/default_features/models.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)]
2 |
3 | use std::borrow::Cow;
4 | use utoipa::openapi::schema::SchemaType;
5 | use utoipa::openapi::{Response, ResponseBuilder, Type};
6 | // This code is used in the tests
7 | use utoipa::{
8 | openapi::{ObjectBuilder, RefOr, Schema},
9 | PartialSchema, ToResponse, ToSchema,
10 | };
11 | use utoipauto_macro::utoipa_ignore;
12 |
13 | #[derive(ToSchema)]
14 | pub struct ModelSchema;
15 | #[derive(ToResponse)]
16 | pub struct ModelResponse;
17 |
18 | #[utoipa_ignore]
19 | #[derive(ToSchema)]
20 | pub struct IgnoredModelSchema;
21 |
22 | // Manual implementation of ToSchema
23 | pub struct ModelSchemaImpl;
24 |
25 | impl PartialSchema for ModelSchemaImpl {
26 | fn schema() -> RefOr {
27 | ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)).into()
28 | }
29 | }
30 |
31 | impl ToSchema for ModelSchemaImpl {
32 | fn name() -> Cow<'static, str> {
33 | Cow::Borrowed("ModelSchemaImpl")
34 | }
35 | }
36 |
37 | // Manual implementation of utoipa::ToSchema
38 | pub struct ModelSchemaImplFullName;
39 |
40 | impl PartialSchema for ModelSchemaImplFullName {
41 | fn schema() -> RefOr {
42 | ObjectBuilder::new().schema_type(SchemaType::Type(Type::String)).into()
43 | }
44 | }
45 |
46 | impl utoipa::ToSchema for ModelSchemaImplFullName {
47 | fn name() -> Cow<'static, str> {
48 | Cow::Borrowed("ModelSchemaImplFullName")
49 | }
50 | }
51 |
52 | // Manual implementation of ToSchema
53 | pub struct ModelResponseImpl;
54 |
55 | impl<'s> ToResponse<'s> for ModelResponseImpl {
56 | fn response() -> (&'s str, RefOr) {
57 | (
58 | "ModelResponseImpl",
59 | ResponseBuilder::new().description("A manual response").into(),
60 | )
61 | }
62 | }
63 |
64 | // Manual implementation of utoipa::ToResponse
65 | pub struct ModelResponseImplFullName;
66 |
67 | impl<'s> utoipa::ToResponse<'s> for ModelResponseImplFullName {
68 | fn response() -> (&'s str, RefOr) {
69 | (
70 | "ModelResponseImplFullName",
71 | ResponseBuilder::new().description("A manual response").into(),
72 | )
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/utoipauto/tests/default_features/test.rs:
--------------------------------------------------------------------------------
1 | use utoipa::OpenApi;
2 |
3 | use utoipauto::utoipauto;
4 |
5 | use crate::default_features::controllers;
6 |
7 | // Discover from multiple controllers
8 | #[utoipauto(
9 | paths = "( crate::controllers::controller1 => ./utoipauto/tests/default_features/controllers/controller1.rs) ; ( crate::controllers::controller2 => ./utoipauto/tests/default_features/controllers/controller2.rs )"
10 | )]
11 | #[derive(OpenApi)]
12 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
13 | pub struct MultiControllerApiDocs {}
14 |
15 | #[test]
16 | fn test_path_import() {
17 | assert_eq!(MultiControllerApiDocs::openapi().paths.paths.len(), 2)
18 | }
19 |
20 | /// Discover from a single controller
21 | #[utoipauto(
22 | paths = "( crate::controllers::controller1 => ./utoipauto/tests/default_features/controllers/controller1.rs)"
23 | )]
24 | #[derive(OpenApi)]
25 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
26 | pub struct SingleControllerApiDocs {}
27 |
28 | #[test]
29 | fn test_ignored_path() {
30 | assert_eq!(SingleControllerApiDocs::openapi().paths.paths.len(), 1)
31 | }
32 |
33 | /// Discover with manual path
34 | #[utoipauto(paths = "./utoipauto/tests/default_features/controllers/controller1.rs")]
35 | #[derive(OpenApi)]
36 | #[openapi(
37 | info(title = "Percentage API", version = "1.0.0"),
38 | paths(controllers::controller2::route3)
39 | )]
40 | pub struct SingleControllerManualPathApiDocs {}
41 |
42 | #[test]
43 | fn test_manual_path() {
44 | assert_eq!(SingleControllerManualPathApiDocs::openapi().paths.paths.len(), 2)
45 | }
46 |
47 | /// Discover from a module root
48 | #[utoipauto(paths = "( crate::controllers => ./utoipauto/tests/default_features/controllers)")]
49 | #[derive(OpenApi)]
50 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
51 | pub struct ModuleApiDocs {}
52 |
53 | #[test]
54 | fn test_module_import_path() {
55 | assert_eq!(ModuleApiDocs::openapi().paths.paths.len(), 2)
56 | }
57 |
58 | /// Discover from the crate root
59 | #[utoipauto(paths = "./utoipauto/tests/default_features")]
60 | #[derive(OpenApi)]
61 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
62 | pub struct CrateApiDocs {}
63 |
64 | #[test]
65 | fn test_crate_import_path() {
66 | assert_eq!(CrateApiDocs::openapi().paths.paths.len(), 2)
67 | }
68 |
69 | // Discover from multiple controllers new syntax
70 | #[utoipauto(
71 | paths = "./utoipauto/tests/default_features/controllers/controller1.rs, ./utoipauto/tests/default_features/controllers/controller2.rs"
72 | )]
73 | #[derive(OpenApi)]
74 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
75 | pub struct MultiControllerNoModuleApiDocs {}
76 |
77 | #[test]
78 | fn test_path_import_no_module() {
79 | assert_eq!(MultiControllerNoModuleApiDocs::openapi().paths.paths.len(), 2)
80 | }
81 |
82 | // Discover from multiple controllers new syntax
83 | #[utoipauto(paths = "./utoipauto/tests/default_features/models.rs")]
84 | #[derive(OpenApi)]
85 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
86 | pub struct ModelsImportApiDocs {}
87 |
88 | #[test]
89 | fn test_path_import_schema() {
90 | assert_eq!(
91 | ModelsImportApiDocs::openapi()
92 | .components
93 | .expect("no components")
94 | .schemas
95 | .len(),
96 | 3, // 1 derive, 1 manual, 1 manual with utoipa::ToSchema
97 | )
98 | }
99 |
100 | // Discover from multiple controllers new syntax
101 | #[utoipauto(paths = "./utoipauto/tests/default_features/models.rs")]
102 | #[derive(OpenApi)]
103 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
104 | pub struct ResponsesImportApiDocs {}
105 |
106 | #[test]
107 | fn test_path_import_responses() {
108 | assert_eq!(
109 | ResponsesImportApiDocs::openapi()
110 | .components
111 | .expect("no components")
112 | .responses
113 | .len(),
114 | 3, // 1 derive, 1 manual, 1 manual with utoipa::ToResponse
115 | )
116 | }
117 |
118 | /// Discover custom handler
119 | #[utoipauto(
120 | paths = "./utoipauto/tests/default_features/controllers/controller3.rs",
121 | function_attribute_name = "test_handler"
122 | )]
123 | #[derive(OpenApi)]
124 | #[openapi(info(title = "Percentage API", version = "1.0.0"))]
125 | pub struct CustomHandlerApiDocs {}
126 |
127 | #[test]
128 | fn test_custom_handler() {
129 | assert_eq!(CustomHandlerApiDocs::openapi().paths.paths.len(), 1)
130 | }
131 |
--------------------------------------------------------------------------------
/utoipauto/tests/test.rs:
--------------------------------------------------------------------------------
1 | mod default_features;
2 |
--------------------------------------------------------------------------------