├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── DocsGenerate.ts.example
├── DocsGeneratev6.ts.example
├── LICENSE
├── README.md
├── package.json
├── pnpm-lock.yaml
├── publish.sh
├── src
├── adonishelpers.ts
├── autoswagger.ts
├── example.ts
├── helpers.ts
├── index.ts
├── parsers.ts
├── scalarCustomCss.ts
└── types.ts
└── tsconfig.json
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 | run-name: Deploying to npmjs 🥹🤞
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 | jobs:
8 | Deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: actions/setup-node@v3
13 | with:
14 | node-version: '20.x'
15 | registry-url: "https://registry.npmjs.com"
16 | - uses: pnpm/action-setup@v4
17 | - run: pnpm install
18 | - run: pnpm build
19 | - run: git config user.email "deploy@github" && git config user.name "GitHub Action"
20 | - run: pnpm publish --no-git-checks
21 | env:
22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/*
--------------------------------------------------------------------------------
/DocsGenerate.ts.example:
--------------------------------------------------------------------------------
1 | import { BaseCommand } from '@adonisjs/core/build/standalone'
2 | import AutoSwagger from 'adonis-autoswagger'
3 | import swagger from '../config/swagger'
4 | export default class DocsGenerate extends BaseCommand {
5 | public static commandName = 'docs:generate'
6 |
7 | public static description = ''
8 |
9 | public static settings = {
10 | loadApp: true,
11 |
12 | stayAlive: false,
13 | }
14 |
15 | public async run() {
16 | const Router = await this.application.container.use('Adonis/Core/Route')
17 | Router.commit()
18 | await AutoSwagger.writeFile(await Router.toJSON(), swagger)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/DocsGeneratev6.ts.example:
--------------------------------------------------------------------------------
1 | import { BaseCommand } from '@adonisjs/core/ace'
2 | import { CommandOptions } from '@adonisjs/core/types/ace'
3 | import AutoSwagger from 'adonis-autoswagger'
4 | import swagger from '#config/swagger'
5 | export default class DocsGenerate extends BaseCommand {
6 | static commandName = 'docs:generate'
7 |
8 | static options: CommandOptions = {
9 | startApp: true,
10 | allowUnknownFlags: false,
11 | staysAlive: false,
12 | }
13 |
14 | async run() {
15 | const Router = await this.app.container.make('router')
16 | Router.commit()
17 | await AutoSwagger.default.writeFile(Router.toJSON(), swagger)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Adis Durakovic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Adonis AutoSwagger
3 |
4 |
5 |
6 | []()
7 | []()
8 | []()
9 | []()
10 |
11 | ### Auto-Generate swagger docs for AdonisJS
12 |
13 | ## 💻️ Install
14 |
15 | ```bash
16 | pnpm i adonis-autoswagger #using pnpm
17 | ```
18 |
19 | ---
20 |
21 | ## ⭐️ Features
22 |
23 | - Creates **paths** automatically based on `routes.ts`
24 | - Creates **schemas** automatically based on `app/Models/*`
25 | - Creates **schemas** automatically based on `app/Interfaces/*`
26 | - Creates **schemas** automatically based on `app/Validators/*` (only for adonisJS v6)
27 | - Creates **schemas** automatically based on `app/Types/*` (only for adonisJS v6)
28 | - **Rich configuration** via comments
29 | - Works also in **production** mode
30 | - `node ace docs:generate` command
31 |
32 | ---
33 |
34 | ## ✌️Usage
35 |
36 | Create a file `/config/swagger.ts`
37 |
38 | ```ts
39 | // for AdonisJS v6
40 | import path from "node:path";
41 | import url from "node:url";
42 | // ---
43 |
44 | export default {
45 | // path: __dirname + "/../", for AdonisJS v5
46 | path: path.dirname(url.fileURLToPath(import.meta.url)) + "/../", // for AdonisJS v6
47 | title: "Foo", // use info instead
48 | version: "1.0.0", // use info instead
49 | description: "", // use info instead
50 | tagIndex: 2,
51 | productionEnv: "production", // optional
52 | info: {
53 | title: "title",
54 | version: "1.0.0",
55 | description: "",
56 | },
57 | snakeCase: true,
58 |
59 | debug: false, // set to true, to get some useful debug output
60 | ignore: ["/swagger", "/docs"],
61 | preferredPutPatch: "PUT", // if PUT/PATCH are provided for the same route, prefer PUT
62 | common: {
63 | parameters: {}, // OpenAPI conform parameters that are commonly used
64 | headers: {}, // OpenAPI conform headers that are commonly used
65 | },
66 | securitySchemes: {}, // optional
67 | authMiddlewares: ["auth", "auth:api"], // optional
68 | defaultSecurityScheme: "BearerAuth", // optional
69 | persistAuthorization: true, // persist authorization between reloads on the swagger page
70 | showFullPath: false, // the path displayed after endpoint summary
71 | };
72 | ```
73 |
74 | In your `routes.ts`
75 |
76 | ## 6️⃣ for AdonisJS v6
77 |
78 | ```js
79 | import AutoSwagger from "adonis-autoswagger";
80 | import swagger from "#config/swagger";
81 | // returns swagger in YAML
82 | router.get("/swagger", async () => {
83 | return AutoSwagger.default.docs(router.toJSON(), swagger);
84 | });
85 |
86 | // Renders Swagger-UI and passes YAML-output of /swagger
87 | router.get("/docs", async () => {
88 | return AutoSwagger.default.ui("/swagger", swagger);
89 | // return AutoSwagger.default.scalar("/swagger"); to use Scalar instead. If you want, you can pass proxy url as second argument here.
90 | // return AutoSwagger.default.rapidoc("/swagger", "view"); to use RapiDoc instead (pass "view" default, or "read" to change the render-style)
91 | });
92 | ```
93 |
94 | ## 5️⃣ for AdonisJS v5
95 |
96 | ```js
97 | import AutoSwagger from "adonis-autoswagger";
98 | import swagger from "Config/swagger";
99 | // returns swagger in YAML
100 | Route.get("/swagger", async () => {
101 | return AutoSwagger.docs(Route.toJSON(), swagger);
102 | });
103 |
104 | // Renders Swagger-UI and passes YAML-output of /swagger
105 | Route.get("/docs", async () => {
106 | return AutoSwagger.ui("/swagger", swagger);
107 | });
108 | ```
109 |
110 | ### 👍️ Done
111 |
112 | Visit `http://localhost:3333/docs` to see AutoSwagger in action.
113 |
114 | ### Functions
115 |
116 | - `async docs(routes, conf)`: get the specification in YAML format
117 | - `async json(routes, conf)`: get the specification in JSON format
118 | - `ui(path, conf)`: get default swagger UI
119 | - `rapidoc(path, style)`: get rapidoc UI
120 | - `scalar(path, proxyUrl)`: get scalar UI
121 | - `stoplight(path, theme)`: get stoplight elements UI
122 | - `jsonToYaml(json)`: can be used to convert `json()` back to yaml
123 |
124 | ---
125 |
126 | ## 💡 Compatibility
127 |
128 | For controllers to get detected properly, please load them lazily.
129 |
130 | ```ts
131 | ✅ const TestController = () => import('#controllers/test_controller')
132 | ❌ import TestController from '#controllers/test_controller'
133 | ```
134 |
135 | ## 🧑💻 Advanced usage
136 |
137 | ### Additional configuration
138 |
139 | **info**
140 | See [Swagger API General Info](https://swagger.io/docs/specification/api-general-info/) for details.
141 |
142 | **securitySchemes**
143 |
144 | Add/Overwrite security schemes [Swagger Authentication](https://swagger.io/docs/specification/authentication/) for details.
145 |
146 | ```ts
147 | // example to override ApiKeyAuth
148 | securitySchemes: {
149 | ApiKeyAuth: {
150 | type: "apiKey"
151 | in: "header",
152 | name: "X-API-Key"
153 | }
154 | }
155 | ```
156 |
157 | **defaultSecurityScheme**
158 |
159 | Override the default security scheme.
160 |
161 | - BearerAuth
162 | - BasicAuth
163 | - ApiKeyAuth
164 | - your own defined under `securitySchemes`
165 |
166 | **authMiddlewares**
167 |
168 | If a route uses a middleware named `auth`, `auth:api`, AutoSwagger will detect it as a Swagger security method. However, you can implement other middlewares that handle authentication.
169 |
170 | ### Modify generated output
171 |
172 | ```ts
173 | Route.get("/myswagger", async () => {
174 | const json = await AutoSwagger.json(Route.toJSON(), swagger);
175 | // modify json to your hearts content
176 | return AutoSwagger.jsonToYaml(json);
177 | });
178 |
179 | Route.get("/docs", async () => {
180 | return AutoSwagger.ui("/myswagger", swagger);
181 | });
182 | ```
183 |
184 | ### Custom Paths in adonisJS v6
185 |
186 | AutoSwagger supports the paths set in `package.json`. Interfaces are expected to be in `app/interfaces`. However, you can override this, by modifying package.json as follows.
187 |
188 | ```json
189 | //...
190 | "imports": {
191 | // ...
192 | "#interfaces/*": "./app/custom/path/interfaces/*.js"
193 | // ...
194 | }
195 | //...
196 |
197 | ```
198 |
199 | ---
200 |
201 | ## 📃 Configure
202 |
203 | ### `tagIndex`
204 |
205 | Tags endpoints automatically
206 |
207 | - If your routes are `/api/v1/products/...` then your tagIndex should be `3`
208 | - If your routes are `/v1/products/...` then your tagIndex should be `2`
209 | - If your routes are `/products/...` then your tagIndex should be `1`
210 |
211 | ### `ignore`
212 |
213 | Ignores specified paths. When used with a wildcard (\*), AutoSwagger will ignore everything matching before/after the wildcard.
214 | `/test/_`will ignore everything starting with`/test/`, whereas `\*/test`will ignore everything ending with`/test`.
215 |
216 | ### `common`
217 |
218 | Sometimes you want to use specific parameters or headers on multiple responses.
219 |
220 | _Example:_ Some resources use the same filter parameters or return the same headers.
221 |
222 | Here's where you can set these and use them with `@paramUse()` and `@responseHeader() @use()`. See practical example for further details.
223 |
224 | ---
225 |
226 | # 💫 Extend Controllers
227 |
228 | ## Add additional documentation to your Controller-files
229 |
230 | **@summary** (only one)
231 | A summary of what the action does
232 |
233 | **@tag** (only one)
234 | Set a custom tag for this action
235 |
236 | **@description** (only one)
237 | A detailed description of what the action does.
238 |
239 | **@operationId** (only one)
240 | An optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API..
241 |
242 | **@responseBody** (multiple)
243 |
244 | Format: ` - - `
245 |
246 | `` can be either a ``, `/` or a custom JSON `{}`
247 |
248 | **@responseHeader** (multiple)
249 |
250 | Format: ` - - - `
251 |
252 | **@param`Type`** (multiple)
253 |
254 | `Type` can be one of [Parameter Types](https://swagger.io/docs/specification/describing-parameters/) (first letter in uppercase)
255 |
256 | **@requestBody** (only one)
257 | A definition of the expected requestBody
258 |
259 | Format: ``
260 |
261 | `` can be either a ``, `/`, or a custom JSON `{}`
262 |
263 | **@requestFormDataBody** (only one)
264 | A definition of the expected requestBody that will be sent with formData format.
265 |
266 | **Schema**
267 | A model or a validator.
268 | Format: ``
269 |
270 | **Custom format**
271 |
272 | Format: `{"fieldname": {"type":"string", "format": "email"}}`
273 | This format should be a valid openapi 3.x json.
274 |
275 | ---
276 |
277 | # 🤘Examples
278 |
279 | ## `@responseBody` examples
280 |
281 | ```ts
282 | @responseBody - Lorem ipsum Dolor sit amet
283 |
284 | @responseBody // returns standard message
285 |
286 | @responseBody - // returns model specification
287 |
288 | @responseBody - // returns model-array specification
289 |
290 | @responseBody - .with(relations, property1, property2.relations, property3.subproperty.relations) // returns a model and a defined relation
291 |
292 | @responseBody - .with(relations).exclude(property1, property2, property3.subproperty) // returns model specification
293 |
294 | @responseBody - .append("some":"valid json") // append additional properties to a Model
295 |
296 | @responseBody - .paginated() // helper function to return adonisJS conform structure like {"data": [], "meta": {}}
297 |
298 | @responseBody - .paginated(dataName, metaName) // returns a paginated model with custom keys for the data array and meta object, use `.paginated(dataName)` or `.paginated(,metaName)` if you want to override only one. Don't forget the ',' for the second parameter.
299 |
300 | @responseBody - .only(property1, property2) // pick only specific properties
301 |
302 | @requestBody // returns a validator object
303 |
304 | @responseBody - {"foo": "bar", "baz": ""} //returns custom json object and also parses the model
305 | @responseBody - ["foo", "bar"] //returns custom json array
306 | ```
307 |
308 | ## `@paramPath` and `@paramQuery` examples
309 |
310 | ```ts
311 | // basicaly same as @response, just without a status
312 | @paramPath - Description - (meta)
313 | @paramQuery - Description - (meta)
314 |
315 | @paramPath id - The ID of the source - @type(number) @required
316 | @paramPath slug - The ID of the source - @type(string)
317 |
318 | @paramQuery q - Search term - @type(string) @required
319 | @paramQuery page - the Page number - @type(number)
320 |
321 | ```
322 |
323 | ## `@requestBody` examples
324 |
325 | ```ts
326 | // basicaly same as @response, just without a status
327 | @requestBody // Expects model specification
328 | @requestBody // Expects validator specification
329 | @requestBody .with(relations) // Expects model and its relations
330 | @requestBody .append("some":"valid json") // append additional properties to a Model
331 | @requestBody {"foo": "bar"} // Expects a specific JSON
332 | ```
333 |
334 | ## `@requestFormDataBody` examples
335 |
336 | ```ts
337 | // Providing a raw JSON
338 | @requestFormDataBody {"name":{"type":"string"},"picture":{"type":"string","format":"binary"}} // Expects a valid OpenAPI 3.x JSON
339 | ```
340 |
341 | ```ts
342 | // Providing a Model, and adding additional fields
343 | @requestFormDataBody // Expects a valid OpenAPI 3.x JSON
344 | @requestFormDataBody .exclude(property1).append("picture":{"type":"string","format":"binary"}) // Expects a valid OpenAPI 3.x JSON
345 | ```
346 |
347 | ---
348 |
349 | # **Practical example**
350 |
351 | `config/swagger.ts`
352 |
353 | ```ts
354 | export default {
355 | path: __dirname + "../",
356 | title: "YourProject",
357 | version: "1.0.0",
358 | tagIndex: 2,
359 | ignore: ["/swagger", "/docs", "/v1", "/", "/something/*", "*/something"],
360 | common: {
361 | parameters: {
362 | sortable: [
363 | {
364 | in: "query",
365 | name: "sortBy",
366 | schema: { type: "string", example: "foo" },
367 | },
368 | {
369 | in: "query",
370 | name: "sortType",
371 | schema: { type: "string", example: "ASC" },
372 | },
373 | ],
374 | },
375 | headers: {
376 | paginated: {
377 | "X-Total-Pages": {
378 | description: "Total amount of pages",
379 | schema: { type: "integer", example: 5 },
380 | },
381 | "X-Total": {
382 | description: "Total amount of results",
383 | schema: { type: "integer", example: 100 },
384 | },
385 | "X-Per-Page": {
386 | description: "Results per page",
387 | schema: { type: "integer", example: 20 },
388 | },
389 | },
390 | },
391 | },
392 | };
393 | ```
394 |
395 | `app/Controllers/Http/SomeController.ts`
396 |
397 | ```ts
398 | export default class SomeController {
399 | /**
400 | * @index
401 | * @operationId getProducts
402 | * @description Returns array of producs and it's relations
403 | * @responseBody 200 - .with(relations)
404 | * @paramUse(sortable, filterable)
405 | * @responseHeader 200 - @use(paginated)
406 | * @responseHeader 200 - X-pages - A description of the header - @example(test)
407 | */
408 | public async index({ request, response }: HttpContextContract) {}
409 |
410 | /**
411 | * @show
412 | * @paramPath id - Describe the path param - @type(string) @required
413 | * @paramQuery foo - Describe the query param - @type(string) @required
414 | * @description Returns a product with it's relation on user and user relations
415 | * @responseBody 200 - .with(user, user.relations)
416 | * @responseBody 404
417 | */
418 | public async show({ request, response }: HttpContextContract) {}
419 |
420 | /**
421 | * @update
422 | * @responseBody 200
423 | * @responseBody 404 - Product could not be found
424 | * @requestBody
425 | */
426 | public async update({ request, response }: HttpContextContract) {}
427 |
428 | /**
429 | * @myCustomFunction
430 | * @summary Lorem ipsum dolor sit amet
431 | * @paramPath provider - The login provider to be used - @enum(google, facebook, apple)
432 | * @responseBody 200 - {"token": "xxxxxxx"}
433 | * @requestBody {"code": "xxxxxx"}
434 | */
435 | public async myCustomFunction({ request, response }: HttpContextContract) {}
436 | }
437 | ```
438 |
439 | ---
440 |
441 | ## What does it do?
442 |
443 | AutoSwagger tries to extracat as much information as possible to generate swagger-docs for you.
444 |
445 | ## Paths
446 |
447 | Automatically generates swagger path-descriptions, based on your application routes. It also detects endpoints, protected by the auth-middlware.
448 |
449 | 
450 |
451 | ### Responses and RequestBody
452 |
453 | Generates responses and requestBody based on your simple Controller-Annotation (see Examples)
454 |
455 | ---
456 |
457 | ## Schemas
458 |
459 | ### Models
460 |
461 | Automatically generates swagger schema-descriptions based on your models
462 |
463 | 
464 |
465 | ### Interfaces
466 |
467 | Instead of using `param: any` you can now use custom interfaces `param: UserDetails`. The interfaces files need to be located at `app/Interfaces/`
468 |
469 | ### Enums
470 |
471 | If you use enums in your models, AutoSwagger will detect them from `app/Types/` folder and add them to the schema.
472 | If you want to add enum on ExampleValue, you can use `.append(enumFieldExample)`
473 |
474 | Example:
475 |
476 | ```ts
477 | @responseBody 200 - .with(relations).append(enumFieldExample)
478 | ```
479 |
480 | ## Extend Models
481 |
482 | Add additional documentation to your Models properties.
483 |
484 | ### SoftDelete
485 |
486 | Either use `compose(BaseModel, SoftDeletes)` or add a line `@swagger-softdeletes` to your Model.
487 |
488 | ## Attention
489 |
490 | The below comments MUST be placed **1 line** above the property.
491 |
492 | ---
493 |
494 | **@no-swagger**
495 | Although, autoswagger detects `serializeAs: null` fields automatically, and does not show them. You can use @no-swagger for other fields.
496 |
497 | **@enum(foo, bar)**
498 | If a field has defined values, you can add them into an enum. This is usesfull for something like a status field.
499 |
500 | **@format(string)**
501 | Specify a format for that field, i.e. uuid, email, binary, etc...
502 |
503 | **@example(foo bar)**
504 | Use this field to provide own example values for specific fields
505 |
506 | **@props({"minLength": 10, "foo": "bar"})**
507 | Use this field to provide additional properties to a field, like minLength, maxLength, etc. Needs to bee valid JSON.
508 |
509 | **@required**
510 | Specify that the field is required
511 |
512 | ```ts
513 | // SomeModel.js
514 | @hasMany(() => ProductView)
515 | // @no-swagger
516 | public views: HasMany
517 |
518 |
519 | @column()
520 | // @enum(pending, active, deleted)
521 | public status: string
522 |
523 | @column()
524 | // @example(johndoe@example.com)
525 | public email: string
526 |
527 | @column()
528 | // @props({"minLength": 10})
529 | public age: number
530 |
531 | ```
532 |
533 | ---
534 |
535 | ## Production environment
536 |
537 | > [!WARNING]
538 | > Make sure **NODE_ENV=production** in your production environment or whatever you set in `options.productionEnv`
539 |
540 | To make it work in production environments, additional steps are required
541 |
542 | - Create a new command for `docs:generate` [See official documentation](https://docs.adonisjs.com/guides/ace/creating-commands)
543 |
544 | - This should create a new file in `commands/DocsGenerate.ts`
545 |
546 | - Use the provided [`DocsGenerate.ts.examle`](https://github.com/ad-on-is/adonis-autoswagger/blob/main/DocsGenerate.ts.example)/[`DocsGeneratev6.ts.example`](https://github.com/ad-on-is/adonis-autoswagger/blob/main/DocsGeneratev6.ts.example) and put its contents into your newly created `DocsGenerate.ts`
547 |
548 | - Modify `/start/env.ts` as follows
549 |
550 | ```ts
551 | //...
552 | // this is necessary to make sure that the `DocsGenerate` command will run in CI/CD pipelines without setting environment variables
553 | const isNodeAce = process.argv.some(
554 | (arg) => arg.endsWith("/ace") || arg === "ace"
555 | );
556 |
557 | export default await Env.create(
558 | new URL("../", import.meta.url),
559 | isNodeAce
560 | ? {}
561 | : {
562 | // leave other settings as is
563 | NODE_ENV: Env.schema.enum([
564 | "development",
565 | "production",
566 | "test",
567 | ] as const),
568 | PORT: Env.schema.number(),
569 | }
570 | );
571 |
572 | //...
573 | ```
574 |
575 | - Execute the following
576 |
577 | ```bash
578 | node ace docs:generate
579 | node ace build --production
580 | cp swagger.yml build/
581 | ```
582 |
583 | ## Known Issues
584 |
585 | - Interfaces with objects are not working like `interface Test {foo: {bar: string}}`
586 | - Solution, just extract the object as it's own interface
587 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adonis-autoswagger",
3 | "version": "3.73.0",
4 | "description": "Auto-Generate swagger docs for AdonisJS",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "dev": "watchexec -e ts -- tsc",
10 | "build": "tsc",
11 | "prepare": "npm run build"
12 | },
13 | "author": "Adis Durakovic ",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/ad-on-is/adonis-autoswagger"
17 | },
18 | "license": "MIT",
19 | "dependencies": {
20 | "@vinejs/vine": "^2.1.0",
21 | "abstract-syntax-tree": "^2.20.5",
22 | "change-case": "^4.1.2",
23 | "espree": "^9.3.1",
24 | "extract-comments": "^1.1.0",
25 | "http-status-code": "^2.1.0",
26 | "json-to-pretty-yaml": "^1.2.2",
27 | "lodash": "^4.17.21",
28 | "parse-imports": "^1.1.2",
29 | "typescript": "^5.8.2",
30 | "typescript-parser": "^2.6.1"
31 | },
32 | "devDependencies": {
33 | "@types/lodash": "^4.14.202",
34 | "@types/node": "^20.10.4"
35 | },
36 | "files": [
37 | "/dist"
38 | ],
39 | "prettier": {
40 | "trailingComma": "es5",
41 | "semi": true,
42 | "useTabs": false,
43 | "quoteProps": "consistent",
44 | "bracketSpacing": true,
45 | "arrowParens": "always"
46 | },
47 | "packageManager": "pnpm@8.11.0"
48 | }
49 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '6.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | dependencies:
8 | '@vinejs/vine':
9 | specifier: ^2.1.0
10 | version: 2.1.0
11 | abstract-syntax-tree:
12 | specifier: ^2.20.5
13 | version: 2.20.5
14 | change-case:
15 | specifier: ^4.1.2
16 | version: 4.1.2
17 | espree:
18 | specifier: ^9.3.1
19 | version: 9.3.1
20 | extract-comments:
21 | specifier: ^1.1.0
22 | version: 1.1.0
23 | http-status-code:
24 | specifier: ^2.1.0
25 | version: 2.1.0
26 | json-to-pretty-yaml:
27 | specifier: ^1.2.2
28 | version: 1.2.2
29 | lodash:
30 | specifier: ^4.17.21
31 | version: 4.17.21
32 | parse-imports:
33 | specifier: ^1.1.2
34 | version: 1.1.2
35 | typescript:
36 | specifier: ^5.8.2
37 | version: 5.8.2
38 | typescript-parser:
39 | specifier: ^2.6.1
40 | version: 2.6.1
41 |
42 | devDependencies:
43 | '@types/lodash':
44 | specifier: ^4.14.202
45 | version: 4.14.202
46 | '@types/node':
47 | specifier: ^20.10.4
48 | version: 20.10.4
49 |
50 | packages:
51 |
52 | /@poppinss/macroable@1.0.2:
53 | resolution: {integrity: sha512-xhhEcEvhQC8mP5oOr5hbE4CmUgmw/IPV1jhpGg2xSkzoFrt9i8YVqBQt9744EFesi5F7pBheWozg63RUBM/5JA==}
54 | engines: {node: '>=18.16.0'}
55 | dev: false
56 |
57 | /@types/lodash@4.14.202:
58 | resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
59 | dev: true
60 |
61 | /@types/node@20.10.4:
62 | resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==}
63 | dependencies:
64 | undici-types: 5.26.5
65 | dev: true
66 |
67 | /@types/validator@13.11.10:
68 | resolution: {integrity: sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==}
69 | dev: false
70 |
71 | /@vinejs/compiler@2.5.0:
72 | resolution: {integrity: sha512-hg4ekaB5Y2zh+IWzBiC/WCDWrIfpVnKu/ubUvelKlidc/VbulsexoFRw5kJGHZenPVI5YzNnDeTdYSALkTV7jQ==}
73 | engines: {node: '>=18.0.0'}
74 | dev: false
75 |
76 | /@vinejs/vine@2.1.0:
77 | resolution: {integrity: sha512-09aJ2OauxpblqiNqd8qC9RAzzm5SV6fTqZhE4e25j4cM7fmNoXRTjM7Oo8llFADMO4eSA44HqYEO3mkRRYdbYw==}
78 | engines: {node: '>=18.16.0'}
79 | dependencies:
80 | '@poppinss/macroable': 1.0.2
81 | '@types/validator': 13.11.10
82 | '@vinejs/compiler': 2.5.0
83 | camelcase: 8.0.0
84 | dayjs: 1.11.11
85 | dlv: 1.1.3
86 | normalize-url: 8.0.1
87 | validator: 13.12.0
88 | dev: false
89 |
90 | /abstract-syntax-tree@2.20.5:
91 | resolution: {integrity: sha512-xxmZemmrsmzXHxdEzy9mSM3c22hCjMcLWMUbg1LQeK1FMDLHurxnMmOAN46B0NkFgembXR17D6lDctyFhVjotQ==}
92 | engines: {node: '>=14.0.0'}
93 | dependencies:
94 | ast-types: 0.14.2
95 | astring: 1.8.1
96 | esquery: 1.4.0
97 | estraverse: 5.3.0
98 | meriyah: 4.2.0
99 | source-map: 0.7.3
100 | dev: false
101 |
102 | /acorn-jsx@5.3.2(acorn@8.7.0):
103 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
104 | peerDependencies:
105 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
106 | dependencies:
107 | acorn: 8.7.0
108 | dev: false
109 |
110 | /acorn@8.7.0:
111 | resolution: {integrity: sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==}
112 | engines: {node: '>=0.4.0'}
113 | hasBin: true
114 | dev: false
115 |
116 | /ast-types@0.14.2:
117 | resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==}
118 | engines: {node: '>=4'}
119 | dependencies:
120 | tslib: 2.3.1
121 | dev: false
122 |
123 | /astring@1.8.1:
124 | resolution: {integrity: sha512-Aj3mbwVzj7Vve4I/v2JYOPFkCGM2YS7OqQTNSxmUR+LECRpokuPgAYghePgr6SALDo5bD5DlfbSaYjOzGJZOLQ==}
125 | hasBin: true
126 | dev: false
127 |
128 | /camel-case@4.1.2:
129 | resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
130 | dependencies:
131 | pascal-case: 3.1.2
132 | tslib: 2.3.1
133 | dev: false
134 |
135 | /camelcase@8.0.0:
136 | resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
137 | engines: {node: '>=16'}
138 | dev: false
139 |
140 | /capital-case@1.0.4:
141 | resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
142 | dependencies:
143 | no-case: 3.0.4
144 | tslib: 2.3.1
145 | upper-case-first: 2.0.2
146 | dev: false
147 |
148 | /change-case@4.1.2:
149 | resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==}
150 | dependencies:
151 | camel-case: 4.1.2
152 | capital-case: 1.0.4
153 | constant-case: 3.0.4
154 | dot-case: 3.0.4
155 | header-case: 2.0.4
156 | no-case: 3.0.4
157 | param-case: 3.0.4
158 | pascal-case: 3.1.2
159 | path-case: 3.0.4
160 | sentence-case: 3.0.4
161 | snake-case: 3.0.4
162 | tslib: 2.3.1
163 | dev: false
164 |
165 | /constant-case@3.0.4:
166 | resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
167 | dependencies:
168 | no-case: 3.0.4
169 | tslib: 2.3.1
170 | upper-case: 2.0.2
171 | dev: false
172 |
173 | /dayjs@1.11.11:
174 | resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==}
175 | dev: false
176 |
177 | /dlv@1.1.3:
178 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
179 | dev: false
180 |
181 | /dot-case@3.0.4:
182 | resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
183 | dependencies:
184 | no-case: 3.0.4
185 | tslib: 2.3.1
186 | dev: false
187 |
188 | /es-module-lexer@1.4.1:
189 | resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==}
190 | dev: false
191 |
192 | /eslint-visitor-keys@3.3.0:
193 | resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==}
194 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
195 | dev: false
196 |
197 | /espree@9.3.1:
198 | resolution: {integrity: sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==}
199 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
200 | dependencies:
201 | acorn: 8.7.0
202 | acorn-jsx: 5.3.2(acorn@8.7.0)
203 | eslint-visitor-keys: 3.3.0
204 | dev: false
205 |
206 | /esprima-extract-comments@1.1.0:
207 | resolution: {integrity: sha512-sBQUnvJwpeE9QnPrxh7dpI/dp67erYG4WXEAreAMoelPRpMR7NWb4YtwRPn9b+H1uLQKl/qS8WYmyaljTpjIsw==}
208 | engines: {node: '>=4'}
209 | dependencies:
210 | esprima: 4.0.1
211 | dev: false
212 |
213 | /esprima@4.0.1:
214 | resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
215 | engines: {node: '>=4'}
216 | hasBin: true
217 | dev: false
218 |
219 | /esquery@1.4.0:
220 | resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==}
221 | engines: {node: '>=0.10'}
222 | dependencies:
223 | estraverse: 5.3.0
224 | dev: false
225 |
226 | /estraverse@5.3.0:
227 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
228 | engines: {node: '>=4.0'}
229 | dev: false
230 |
231 | /extract-comments@1.1.0:
232 | resolution: {integrity: sha512-dzbZV2AdSSVW/4E7Ti5hZdHWbA+Z80RJsJhr5uiL10oyjl/gy7/o+HI1HwK4/WSZhlq4SNKU3oUzXlM13Qx02Q==}
233 | engines: {node: '>=6'}
234 | dependencies:
235 | esprima-extract-comments: 1.1.0
236 | parse-code-context: 1.0.0
237 | dev: false
238 |
239 | /header-case@2.0.4:
240 | resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
241 | dependencies:
242 | capital-case: 1.0.4
243 | tslib: 2.3.1
244 | dev: false
245 |
246 | /http-status-code@2.1.0:
247 | resolution: {integrity: sha1-Pg3IXqQ2nPTL2xvERH0rQRS9TTo=}
248 | dependencies:
249 | strip-json-comments: 1.0.4
250 | dev: false
251 |
252 | /json-to-pretty-yaml@1.2.2:
253 | resolution: {integrity: sha1-9M0L0KXo/h3yWq9boRiwmf2ZLVs=}
254 | engines: {node: '>= 0.2.0'}
255 | dependencies:
256 | remedial: 1.0.8
257 | remove-trailing-spaces: 1.0.8
258 | dev: false
259 |
260 | /lodash-es@4.17.21:
261 | resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
262 | dev: false
263 |
264 | /lodash@4.17.21:
265 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
266 | dev: false
267 |
268 | /lower-case@2.0.2:
269 | resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
270 | dependencies:
271 | tslib: 2.3.1
272 | dev: false
273 |
274 | /meriyah@4.2.0:
275 | resolution: {integrity: sha512-fCVh5GB9YT53Bq14l00HLYE3i9DywrY0JVZxbk0clXWDuMsUKKwluvC5sY0bMBqHbnIbpIjfSSIsnrzbauA8Yw==}
276 | engines: {node: '>=10.4.0'}
277 | dev: false
278 |
279 | /no-case@3.0.4:
280 | resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
281 | dependencies:
282 | lower-case: 2.0.2
283 | tslib: 2.3.1
284 | dev: false
285 |
286 | /normalize-url@8.0.1:
287 | resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
288 | engines: {node: '>=14.16'}
289 | dev: false
290 |
291 | /param-case@3.0.4:
292 | resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
293 | dependencies:
294 | dot-case: 3.0.4
295 | tslib: 2.3.1
296 | dev: false
297 |
298 | /parse-code-context@1.0.0:
299 | resolution: {integrity: sha512-OZQaqKaQnR21iqhlnPfVisFjBWjhnMl5J9MgbP8xC+EwoVqbXrq78lp+9Zb3ahmLzrIX5Us/qbvBnaS3hkH6OA==}
300 | engines: {node: '>=6'}
301 | dev: false
302 |
303 | /parse-imports@1.1.2:
304 | resolution: {integrity: sha512-UgTSNWlBvx+f4nxVSH3fOyJPJKol8GkFuG8mN8q9FqtmJgwaEx0azPRlXXX0klNlRxoP2gwme00TPDSm6rm/IA==}
305 | engines: {node: '>= 12.17'}
306 | dependencies:
307 | es-module-lexer: 1.4.1
308 | slashes: 3.0.12
309 | dev: false
310 |
311 | /pascal-case@3.1.2:
312 | resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
313 | dependencies:
314 | no-case: 3.0.4
315 | tslib: 2.3.1
316 | dev: false
317 |
318 | /path-case@3.0.4:
319 | resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==}
320 | dependencies:
321 | dot-case: 3.0.4
322 | tslib: 2.3.1
323 | dev: false
324 |
325 | /remedial@1.0.8:
326 | resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==}
327 | dev: false
328 |
329 | /remove-trailing-spaces@1.0.8:
330 | resolution: {integrity: sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==}
331 | dev: false
332 |
333 | /sentence-case@3.0.4:
334 | resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==}
335 | dependencies:
336 | no-case: 3.0.4
337 | tslib: 2.3.1
338 | upper-case-first: 2.0.2
339 | dev: false
340 |
341 | /slashes@3.0.12:
342 | resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==}
343 | dev: false
344 |
345 | /snake-case@3.0.4:
346 | resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
347 | dependencies:
348 | dot-case: 3.0.4
349 | tslib: 2.3.1
350 | dev: false
351 |
352 | /source-map@0.7.3:
353 | resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==}
354 | engines: {node: '>= 8'}
355 | dev: false
356 |
357 | /strip-json-comments@1.0.4:
358 | resolution: {integrity: sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=}
359 | engines: {node: '>=0.8.0'}
360 | hasBin: true
361 | dev: false
362 |
363 | /tslib@1.14.1:
364 | resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
365 | dev: false
366 |
367 | /tslib@2.3.1:
368 | resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==}
369 | dev: false
370 |
371 | /typescript-parser@2.6.1:
372 | resolution: {integrity: sha512-p4ZC10pu67KO8+WJALsJWhbAq4pRBIcP+ls8Bhl+V8KvzYQDwxw/P5hJhn3rBdLnfS5aGLflfh7WiZpN6yi+5g==}
373 | dependencies:
374 | lodash: 4.17.21
375 | lodash-es: 4.17.21
376 | tslib: 1.14.1
377 | typescript: 3.9.10
378 | dev: false
379 |
380 | /typescript@3.9.10:
381 | resolution: {integrity: sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==}
382 | engines: {node: '>=4.2.0'}
383 | hasBin: true
384 | dev: false
385 |
386 | /typescript@5.8.2:
387 | resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
388 | engines: {node: '>=14.17'}
389 | hasBin: true
390 | dev: false
391 |
392 | /undici-types@5.26.5:
393 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
394 | dev: true
395 |
396 | /upper-case-first@2.0.2:
397 | resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==}
398 | dependencies:
399 | tslib: 2.3.1
400 | dev: false
401 |
402 | /upper-case@2.0.2:
403 | resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==}
404 | dependencies:
405 | tslib: 2.3.1
406 | dev: false
407 |
408 | /validator@13.12.0:
409 | resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
410 | engines: {node: '>= 0.10'}
411 | dev: false
412 |
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | pnpm version minor
3 | git push && git push --tags
--------------------------------------------------------------------------------
/src/adonishelpers.ts:
--------------------------------------------------------------------------------
1 | export function serializeV6Middleware(mw: any): string[] {
2 | return [...mw.all()].reduce((result, one) => {
3 | if (typeof one === "function") {
4 | result.push(one.name || "closure");
5 | return result;
6 | }
7 |
8 | if ("name" in one && one.name) {
9 | result.push(one.name);
10 | }
11 |
12 | return result;
13 | }, []);
14 | }
15 |
16 | export async function serializeV6Handler(handler: any): Promise {
17 | /**
18 | * Value is a controller reference
19 | */
20 | if ("reference" in handler) {
21 | return {
22 | type: "controller" as const,
23 | ...(await parseBindingReference(handler.reference)),
24 | };
25 | }
26 |
27 | /**
28 | * Value is an inline closure
29 | */
30 | return {
31 | type: "closure" as const,
32 | name: handler.name || "closure",
33 | };
34 | }
35 |
36 | export async function parseBindingReference(
37 | binding: string | [any | any, any]
38 | ): Promise<{ moduleNameOrPath: string; method: string }> {
39 | const parseImports = (await import("parse-imports")).default;
40 | /**
41 | * The binding reference is a magic string. It might not have method
42 | * name attached to it. Therefore we split the string and attempt
43 | * to find the method or use the default method name "handle".
44 | */
45 | if (typeof binding === "string") {
46 | const tokens = binding.split(".");
47 | if (tokens.length === 1) {
48 | return { moduleNameOrPath: binding, method: "handle" };
49 | }
50 | return { method: tokens.pop()!, moduleNameOrPath: tokens.join(".") };
51 | }
52 |
53 | const [bindingReference, method] = binding;
54 |
55 | /**
56 | * Parsing the binding reference for dynamic imports and using its
57 | * import value.
58 | */
59 | const imports = [...(await parseImports(bindingReference.toString()))];
60 | const importedModule = imports.find(
61 | ($import) => $import.isDynamicImport && $import.moduleSpecifier.value
62 | );
63 | if (importedModule) {
64 | return {
65 | moduleNameOrPath: importedModule.moduleSpecifier.value!,
66 | method: method || "handle",
67 | };
68 | }
69 |
70 | /**
71 | * Otherwise using the name of the binding reference.
72 | */
73 | return {
74 | moduleNameOrPath: bindingReference.name,
75 | method: method || "handle",
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/src/autoswagger.ts:
--------------------------------------------------------------------------------
1 | import YAML from "json-to-pretty-yaml";
2 | import fs from "fs";
3 | import path from "path";
4 | import util from "util";
5 | import HTTPStatusCode from "http-status-code";
6 | import _ from "lodash";
7 | import { isEmpty, isUndefined } from "lodash";
8 | import { existsSync } from "fs";
9 | import { scalarCustomCss } from "./scalarCustomCss";
10 | import { serializeV6Middleware, serializeV6Handler } from "./adonishelpers";
11 | import {
12 | InterfaceParser,
13 | ModelParser,
14 | CommentParser,
15 | RouteParser,
16 | ValidatorParser,
17 | EnumParser,
18 | } from "./parsers";
19 |
20 | import type { options, AdonisRoutes, v6Handler, AdonisRoute } from "./types";
21 |
22 | import { mergeParams, formatOperationId } from "./helpers";
23 | import ExampleGenerator, { ExampleInterfaces } from "./example";
24 | // @ts-expect-error moduleResolution:nodenext issue 54523
25 | import { VineValidator } from "@vinejs/vine";
26 |
27 | export class AutoSwagger {
28 | private options: options;
29 | private schemas = {};
30 | private commentParser: CommentParser;
31 | private modelParser: ModelParser;
32 | private interfaceParser: InterfaceParser;
33 | private enumParser: EnumParser;
34 | private routeParser: RouteParser;
35 | private validatorParser: ValidatorParser;
36 | private customPaths = {};
37 |
38 | ui(url: string, options?: options) {
39 | const persistAuthString = options?.persistAuthorization
40 | ? "persistAuthorization: true,"
41 | : "";
42 | return `
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Documentation
52 |
53 |
54 |
55 |
69 |
70 | `;
71 | }
72 |
73 | rapidoc(url: string, style = "view") {
74 | return (
75 | `
76 |
77 |
78 |
79 |
80 |
81 | Documentation
82 |
83 |
84 |
111 |
112 |
113 | `
114 | );
115 | }
116 |
117 | scalar(url: string, proxyUrl: string = "https://proxy.scalar.com") {
118 | return `
119 |
120 |
121 |
122 | API Reference
123 |
124 |
127 |
130 |
131 |
132 |
136 |
137 |
138 |
139 | `;
140 | }
141 |
142 | stoplight(url: string, theme: "light" | "dark" = "dark") {
143 | return `
144 |
145 |
146 |
147 | API Documentation - Stoplight
148 |
149 |
150 |
151 |
152 |
153 |
154 |
160 |
161 |
162 | `;
163 | }
164 |
165 | jsonToYaml(json: any) {
166 | return YAML.stringify(json);
167 | }
168 |
169 | async json(routes: any, options: options) {
170 | if (process.env.NODE_ENV === (options.productionEnv || "production")) {
171 | const str = await this.readFile(options.path, "json");
172 | return JSON.parse(str);
173 | }
174 | return await this.generate(routes, options);
175 | }
176 |
177 | async writeFile(routes: any, options: options) {
178 | const json = await this.generate(routes, options);
179 | const contents = this.jsonToYaml(json);
180 | const filePath = options.path + "swagger.yml";
181 | const filePathJson = options.path + "swagger.json";
182 |
183 | fs.writeFileSync(filePath, contents);
184 | fs.writeFileSync(filePathJson, JSON.stringify(json, null, 2));
185 | }
186 |
187 | private async readFile(rootPath, type = "yml") {
188 | const filePath = rootPath + "swagger." + type;
189 | const data = fs.readFileSync(filePath, "utf-8");
190 | if (!data) {
191 | console.error("Error reading file");
192 | return;
193 | }
194 | return data;
195 | }
196 |
197 | async docs(routes: any, options: options) {
198 | if (process.env.NODE_ENV === (options.productionEnv || "production")) {
199 | return this.readFile(options.path);
200 | }
201 | return this.jsonToYaml(await this.generate(routes, options));
202 | }
203 |
204 | private async generate(adonisRoutes: AdonisRoutes, options: options) {
205 | this.options = {
206 | ...{
207 | snakeCase: true,
208 | preferredPutPatch: "PUT",
209 | debug: false,
210 | },
211 | ...options,
212 | };
213 |
214 | const routes = adonisRoutes.root;
215 | this.options.appPath = this.options.path + "app";
216 |
217 | try {
218 | const pj = fs.readFileSync(path.join(this.options.path, "package.json"));
219 |
220 | const pjson = JSON.parse(pj.toString());
221 | if (pjson.imports) {
222 | Object.entries(pjson.imports).forEach(([key, value]) => {
223 | const k = (key as string).replaceAll("/*", "");
224 | this.customPaths[k] = (value as string)
225 | .replaceAll("/*.js", "")
226 | .replaceAll("./", "");
227 | });
228 | }
229 | } catch (e) {
230 | console.error(e);
231 | }
232 |
233 | this.commentParser = new CommentParser(this.options);
234 | this.routeParser = new RouteParser(this.options);
235 | this.modelParser = new ModelParser(this.options.snakeCase);
236 | this.interfaceParser = new InterfaceParser(this.options.snakeCase);
237 | this.validatorParser = new ValidatorParser();
238 | this.enumParser = new EnumParser();
239 | this.schemas = await this.getSchemas();
240 | if (this.options.debug) {
241 | console.log(this.options);
242 | console.log("Found Schemas", Object.keys(this.schemas));
243 | console.log("Using custom paths", this.customPaths);
244 | }
245 | this.commentParser.exampleGenerator = new ExampleGenerator(this.schemas);
246 |
247 | const docs = {
248 | openapi: "3.0.0",
249 | info: options.info || {
250 | title: options.title,
251 | version: options.version,
252 | description:
253 | options.description ||
254 | "Generated by AdonisJS AutoSwagger https://github.com/ad-on-is/adonis-autoswagger",
255 | },
256 |
257 | components: {
258 | responses: {
259 | Forbidden: {
260 | description: "Access token is missing or invalid",
261 | },
262 | Accepted: {
263 | description: "The request was accepted",
264 | },
265 | Created: {
266 | description: "The resource has been created",
267 | },
268 | NotFound: {
269 | description: "The resource has been created",
270 | },
271 | NotAcceptable: {
272 | description: "The resource has been created",
273 | },
274 | },
275 | securitySchemes: {
276 | BearerAuth: {
277 | type: "http",
278 | scheme: "bearer",
279 | },
280 | BasicAuth: {
281 | type: "http",
282 | scheme: "basic",
283 | },
284 | ApiKeyAuth: {
285 | type: "apiKey",
286 | in: "header",
287 | name: "X-API-Key",
288 | },
289 | ...this.options.securitySchemes,
290 | },
291 | schemas: this.schemas,
292 | },
293 | paths: {},
294 | tags: [],
295 | };
296 | let paths = {};
297 |
298 | let sscheme = "BearerAuth";
299 | if (this.options.defaultSecurityScheme) {
300 | sscheme = this.options.defaultSecurityScheme;
301 | }
302 |
303 | let securities = {
304 | "auth": { [sscheme]: ["access"] },
305 | "auth:api": { [sscheme]: ["access"] },
306 | ...this.options.authMiddlewares
307 | ?.map((am) => ({
308 | [am]: { [sscheme]: ["access"] },
309 | }))
310 | .reduce((acc, val) => ({ ...acc, ...val }), {}),
311 | };
312 |
313 | let globalTags = [];
314 |
315 | if (this.options.debug) {
316 | console.log("Route annotations:");
317 | console.log("Checking if controllers have propper comment annotations");
318 | console.log("-----");
319 | }
320 |
321 | for await (const route of routes) {
322 | let ignore = false;
323 | for (const i of options.ignore) {
324 | if (
325 | route.pattern == i ||
326 | (i.endsWith("*") && route.pattern.startsWith(i.slice(0, -1))) ||
327 | (i.startsWith("*") && route.pattern.endsWith(i.slice(1)))
328 | ) {
329 | ignore = true;
330 | break;
331 | }
332 | }
333 | if (ignore) continue;
334 |
335 | let security = [];
336 | const responseCodes = {
337 | GET: "200",
338 | POST: "201",
339 | DELETE: "202",
340 | PUT: "204",
341 | };
342 |
343 | if (!Array.isArray(route.middleware)) {
344 | route.middleware = serializeV6Middleware(route.middleware) as string[];
345 | }
346 |
347 | (route.middleware as string[]).forEach((m) => {
348 | if (typeof securities[m] !== "undefined") {
349 | security.push(securities[m]);
350 | }
351 | });
352 |
353 | let { tags, parameters, pattern } = this.routeParser.extractInfos(
354 | route.pattern
355 | );
356 |
357 | tags.forEach((tag) => {
358 | if (globalTags.filter((e) => e.name === tag).length > 0) return;
359 | if (tag === "") return;
360 | globalTags.push({
361 | name: tag,
362 | description: "Everything related to " + tag,
363 | });
364 | });
365 |
366 | const { sourceFile, action, customAnnotations, operationId } =
367 | await this.getDataBasedOnAdonisVersion(route);
368 |
369 | route.methods.forEach((method) => {
370 | let responses = {};
371 | if (method === "HEAD") return;
372 |
373 | if (
374 | route.methods.includes("PUT") &&
375 | route.methods.includes("PATCH") &&
376 | method !== this.options.preferredPutPatch
377 | )
378 | return;
379 |
380 | let description = "";
381 | let summary = "";
382 | let tag = "";
383 | let operationId: string;
384 |
385 | if (security.length > 0) {
386 | responses["401"] = {
387 | description: `Returns **401** (${HTTPStatusCode.getMessage(401)})`,
388 | };
389 | responses["403"] = {
390 | description: `Returns **403** (${HTTPStatusCode.getMessage(403)})`,
391 | };
392 | }
393 |
394 | let requestBody = {
395 | content: {
396 | "application/json": {},
397 | },
398 | };
399 |
400 | let actionParams = {};
401 |
402 | if (action !== "" && typeof customAnnotations[action] !== "undefined") {
403 | description = customAnnotations[action].description;
404 | summary = customAnnotations[action].summary;
405 | operationId = customAnnotations[action].operationId;
406 | responses = { ...responses, ...customAnnotations[action].responses };
407 | requestBody = customAnnotations[action].requestBody;
408 | actionParams = customAnnotations[action].parameters;
409 | tag = customAnnotations[action].tag;
410 | }
411 | parameters = mergeParams(parameters, actionParams);
412 |
413 | if (tag != "") {
414 | globalTags.push({
415 | name: tag.toUpperCase(),
416 | description: "Everything related to " + tag.toUpperCase(),
417 | });
418 | tags = [tag.toUpperCase()];
419 | }
420 |
421 | if (isEmpty(responses)) {
422 | responses[responseCodes[method]] = {
423 | description: HTTPStatusCode.getMessage(responseCodes[method]),
424 | content: {
425 | "application/json": {},
426 | },
427 | };
428 | } else {
429 | if (
430 | typeof responses[responseCodes[method]] !== "undefined" &&
431 | typeof responses[responseCodes[method]]["summary"] !== "undefined"
432 | ) {
433 | if (summary === "") {
434 | summary = responses[responseCodes[method]]["summary"];
435 | }
436 | delete responses[responseCodes[method]]["summary"];
437 | }
438 | if (
439 | typeof responses[responseCodes[method]] !== "undefined" &&
440 | typeof responses[responseCodes[method]]["description"] !==
441 | "undefined"
442 | ) {
443 | description = responses[responseCodes[method]]["description"];
444 | }
445 | }
446 |
447 | if (action !== "" && summary === "") {
448 | // Solve toLowerCase undefined exception
449 | // https://github.com/ad-on-is/adonis-autoswagger/issues/28
450 | tags[0] = tags[0] ?? "";
451 |
452 | switch (action) {
453 | case "index":
454 | summary = "Get a list of " + tags[0].toLowerCase();
455 | break;
456 | case "show":
457 | summary = "Get a single instance of " + tags[0].toLowerCase();
458 | break;
459 | case "update":
460 | summary = "Update " + tags[0].toLowerCase();
461 | break;
462 | case "destroy":
463 | summary = "Delete " + tags[0].toLowerCase();
464 | break;
465 | case "store":
466 | summary = "Create " + tags[0].toLowerCase();
467 | break;
468 | // frontend defaults
469 | case "create":
470 | summary = "Create (Frontend) " + tags[0].toLowerCase();
471 | break;
472 | case "edit":
473 | summary = "Update (Frontend) " + tags[0].toLowerCase();
474 | break;
475 | }
476 | }
477 |
478 | const sf = sourceFile.split("/").at(-1).replace(".ts", "");
479 | let m = {
480 | summary: `${summary}${action !== "" ? ` (${action})` : "route"}`,
481 | description:
482 | description + "\n\n _" + sourceFile + "_ - **" + action + "**",
483 | operationId: operationId,
484 | parameters: parameters,
485 | tags: tags,
486 | responses: responses,
487 | security: security,
488 | };
489 |
490 | if (method !== "GET" && method !== "DELETE") {
491 | m["requestBody"] = requestBody;
492 | }
493 |
494 | pattern = pattern.slice(1);
495 | if (pattern === "") {
496 | pattern = "/";
497 | }
498 |
499 | paths = {
500 | ...paths,
501 | [pattern]: { ...paths[pattern], [method.toLowerCase()]: m },
502 | };
503 | });
504 | }
505 |
506 | // filter unused tags
507 | const usedTags = _.uniq(
508 | Object.entries(paths)
509 | .map(([p, val]) => Object.entries(val)[0][1].tags)
510 | .flat()
511 | );
512 |
513 | docs.tags = globalTags.filter((tag) => usedTags.includes(tag.name));
514 | docs.paths = paths;
515 | return docs;
516 | }
517 |
518 | private async getDataBasedOnAdonisVersion(route: AdonisRoute) {
519 | let sourceFile = "";
520 | let action = "";
521 | let customAnnotations;
522 | let operationId = "";
523 | if (
524 | route.meta.resolvedHandler !== null &&
525 | route.meta.resolvedHandler !== undefined
526 | ) {
527 | if (
528 | typeof route.meta.resolvedHandler.namespace !== "undefined" &&
529 | route.meta.resolvedHandler.method !== "handle"
530 | ) {
531 | sourceFile = route.meta.resolvedHandler.namespace;
532 |
533 | action = route.meta.resolvedHandler.method;
534 | // If not defined by an annotation, use the combination of "controllerNameMethodName"
535 | if (action !== "" && isUndefined(operationId) && route.handler) {
536 | operationId = formatOperationId(route.handler as string);
537 | }
538 | }
539 | }
540 |
541 | let v6handler = route.handler;
542 | if (
543 | v6handler.reference !== null &&
544 | v6handler.reference !== undefined &&
545 | v6handler.reference !== ""
546 | ) {
547 | if (!Array.isArray(v6handler.reference)) {
548 | // handles magic strings
549 | // router.resource('/test', '#controllers/test_controller')
550 | [sourceFile, action] = v6handler.reference.split(".");
551 | const split = sourceFile.split("/");
552 |
553 | if (split[0].includes("#")) {
554 | sourceFile = sourceFile.replaceAll(
555 | split[0],
556 | this.customPaths[split[0]]
557 | );
558 | } else {
559 | sourceFile = this.options.appPath + "/controllers/" + sourceFile;
560 | }
561 | operationId = formatOperationId(v6handler.reference);
562 | } else {
563 | // handles lazy import
564 | // const TestController = () => import('#controllers/test_controller')
565 | v6handler = await serializeV6Handler(v6handler);
566 | action = v6handler.method;
567 | sourceFile = v6handler.moduleNameOrPath;
568 | operationId = formatOperationId(sourceFile + "." + action);
569 | const split = sourceFile.split("/");
570 | if (split[0].includes("#")) {
571 | sourceFile = sourceFile.replaceAll(
572 | split[0],
573 | this.customPaths[split[0]]
574 | );
575 | } else {
576 | sourceFile = this.options.appPath + "/" + sourceFile;
577 | }
578 | }
579 | }
580 |
581 | if (sourceFile !== "" && action !== "") {
582 | sourceFile = sourceFile.replace("App/", "app/") + ".ts";
583 | sourceFile = sourceFile.replace(".js", "");
584 |
585 | customAnnotations = await this.commentParser.getAnnotations(
586 | sourceFile,
587 | action
588 | );
589 | }
590 | if (
591 | typeof customAnnotations !== "undefined" &&
592 | typeof customAnnotations.operationId !== "undefined" &&
593 | customAnnotations.operationId !== ""
594 | ) {
595 | operationId = customAnnotations.operationId;
596 | }
597 | if (this.options.debug) {
598 | if (sourceFile !== "") {
599 | console.log(
600 | typeof customAnnotations !== "undefined" &&
601 | !_.isEmpty(customAnnotations)
602 | ? `\x1b[32m✓ FOUND for ${action}\x1b[0m`
603 | : `\x1b[33m✗ MISSING for ${action}\x1b[0m`,
604 |
605 | `${sourceFile} (${route.methods[0].toUpperCase()} ${route.pattern})`
606 | );
607 | }
608 | }
609 | return { sourceFile, action, customAnnotations, operationId };
610 | }
611 |
612 | private async getSchemas() {
613 | let schemas = {
614 | Any: {
615 | description: "Any JSON object not defined as schema",
616 | },
617 | };
618 |
619 | schemas = {
620 | ...schemas,
621 | ...(await this.getInterfaces()),
622 | ...(await this.getSerializers()),
623 | ...(await this.getModels()),
624 | ...(await this.getValidators()),
625 | ...(await this.getEnums()),
626 | };
627 |
628 | return schemas;
629 | }
630 |
631 | private async getValidators() {
632 | const validators = {};
633 | let p6 = path.join(this.options.appPath, "validators");
634 |
635 | if (typeof this.customPaths["#validators"] !== "undefined") {
636 | // it's v6
637 | p6 = p6.replaceAll("app/validators", this.customPaths["#validators"]);
638 | p6 = p6.replaceAll("app\\validators", this.customPaths["#validators"]);
639 | }
640 |
641 | if (!existsSync(p6)) {
642 | if (this.options.debug) {
643 | console.log("Validators paths don't exist", p6);
644 | }
645 | return validators;
646 | }
647 |
648 | const files = await this.getFiles(p6, []);
649 | if (this.options.debug) {
650 | console.log("Found validator files", files);
651 | }
652 |
653 | try {
654 | for (let file of files) {
655 | if (/^[a-zA-Z]:/.test(file)) {
656 | file = "file:///" + file;
657 | }
658 |
659 | const val = await import(file);
660 | for (const [key, value] of Object.entries(val)) {
661 | if (value.constructor.name.includes("VineValidator")) {
662 | validators[key] = await this.validatorParser.validatorToObject(
663 | value as VineValidator
664 | );
665 | validators[key].description = key + " (Validator)";
666 | }
667 | }
668 | }
669 | } catch (e) {
670 | console.log(
671 | "**You are probably using 'node ace serve --hmr', which is not supported yet. Use 'node ace serve --watch' instead.**"
672 | );
673 | console.error(e.message);
674 | }
675 |
676 | return validators;
677 | }
678 |
679 | private async getSerializers() {
680 | const serializers = {};
681 | let p6 = path.join(this.options.appPath, "serializers");
682 |
683 | if (typeof this.customPaths["#serializers"] !== "undefined") {
684 | // it's v6
685 | p6 = p6.replaceAll("app/serializers", this.customPaths["#serializers"]);
686 | p6 = p6.replaceAll("app\\serializers", this.customPaths["#serializers"]);
687 | }
688 |
689 | if (!existsSync(p6)) {
690 | if (this.options.debug) {
691 | console.log("Serializers paths don't exist", p6);
692 | }
693 | return serializers;
694 | }
695 |
696 | const files = await this.getFiles(p6, []);
697 | if (this.options.debug) {
698 | console.log("Found serializer files", files);
699 | }
700 |
701 | for (let file of files) {
702 | if (/^[a-zA-Z]:/.test(file)) {
703 | file = "file:///" + file;
704 | }
705 |
706 | const val = await import(file);
707 |
708 | for (const [key, value] of Object.entries(val)) {
709 | if (key.indexOf("Serializer") > -1) {
710 | serializers[key] = value;
711 | }
712 | }
713 | }
714 |
715 | return serializers;
716 | }
717 |
718 | private async getModels() {
719 | const models = {};
720 | let p = path.join(this.options.appPath, "Models");
721 | let p6 = path.join(this.options.appPath, "models");
722 |
723 | if (typeof this.customPaths["#models"] !== "undefined") {
724 | // it's v6
725 | p6 = p6.replaceAll("app/models", this.customPaths["#models"]);
726 | p6 = p6.replaceAll("app\\models", this.customPaths["#models"]);
727 | }
728 |
729 | if (!existsSync(p) && !existsSync(p6)) {
730 | if (this.options.debug) {
731 | console.log("Model paths don't exist", p, p6);
732 | }
733 | return models;
734 | }
735 | if (existsSync(p6)) {
736 | p = p6;
737 | }
738 | const files = await this.getFiles(p, []);
739 | const readFile = util.promisify(fs.readFile);
740 | if (this.options.debug) {
741 | console.log("Found model files", files);
742 | }
743 | for (let file of files) {
744 | file = file.replace(".js", "");
745 | const data = await readFile(file, "utf8");
746 | file = file.replace(".ts", "");
747 | const split = file.split("/");
748 | let name = split[split.length - 1].replace(".ts", "");
749 | file = file.replace("app/", "/app/");
750 | const parsed = this.modelParser.parseModelProperties(data);
751 | if (parsed.name !== "") {
752 | name = parsed.name;
753 | }
754 | let schema = {
755 | type: "object",
756 | required: parsed.required,
757 | properties: parsed.props,
758 | description: name + " (Model)",
759 | };
760 | models[name] = schema;
761 | }
762 | return models;
763 | }
764 |
765 | private async getInterfaces() {
766 | let interfaces = {
767 | ...ExampleInterfaces.paginationInterface(),
768 | };
769 | let p = path.join(this.options.appPath, "Interfaces");
770 | let p6 = path.join(this.options.appPath, "interfaces");
771 |
772 | if (typeof this.customPaths["#interfaces"] !== "undefined") {
773 | // it's v6
774 | p6 = p6.replaceAll("app/interfaces", this.customPaths["#interfaces"]);
775 | p6 = p6.replaceAll("app\\interfaces", this.customPaths["#interfaces"]);
776 | }
777 |
778 | if (!existsSync(p) && !existsSync(p6)) {
779 | if (this.options.debug) {
780 | console.log("Interface paths don't exist", p, p6);
781 | }
782 | return interfaces;
783 | }
784 | if (existsSync(p6)) {
785 | p = p6;
786 | }
787 | const files = await this.getFiles(p, []);
788 | if (this.options.debug) {
789 | console.log("Found interfaces files", files);
790 | }
791 | const readFile = util.promisify(fs.readFile);
792 | for (let file of files) {
793 | file = file.replace(".js", "");
794 | const data = await readFile(file, "utf8");
795 | file = file.replace(".ts", "");
796 | interfaces = {
797 | ...interfaces,
798 | ...this.interfaceParser.parseInterfaces(data),
799 | };
800 | }
801 |
802 | return interfaces;
803 | }
804 |
805 | private async getFiles(dir, files_) {
806 | const fs = require("fs");
807 | files_ = files_ || [];
808 | var files = await fs.readdirSync(dir);
809 | for (let i in files) {
810 | var name = dir + "/" + files[i];
811 | if (fs.statSync(name).isDirectory()) {
812 | await this.getFiles(name, files_);
813 | } else {
814 | files_.push(name);
815 | }
816 | }
817 | return files_;
818 | }
819 |
820 | private async getEnums() {
821 | let enums = {};
822 |
823 | const enumParser = new EnumParser();
824 |
825 | let p = path.join(this.options.appPath, "Types");
826 | let p6 = path.join(this.options.appPath, "types");
827 |
828 | if (typeof this.customPaths["#types"] !== "undefined") {
829 | // it's v6
830 | p6 = p6.replaceAll("app/types", this.customPaths["#types"]);
831 | p6 = p6.replaceAll("app\\types", this.customPaths["#types"]);
832 | }
833 |
834 | if (!existsSync(p) && !existsSync(p6)) {
835 | if (this.options.debug) {
836 | console.log("Enum paths don't exist", p, p6);
837 | }
838 | return enums;
839 | }
840 |
841 | if (existsSync(p6)) {
842 | p = p6;
843 | }
844 |
845 | const files = await this.getFiles(p, []);
846 | if (this.options.debug) {
847 | console.log("Found enum files", files);
848 | }
849 |
850 | const readFile = util.promisify(fs.readFile);
851 | for (let file of files) {
852 | file = file.replace(".js", "");
853 | const data = await readFile(file, "utf8");
854 | file = file.replace(".ts", "");
855 | const split = file.split("/");
856 | const name = split[split.length - 1].replace(".ts", "");
857 | file = file.replace("app/", "/app/");
858 |
859 | const parsedEnums = enumParser.parseEnums(data);
860 | enums = {
861 | ...enums,
862 | ...parsedEnums,
863 | };
864 | }
865 |
866 | return enums;
867 | }
868 | }
869 |
--------------------------------------------------------------------------------
/src/example.ts:
--------------------------------------------------------------------------------
1 | import { snakeCase } from "lodash";
2 | import { getBetweenBrackets } from "./helpers";
3 | export default class ExampleGenerator {
4 | public schemas = {};
5 | constructor(schemas: any) {
6 | this.schemas = schemas;
7 | }
8 |
9 | jsonToRef(json) {
10 | const jsonObjectIsArray = Array.isArray(json);
11 | let out = {};
12 | let outArr = [];
13 | for (let [k, v] of Object.entries(json)) {
14 | if (typeof v === "object") {
15 | if (!Array.isArray(v)) {
16 | v = this.jsonToRef(v);
17 | }
18 | }
19 | if (typeof v === "string") {
20 | v = this.parseRef(v, true);
21 | }
22 |
23 | if (jsonObjectIsArray) {
24 | outArr.push(v);
25 | } else {
26 | out[k] = v;
27 | }
28 | }
29 | return outArr.length > 0 ? outArr.flat() : out;
30 | }
31 |
32 | parseRef(line: string, exampleOnly = false) {
33 | let rawRef = line.substring(line.indexOf("<") + 1, line.lastIndexOf(">"));
34 |
35 | if (rawRef === "") {
36 | if (exampleOnly) {
37 | return line;
38 | }
39 | // No format valid, returning the line as text/plain
40 | return {
41 | content: {
42 | "text/plain": {
43 | example: line,
44 | },
45 | },
46 | };
47 | }
48 |
49 | let inc = getBetweenBrackets(line, "with");
50 | let exc = getBetweenBrackets(line, "exclude");
51 | const append = getBetweenBrackets(line, "append");
52 | let only = getBetweenBrackets(line, "only");
53 | const paginated = getBetweenBrackets(line, "paginated");
54 | const serializer = getBetweenBrackets(line, "serialized");
55 |
56 | if (serializer) {
57 | // we override to be sure
58 | inc = "";
59 | exc = "";
60 | only = "";
61 |
62 | if (this.schemas[serializer].fields.pick) {
63 | only += this.schemas[serializer].fields.pick.join(",");
64 | }
65 | if (this.schemas[serializer].fields.omit) {
66 | exc += this.schemas[serializer].fields.omit.join(",");
67 | }
68 | if (this.schemas[serializer].relations) {
69 | // get relations names and add them to inc
70 | const relations = Object.keys(this.schemas[serializer].relations);
71 | inc = relations.join(",");
72 |
73 | // we need to add the relation name to only and also we add the relation fields we want to only
74 | // ex : comment,comment.id,comment.createdAt
75 | relations.forEach((relation) => {
76 | const relationFields = this.schemas[serializer].relations[
77 | relation
78 | ].map((field) => relation + "." + field);
79 |
80 | only += "," + relation + "," + relationFields.join(",");
81 | });
82 | }
83 | }
84 |
85 | let app = {};
86 | try {
87 | app = JSON.parse("{" + append + "}");
88 | } catch {}
89 |
90 | const cleanedRef = rawRef.replace("[]", "");
91 |
92 | let ex = {};
93 | try {
94 | ex = Object.assign(
95 | this.getSchemaExampleBasedOnAnnotation(cleanedRef, inc, exc, only),
96 | app
97 | );
98 | } catch (e) {
99 | console.error("Error", cleanedRef);
100 | }
101 |
102 | const { dataName, metaName } = this.getPaginatedData(line);
103 |
104 | const paginatedEx = {
105 | [dataName]: [ex],
106 | [metaName]: this.getSchemaExampleBasedOnAnnotation("PaginationMeta"),
107 | };
108 |
109 | const paginatedSchema = {
110 | type: "object",
111 | properties: {
112 | [dataName]: {
113 | type: "array",
114 | items: { $ref: "#/components/schemas/" + cleanedRef },
115 | },
116 | [metaName]: { $ref: "#/components/schemas/PaginationMeta" },
117 | },
118 | };
119 |
120 | const normalArraySchema = {
121 | type: "array",
122 | items: { $ref: "#/components/schemas/" + cleanedRef },
123 | };
124 |
125 | if (rawRef.includes("[]")) {
126 | if (exampleOnly) {
127 | return paginated === "true" ? paginatedEx : [ex];
128 | }
129 | return {
130 | content: {
131 | "application/json": {
132 | schema: paginated === "true" ? paginatedSchema : normalArraySchema,
133 | example: paginated === "true" ? paginatedEx : [ex],
134 | },
135 | },
136 | };
137 | }
138 | if (exampleOnly) {
139 | return ex;
140 | }
141 |
142 | return {
143 | content: {
144 | "application/json": {
145 | schema: {
146 | $ref: "#/components/schemas/" + rawRef,
147 | },
148 | example: ex,
149 | },
150 | },
151 | };
152 | }
153 |
154 | exampleByValidatorRule(rule: string) {
155 | switch (rule) {
156 | case "email":
157 | return "user@example.com";
158 | default:
159 | return "Some string";
160 | }
161 | }
162 |
163 | getSchemaExampleBasedOnAnnotation(
164 | schema: string,
165 | inc = "",
166 | exc = "",
167 | onl = "",
168 | first = "",
169 | parent = "",
170 | deepRels = [""]
171 | ) {
172 | let props = {};
173 | if (!this.schemas[schema]) {
174 | return props;
175 | }
176 | if (this.schemas[schema].example) {
177 | return this.schemas[schema].example;
178 | }
179 |
180 | let properties = this.schemas[schema].properties;
181 | let include = inc.toString().split(",");
182 | let exclude = exc.toString().split(",");
183 | let only = onl.toString().split(",");
184 | only = only.length === 1 && only[0] === "" ? [] : only;
185 |
186 | if (typeof properties === "undefined") return null;
187 |
188 | // skip nested if not requested
189 | if (
190 | parent !== "" &&
191 | schema !== "" &&
192 | parent.includes(".") &&
193 | this.schemas[schema].description.includes("Model") &&
194 | !inc.includes("relations") &&
195 | !inc.includes(parent) &&
196 | !inc.includes(parent + ".relations") &&
197 | !inc.includes(first + ".relations")
198 | ) {
199 | return null;
200 | }
201 |
202 | deepRels.push(schema);
203 |
204 | for (const [key, value] of Object.entries(properties)) {
205 | let isArray = false;
206 | if (exclude.includes(key)) continue;
207 | if (exclude.includes(parent + "." + key)) continue;
208 |
209 | if (
210 | key === "password" &&
211 | !include.includes("password") &&
212 | !only.includes("password")
213 | )
214 | continue;
215 | if (
216 | key === "password_confirmation" &&
217 | !include.includes("password_confirmation") &&
218 | !only.includes("password_confirmation")
219 | )
220 | continue;
221 | if (
222 | (key === "created_at" ||
223 | key === "updated_at" ||
224 | key === "deleted_at") &&
225 | exc.includes("timestamps")
226 | )
227 | continue;
228 |
229 | let rel = "";
230 | let example = value["example"];
231 |
232 | if (parent === "" && only.length > 0 && !only.includes(key)) continue;
233 |
234 | // for relations we can select the fields we want with this syntax
235 | // ex : comment.id,comment.createdAt
236 | if (
237 | parent !== "" &&
238 | only.length > 0 &&
239 | !only.includes(parent + "." + key)
240 | )
241 | continue;
242 |
243 | if (typeof value["$ref"] !== "undefined") {
244 | rel = value["$ref"].replace("#/components/schemas/", "");
245 | }
246 |
247 | if (
248 | typeof value["items"] !== "undefined" &&
249 | typeof value["items"]["$ref"] !== "undefined"
250 | ) {
251 | rel = value["items"]["$ref"].replace("#/components/schemas/", "");
252 | }
253 |
254 | if (typeof value["items"] !== "undefined") {
255 | isArray = true;
256 | example = value["items"]["example"];
257 | }
258 |
259 | if (rel !== "") {
260 | // skip related models of main schema
261 | if (
262 | parent === "" &&
263 | typeof this.schemas[rel] !== "undefined" &&
264 | this.schemas[rel].description?.includes("Model") &&
265 | !include.includes("relations") &&
266 | !include.includes(key)
267 | ) {
268 | continue;
269 | }
270 |
271 | if (
272 | parent !== "" &&
273 | !include.includes(parent + ".relations") &&
274 | !include.includes(parent + "." + key)
275 | ) {
276 | continue;
277 | }
278 |
279 | if (
280 | typeof value["items"] !== "undefined" &&
281 | typeof value["items"]["$ref"] !== "undefined"
282 | ) {
283 | rel = value["items"]["$ref"].replace("#/components/schemas/", "");
284 | }
285 | if (rel == "") {
286 | return;
287 | }
288 |
289 | let propdata: any = "";
290 |
291 | // if (!deepRels.includes(rel)) {
292 | // deepRels.push(rel);
293 | propdata = this.getSchemaExampleBasedOnAnnotation(
294 | rel,
295 | inc,
296 | exc,
297 | onl,
298 | parent,
299 | parent === "" ? key : parent + "." + key,
300 | deepRels
301 | );
302 |
303 | if (propdata === null) {
304 | continue;
305 | }
306 |
307 | props[key] = isArray ? [propdata] : propdata;
308 | } else {
309 | props[key] = isArray ? [example] : example;
310 | }
311 | }
312 |
313 | return props;
314 | }
315 |
316 | exampleByType(type) {
317 | switch (type) {
318 | case "string":
319 | return this.exampleByField("title");
320 | case "number":
321 | return Math.floor(Math.random() * 1000);
322 | case "integer":
323 | return Math.floor(Math.random() * 1000);
324 | case "boolean":
325 | return true;
326 | case "DateTime":
327 | return this.exampleByField("datetime");
328 | case "datetime":
329 | return this.exampleByField("datetime");
330 | case "date":
331 | return this.exampleByField("date");
332 | case "object":
333 | return {};
334 | default:
335 | return null;
336 | }
337 | }
338 |
339 | exampleByField(field, type: string = "") {
340 | const ex = {
341 | datetime: "2021-03-23T16:13:08.489+01:00",
342 | DateTime: "2021-03-23T16:13:08.489+01:00",
343 | date: "2021-03-23",
344 | title: "Lorem Ipsum",
345 | year: 2023,
346 | description: "Lorem ipsum dolor sit amet",
347 | name: "John Doe",
348 | full_name: "John Doe",
349 | first_name: "John",
350 | last_name: "Doe",
351 | email: "johndoe@example.com",
352 | address: "1028 Farland Street",
353 | street: "1028 Farland Street",
354 | country: "United States of America",
355 | country_code: "US",
356 | zip: 60617,
357 | city: "Chicago",
358 | password: "S3cur3P4s5word!",
359 | password_confirmation: "S3cur3P4s5word!",
360 | lat: 41.705,
361 | long: -87.475,
362 | price: 10.5,
363 | avatar: "https://example.com/avatar.png",
364 | url: "https://example.com",
365 | };
366 | if (typeof ex[field] !== "undefined") {
367 | return ex[field];
368 | }
369 | if (typeof ex[snakeCase(field)] !== "undefined") {
370 | return ex[snakeCase(field)];
371 | }
372 | return null;
373 | }
374 |
375 | getPaginatedData(line: string): { dataName: string; metaName: string } {
376 | const match = line.match(/<.*>\.paginated\((.*)\)/);
377 | if (!match) {
378 | return { dataName: "data", metaName: "meta" };
379 | }
380 |
381 | const params = match[1].split(",").map((s) => s.trim());
382 | const dataName = params[0] || "data";
383 | const metaName = params[1] || "meta";
384 |
385 | return { dataName, metaName };
386 | }
387 | }
388 |
389 | export abstract class ExampleInterfaces {
390 | public static paginationInterface() {
391 | return {
392 | PaginationMeta: {
393 | type: "object",
394 | properties: {
395 | total: { type: "number", example: 100, nullable: false },
396 | page: { type: "number", example: 2, nullable: false },
397 | perPage: { type: "number", example: 10, nullable: false },
398 | currentPage: { type: "number", example: 3, nullable: false },
399 | lastPage: { type: "number", example: 10, nullable: false },
400 | firstPage: { type: "number", example: 1, nullable: false },
401 | lastPageUrl: {
402 | type: "string",
403 | example: "/?page=10",
404 | nullable: false,
405 | },
406 | firstPageUrl: {
407 | type: "string",
408 | example: "/?page=1",
409 | nullable: false,
410 | },
411 | nextPageUrl: { type: "string", example: "/?page=6", nullable: false },
412 | previousPageUrl: {
413 | type: "string",
414 | example: "/?page=5",
415 | nullable: false,
416 | },
417 | },
418 | },
419 | };
420 | }
421 | }
422 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if a string is a valid JSON
3 | */
4 | import { camelCase, isEmpty, isUndefined, snakeCase, startCase } from "lodash";
5 | export function isJSONString(str: string): boolean {
6 | try {
7 | JSON.parse(str);
8 | return true;
9 | } catch (error) {
10 | return false;
11 | }
12 | }
13 |
14 | export function getBetweenBrackets(value: string, start: string) {
15 | let match = value.match(new RegExp(start + "\\(([^()]*)\\)", "g"));
16 |
17 | if (match !== null) {
18 | let m = match[0].replace(start + "(", "").replace(")", "");
19 |
20 | if (start !== "example") {
21 | m = m.replace(/ /g, "");
22 | }
23 | if (start === "paginated") {
24 | return "true";
25 | }
26 | return m;
27 | }
28 |
29 | return "";
30 | }
31 |
32 | export function mergeParams(initial, custom) {
33 | let merge = Object.assign(initial, custom);
34 | let params = [];
35 | for (const [key, value] of Object.entries(merge)) {
36 | params.push(value);
37 | }
38 |
39 | return params;
40 | }
41 |
42 | /**
43 | * Helpers
44 | */
45 |
46 | export function formatOperationId(inputString: string): string {
47 | // Remove non-alphanumeric characters and split the string into words
48 | const cleanedWords = inputString.replace(/[^a-zA-Z0-9]/g, " ").split(" ");
49 |
50 | // Pascal casing words
51 | const pascalCasedWords = cleanedWords.map((word) =>
52 | startCase(camelCase(word))
53 | );
54 |
55 | // Generate operationId by joining every parts
56 | const operationId = pascalCasedWords.join();
57 |
58 | // CamelCase the operationId
59 | return camelCase(operationId);
60 | }
61 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { AutoSwagger } from "./autoswagger";
2 | export default new AutoSwagger();
3 |
--------------------------------------------------------------------------------
/src/parsers.ts:
--------------------------------------------------------------------------------
1 | import HTTPStatusCode from "http-status-code";
2 | import { isJSONString, getBetweenBrackets } from "./helpers";
3 | import util from "util";
4 | import extract from "extract-comments";
5 | import fs from "fs";
6 | import {
7 | camelCase,
8 | isEmpty,
9 | isUndefined,
10 | max,
11 | min,
12 | snakeCase,
13 | startCase,
14 | } from "lodash";
15 | import ExampleGenerator from "./example";
16 | import type { options, AdonisRoutes, v6Handler } from "./types";
17 | import { standardTypes } from "./types";
18 | import _ from "lodash";
19 | // @ts-expect-error moduleResolution:nodenext issue 54523
20 | import { VineValidator } from "@vinejs/vine";
21 |
22 | export class CommentParser {
23 | private parsedFiles: { [file: string]: string } = {};
24 | public exampleGenerator: ExampleGenerator;
25 |
26 | options: options;
27 |
28 | constructor(options: options) {
29 | this.options = options;
30 | }
31 |
32 | private parseAnnotations(lines: string[]) {
33 | let summary = "";
34 | let tag = "";
35 | let description = "";
36 | let operationId;
37 | let responses = {};
38 | let requestBody;
39 | let parameters = {};
40 | let headers = {};
41 | lines.forEach((line) => {
42 | if (line.startsWith("@summary")) {
43 | summary = line.replace("@summary ", "");
44 | }
45 | if (line.startsWith("@tag")) {
46 | tag = line.replace("@tag ", "");
47 | }
48 |
49 | if (line.startsWith("@description")) {
50 | description = line.replace("@description ", "");
51 | }
52 |
53 | if (line.startsWith("@operationId")) {
54 | operationId = line.replace("@operationId ", "");
55 | }
56 |
57 | if (line.startsWith("@responseBody")) {
58 | responses = {
59 | ...responses,
60 | ...this.parseResponseBody(line),
61 | };
62 | }
63 | if (line.startsWith("@responseHeader")) {
64 | const header = this.parseResponseHeader(line);
65 | if (header === null) {
66 | console.error("Error with line: " + line);
67 | return;
68 | }
69 | headers[header["status"]] = {
70 | ...headers[header["status"]],
71 | ...header["header"],
72 | };
73 | }
74 | if (line.startsWith("@requestBody")) {
75 | requestBody = this.parseBody(line, "requestBody");
76 | }
77 | if (line.startsWith("@requestFormDataBody")) {
78 | const parsedBody = this.parseRequestFormDataBody(line);
79 | if (parsedBody) {
80 | requestBody = parsedBody;
81 | }
82 | }
83 | if (line.startsWith("@param")) {
84 | parameters = { ...parameters, ...this.parseParam(line) };
85 | }
86 | });
87 |
88 | for (const [key, value] of Object.entries(responses)) {
89 | if (typeof headers[key] !== undefined) {
90 | responses[key]["headers"] = headers[key];
91 | }
92 | if (!responses[key]["description"]) {
93 | responses[key][
94 | "description"
95 | ] = `Returns **${key}** (${HTTPStatusCode.getMessage(key)}) as **${Object.entries(responses[key]["content"])[0][0]
96 | }**`;
97 | }
98 | }
99 |
100 | return {
101 | description,
102 | responses,
103 | requestBody,
104 | parameters,
105 | summary,
106 | operationId,
107 | tag,
108 | };
109 | }
110 |
111 | private parseParam(line: string) {
112 | let where = "path";
113 | let required = true;
114 | let type = "string";
115 | let example: any = null;
116 | let enums = [];
117 |
118 | if (line.startsWith("@paramUse")) {
119 | let use = getBetweenBrackets(line, "paramUse");
120 | const used = use.split(",");
121 | let h = [];
122 | used.forEach((u) => {
123 | if (typeof this.options.common.parameters[u] === "undefined") {
124 | return;
125 | }
126 | const common = this.options.common.parameters[u];
127 | h = [...h, ...common];
128 | });
129 |
130 | return h;
131 | }
132 |
133 | if (line.startsWith("@paramPath")) {
134 | required = false;
135 | }
136 | if (line.startsWith("@paramQuery")) {
137 | required = false;
138 | }
139 |
140 | let m = line.match("@param([a-zA-Z]*)");
141 | if (m !== null) {
142 | where = m[1].toLowerCase();
143 | line = line.replace(m[0] + " ", "");
144 | }
145 |
146 | let [param, des, meta] = line.split(" - ");
147 | if (typeof param === "undefined") {
148 | return;
149 | }
150 | if (typeof des === "undefined") {
151 | des = "";
152 | }
153 |
154 | if (typeof meta !== "undefined") {
155 | if (meta.includes("@required")) {
156 | required = true;
157 | }
158 | let en = getBetweenBrackets(meta, "enum");
159 | example = getBetweenBrackets(meta, "example");
160 | const mtype = getBetweenBrackets(meta, "type");
161 | if (mtype !== "") {
162 | type = mtype;
163 | }
164 | if (en !== "") {
165 | enums = en.split(",");
166 | example = enums[0];
167 | }
168 | }
169 |
170 | let p = {
171 | in: where,
172 | name: param,
173 | description: des,
174 | schema: {
175 | example: example,
176 | type: type,
177 | },
178 | required: required,
179 | };
180 |
181 | if (enums.length > 1) {
182 | p["schema"]["enum"] = enums;
183 | }
184 |
185 | return { [param]: p };
186 | }
187 |
188 | private parseResponseHeader(responseLine: string) {
189 | let description = "";
190 | let example: any = "";
191 | let type = "string";
192 | let enums = [];
193 | const line = responseLine.replace("@responseHeader ", "");
194 | let [status, name, desc, meta] = line.split(" - ");
195 |
196 | if (typeof status === "undefined" || typeof name === "undefined") {
197 | return null;
198 | }
199 |
200 | if (typeof desc !== "undefined") {
201 | description = desc;
202 | }
203 |
204 | if (name.includes("@use")) {
205 | let use = getBetweenBrackets(name, "use");
206 | const used = use.split(",");
207 | let h = {};
208 | used.forEach((u) => {
209 | if (typeof this.options.common.headers[u] === "undefined") {
210 | return;
211 | }
212 | const common = this.options.common.headers[u];
213 | h = { ...h, ...common };
214 | });
215 |
216 | return {
217 | status: status,
218 | header: h,
219 | };
220 | }
221 |
222 | if (typeof meta !== "undefined") {
223 | example = getBetweenBrackets(meta, "example");
224 | const mtype = getBetweenBrackets(meta, "type");
225 | if (mtype !== "") {
226 | type = mtype;
227 | }
228 | }
229 |
230 | if (example === "" || example === null) {
231 | switch (type) {
232 | case "string":
233 | example = "string";
234 | break;
235 | case "integer":
236 | example = 1;
237 | break;
238 | case "float":
239 | example = 1.5;
240 | break;
241 | }
242 | }
243 |
244 | let h = {
245 | schema: { type: type, example: example },
246 | description: description,
247 | };
248 |
249 | if (enums.length > 1) {
250 | h["schema"]["enum"] = enums;
251 | }
252 | return {
253 | status: status,
254 | header: {
255 | [name]: h,
256 | },
257 | };
258 | }
259 |
260 | private parseResponseBody(responseLine: string) {
261 | let responses = {};
262 | const line = responseLine.replace("@responseBody ", "");
263 | let [status, res, desc] = line.split(" - ");
264 | if (typeof status === "undefined") return;
265 | responses[status] = this.parseBody(res, "responseBody");
266 | responses[status]["description"] = desc;
267 | return responses;
268 | }
269 |
270 | private parseRequestFormDataBody(rawLine: string) {
271 | const line = rawLine.replace("@requestFormDataBody ", "");
272 | let json = {},
273 | required = [];
274 | const isJson = isJSONString(line);
275 | if (!isJson) {
276 | // try to get json from reference
277 | let rawRef = line.substring(line.indexOf("<") + 1, line.lastIndexOf(">"));
278 |
279 | const cleandRef = rawRef.replace("[]", "");
280 | if (cleandRef === "") {
281 | return;
282 | }
283 | const parsedRef = this.exampleGenerator.parseRef(line, true);
284 | let props = [];
285 | const ref = this.exampleGenerator.schemas[cleandRef];
286 | const ks = [];
287 | if (ref.required && Array.isArray(ref.required))
288 | required.push(...ref.required);
289 | Object.entries(ref.properties).map(([key, value]) => {
290 | if (typeof parsedRef[key] === "undefined") {
291 | return;
292 | }
293 | ks.push(key);
294 | if (value["required"]) required.push(key);
295 | props.push({
296 | [key]: {
297 | type:
298 | typeof value["type"] === "undefined" ? "string" : value["type"],
299 | format:
300 | typeof value["format"] === "undefined"
301 | ? "string"
302 | : value["format"],
303 | },
304 | });
305 | });
306 | const p = props.reduce((acc, curr) => ({ ...acc, ...curr }), {});
307 | const appends = Object.keys(parsedRef).filter((k) => !ks.includes(k));
308 | json = p;
309 | if (appends.length > 0) {
310 | appends.forEach((a) => {
311 | json[a] = parsedRef[a];
312 | });
313 | }
314 | } else {
315 | json = JSON.parse(line);
316 | for (let key in json) {
317 | if (json[key].required === "true") {
318 | required.push(key);
319 | }
320 | }
321 | }
322 | // No need to try/catch this JSON.parse as we already did that in the isJSONString function
323 |
324 | return {
325 | content: {
326 | "multipart/form-data": {
327 | schema: {
328 | type: "object",
329 | properties: json,
330 | required,
331 | },
332 | },
333 | },
334 | };
335 | }
336 |
337 | private parseBody(rawLine: string, type: string) {
338 | let line = rawLine.replace(`@${type} `, "");
339 |
340 | const isJson = isJSONString(line);
341 |
342 | if (isJson) {
343 | // No need to try/catch this JSON.parse as we already did that in the isJSONString function
344 | const json = JSON.parse(line);
345 | const o = this.jsonToObj(json);
346 | return {
347 | content: {
348 | "application/json": {
349 | schema: {
350 | type: Array.isArray(json) ? "array" : "object",
351 | ...(Array.isArray(json) ? { items: this.arrayItems(json) } : o),
352 | },
353 |
354 | example: this.exampleGenerator.jsonToRef(json),
355 | },
356 | },
357 | };
358 | }
359 | return this.exampleGenerator.parseRef(line);
360 | }
361 |
362 | arrayItems(json) {
363 | const oneOf = [];
364 |
365 | const t = typeof json[0];
366 |
367 | if (t === "string") {
368 | json.forEach((j) => {
369 | const value = this.exampleGenerator.parseRef(j);
370 |
371 | if (_.has(value, "content.application/json.schema.$ref")) {
372 | oneOf.push({
373 | $ref: value["content"]["application/json"]["schema"]["$ref"],
374 | });
375 | }
376 | });
377 | }
378 |
379 | if (oneOf.length > 0) {
380 | return { oneOf: oneOf };
381 | }
382 | return { type: typeof json[0] };
383 | }
384 |
385 | jsonToObj(json) {
386 | const o = {
387 | type: "object",
388 | properties: Object.keys(json)
389 | .map((key) => {
390 | const t = typeof json[key];
391 | const v = json[key];
392 | let value = v;
393 | if (t === "object") {
394 | value = this.jsonToObj(json[key]);
395 | }
396 | if (t === "string" && v.includes("<") && v.includes(">")) {
397 | value = this.exampleGenerator.parseRef(v);
398 | if (v.includes("[]")) {
399 | let ref = "";
400 | if (_.has(value, "content.application/json.schema.$ref")) {
401 | ref = value["content"]["application/json"]["schema"]["$ref"];
402 | }
403 | if (_.has(value, "content.application/json.schema.items.$ref")) {
404 | ref =
405 | value["content"]["application/json"]["schema"]["items"][
406 | "$ref"
407 | ];
408 | }
409 | value = {
410 | type: "array",
411 | items: {
412 | $ref: ref,
413 | },
414 | };
415 | } else {
416 | value = {
417 | $ref: value["content"]["application/json"]["schema"]["$ref"],
418 | };
419 | }
420 | }
421 | return {
422 | [key]: value,
423 | };
424 | })
425 | .reduce((acc, curr) => ({ ...acc, ...curr }), {}),
426 | };
427 | // console.dir(o, { depth: null });
428 | // console.log(json);
429 | return o;
430 | }
431 |
432 | async getAnnotations(file: string, action: string) {
433 | let annotations = {};
434 | let newdata = "";
435 | if (typeof file === "undefined") return;
436 |
437 | if (typeof this.parsedFiles[file] !== "undefined") {
438 | newdata = this.parsedFiles[file];
439 | } else {
440 | try {
441 | const readFile = util.promisify(fs.readFile);
442 | const data = await readFile(file, "utf8");
443 | for (const line of data.split("\n")) {
444 | const l = line.trim();
445 | if (!l.startsWith("@")) {
446 | newdata += l + "\n";
447 | }
448 | }
449 | this.parsedFiles[file] = newdata;
450 | } catch (e) {
451 | console.error("\x1b[31m✗ File not found\x1b[0m", file)
452 | }
453 | }
454 |
455 | const comments = extract(newdata);
456 | if (comments.length > 0) {
457 | comments.forEach((comment) => {
458 | if (comment.type !== "BlockComment") return;
459 | let lines = comment.value.split("\n").filter((l) => l != "");
460 | // fix for decorators
461 | if (lines[0].trim() !== "@" + action) return;
462 | lines = lines.filter((l) => l != "");
463 |
464 | annotations[action] = this.parseAnnotations(lines);
465 | });
466 | }
467 | return annotations;
468 | }
469 | }
470 |
471 | export class RouteParser {
472 | options: options;
473 | constructor(options: options) {
474 | this.options = options;
475 | }
476 |
477 | /*
478 | extract path-variables, tags and the uri-pattern
479 | */
480 | extractInfos(p: string) {
481 | let parameters = {};
482 | let pattern = "";
483 | let tags = [];
484 | let required: boolean;
485 |
486 | const split = p.split("/");
487 | if (split.length > this.options.tagIndex) {
488 | tags = [split[this.options.tagIndex].toUpperCase()];
489 | }
490 | split.forEach((part) => {
491 | if (part.startsWith(":")) {
492 | required = !part.endsWith("?");
493 | const param = part.replace(":", "").replace("?", "");
494 | part = "{" + param + "}";
495 | parameters = {
496 | ...parameters,
497 | [param]: {
498 | in: "path",
499 | name: param,
500 | schema: {
501 | type: "string",
502 | },
503 | required: required,
504 | },
505 | };
506 | }
507 | pattern += "/" + part;
508 | });
509 | if (pattern.endsWith("/")) {
510 | pattern = pattern.slice(0, -1);
511 | }
512 | return { tags, parameters, pattern };
513 | }
514 | }
515 |
516 | export class ModelParser {
517 | exampleGenerator: ExampleGenerator;
518 | snakeCase: boolean;
519 | constructor(snakeCase: boolean) {
520 | this.snakeCase = snakeCase;
521 | this.exampleGenerator = new ExampleGenerator({});
522 | }
523 |
524 | parseModelProperties(data) {
525 | let props = {};
526 | let required = [];
527 | // remove empty lines
528 | data = data.replace(/\t/g, "").replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "");
529 | const lines = data.split("\n");
530 | let softDelete = false;
531 | let name = "";
532 | lines.forEach((line, index) => {
533 | line = line.trim();
534 | // skip comments
535 | if (line.startsWith("export default class")) {
536 | name = line.split(" ")[3];
537 | }
538 | if (
539 | line.includes("@swagger-softdelete") ||
540 | line.includes("SoftDeletes")
541 | ) {
542 | softDelete = true;
543 | }
544 |
545 | if (
546 | line.startsWith("//") ||
547 | line.startsWith("/*") ||
548 | line.startsWith("*") ||
549 | line.startsWith("public static ") ||
550 | line.startsWith("private static ") ||
551 | line.startsWith("static ")
552 | )
553 | return;
554 |
555 | if (index > 0 && lines[index - 1].includes("serializeAs: null")) return;
556 | if (index > 0 && lines[index - 1].includes("@no-swagger")) return;
557 | if (
558 | !line.startsWith("public ") &&
559 | !line.startsWith("public get") &&
560 | !line.includes("declare ")
561 | )
562 | return;
563 |
564 | let s = [];
565 |
566 | if (line.includes("declare ")) {
567 | s = line.split("declare ");
568 | }
569 | if (line.startsWith("public ")) {
570 | if (line.startsWith("public get")) {
571 | s = line.split("public get");
572 | let s2 = s[1].replace(/;/g, "").split(":");
573 | } else {
574 | s = line.split("public ");
575 | }
576 | }
577 |
578 | let s2 = s[1].replace(/;/g, "").split(":");
579 |
580 | let field = s2[0];
581 | let type = s2[1] || "";
582 | type = type.trim();
583 | let enums = [];
584 | let format = "";
585 | let keyprops = {};
586 | let example: any = null;
587 |
588 | if (index > 0 && lines[index - 1].includes("@enum")) {
589 | const l = lines[index - 1];
590 | let en = getBetweenBrackets(l, "enum");
591 | if (en !== "") {
592 | enums = en.split(",");
593 | example = enums[0];
594 | }
595 | }
596 |
597 | if (index > 0 && lines[index - 1].includes("@format")) {
598 | const l = lines[index - 1];
599 | let en = getBetweenBrackets(l, "format");
600 | if (en !== "") {
601 | format = en;
602 | }
603 | }
604 |
605 | if (index > 0 && lines[index - 1].includes("@example")) {
606 | const l = lines[index - 1];
607 | let match = l.match(/example\(([^()]*)\)/g);
608 | if (match !== null) {
609 | const m = match[0].replace("example(", "").replace(")", "");
610 | example = m;
611 | if (type === "number") {
612 | example = parseInt(m);
613 | }
614 | }
615 | }
616 |
617 | if (index > 0 && lines[index - 1].includes("@required")) {
618 | required.push(field);
619 | }
620 |
621 | if (index > 0 && lines[index - 1].includes("@props")) {
622 | const l = lines[index - 1].replace("@props", "props");
623 | const j = getBetweenBrackets(l, "props");
624 | if (isJSONString(j)) {
625 | keyprops = JSON.parse(j);
626 | }
627 | }
628 |
629 | if (typeof type === "undefined") {
630 | type = "string";
631 | format = "";
632 | }
633 |
634 | field = field.trim();
635 |
636 | type = type.trim();
637 |
638 | //TODO: make oneOf
639 | if (type.includes(" | ")) {
640 | const types = type.split(" | ");
641 | type = types.filter((t) => t !== "null")[0];
642 | }
643 |
644 | field = field.replace("()", "");
645 | field = field.replace("get ", "");
646 | type = type.replace("{", "").trim();
647 |
648 | if (this.snakeCase) {
649 | field = snakeCase(field);
650 | }
651 |
652 | let indicator = "type";
653 |
654 | if (example === null) {
655 | example = "string";
656 | }
657 |
658 | // if relation to another model
659 | if (type.includes("typeof")) {
660 | s = type.split("typeof ");
661 | type = "#/components/schemas/" + s[1].slice(0, -1);
662 | indicator = "$ref";
663 | } else {
664 | if (standardTypes.includes(type.toLowerCase())) {
665 | type = type.toLowerCase();
666 | } else {
667 | // assume its a custom interface
668 | indicator = "$ref";
669 | type = "#/components/schemas/" + type;
670 | }
671 | }
672 | type = type.trim();
673 | let isArray = false;
674 |
675 | if (
676 | line.includes("HasMany") ||
677 | line.includes("ManyToMany") ||
678 | line.includes("HasManyThrough") ||
679 | type.includes("[]")
680 | ) {
681 | isArray = true;
682 | if (type.slice(type.length - 2, type.length) === "[]") {
683 | type = type.split("[]")[0];
684 | }
685 | }
686 | if (example === null || example === "string") {
687 | example =
688 | this.exampleGenerator.exampleByField(field) ||
689 | this.exampleGenerator.exampleByType(type);
690 | }
691 |
692 | if (type === "datetime") {
693 | indicator = "type";
694 | type = "string";
695 | format = "date-time";
696 | }
697 |
698 | if (type === "date") {
699 | indicator = "type";
700 | type = "string";
701 | format = "date";
702 | }
703 |
704 | if (field === "email") {
705 | indicator = "type";
706 | type = "string";
707 | format = "email";
708 | }
709 | if (field === "password") {
710 | indicator = "type";
711 | type = "string";
712 | format = "password";
713 | }
714 |
715 | if (enums.length > 0) {
716 | indicator = "type";
717 | type = "string";
718 | }
719 |
720 | if (type === "any") {
721 | indicator = "$ref";
722 | type = "#/components/schemas/Any";
723 | }
724 |
725 | let prop = {};
726 | if (type === "integer" || type === "number") {
727 | if (example === null || example === "string") {
728 | example = Math.floor(Math.random() * 1000);
729 | }
730 | }
731 | if (type === "boolean") {
732 | example = true;
733 | }
734 |
735 | prop[indicator] = type;
736 | prop["example"] = example;
737 | // if array
738 | if (isArray) {
739 | props[field] = { type: "array", items: prop };
740 | } else {
741 | props[field] = prop;
742 | if (format !== "") {
743 | props[field]["format"] = format;
744 | }
745 | }
746 | Object.entries(keyprops).map(([key, value]) => {
747 | props[field][key] = value;
748 | });
749 | if (enums.length > 0) {
750 | props[field]["enum"] = enums;
751 | }
752 | });
753 |
754 | if (softDelete) {
755 | props["deleted_at"] = {
756 | type: "string",
757 | format: "date-time",
758 | example: "2021-03-23T16:13:08.489+01:00",
759 | };
760 | }
761 |
762 | return { name: name, props: props, required: required };
763 | }
764 | }
765 |
766 | export class ValidatorParser {
767 | exampleGenerator: ExampleGenerator;
768 | constructor() {
769 | this.exampleGenerator = new ExampleGenerator({});
770 | }
771 | async validatorToObject(validator: VineValidator) {
772 | // console.dir(validator.toJSON()["refs"], { depth: null });
773 | // console.dir(json, { depth: null });
774 | const obj = {
775 | type: "object",
776 | properties: this.parseSchema(
777 | validator.toJSON()["schema"]["schema"],
778 | validator.toJSON()["refs"]
779 | ),
780 | };
781 | // console.dir(obj, { depth: null });
782 | const testObj = this.objToTest(obj["properties"]);
783 | return await this.parsePropsAndMeta(obj, testObj, validator);
784 | }
785 |
786 | async parsePropsAndMeta(obj, testObj, validator: VineValidator) {
787 | // console.log(Object.keys(errors));
788 | const { SimpleMessagesProvider } = await import("@vinejs/vine");
789 | const [e] = await validator.tryValidate(testObj, {
790 | messagesProvider: new SimpleMessagesProvider({
791 | required: "REQUIRED",
792 | string: "TYPE",
793 | object: "TYPE",
794 | number: "TYPE",
795 | boolean: "TYPE",
796 | }),
797 | });
798 |
799 | // if no errors, this means all object-fields are of type number (which we use by default)
800 | // and we can return the object
801 | if (e === null) {
802 | obj["example"] = testObj;
803 | return obj;
804 | }
805 |
806 | const msgs = e.messages;
807 |
808 | for (const m of msgs) {
809 | const err = m["message"];
810 | let objField = m["field"].replace(".", ".properties.");
811 | if (m["field"].includes(".0")) {
812 | objField = objField.replaceAll(`.0`, ".items");
813 | }
814 | if (err === "TYPE") {
815 | _.set(obj["properties"], objField, {
816 | ..._.get(obj["properties"], objField),
817 | type: m["rule"],
818 | example: this.exampleGenerator.exampleByType(m["rule"]),
819 | });
820 | if (m["rule"] === "string") {
821 | if (_.get(obj["properties"], objField)["minimum"]) {
822 | _.set(obj["properties"], objField, {
823 | ..._.get(obj["properties"], objField),
824 | minLength: _.get(obj["properties"], objField)["minimum"],
825 | });
826 | _.unset(obj["properties"], objField + ".minimum");
827 | }
828 | if (_.get(obj["properties"], objField)["maximum"]) {
829 | _.set(obj["properties"], objField, {
830 | ..._.get(obj["properties"], objField),
831 | maxLength: _.get(obj["properties"], objField)["maximum"],
832 | });
833 | _.unset(obj["properties"], objField + ".maximum");
834 | }
835 | }
836 |
837 | _.set(
838 | testObj,
839 | m["field"],
840 | this.exampleGenerator.exampleByType(m["rule"])
841 | );
842 | }
843 |
844 | if (err === "FORMAT") {
845 | _.set(obj["properties"], objField, {
846 | ..._.get(obj["properties"], objField),
847 | format: m["rule"],
848 | type: "string",
849 | example: this.exampleGenerator.exampleByValidatorRule(m["rule"]),
850 | });
851 | _.set(
852 | testObj,
853 | m["field"],
854 | this.exampleGenerator.exampleByValidatorRule(m["rule"])
855 | );
856 | }
857 | }
858 |
859 | // console.dir(obj, { depth: null });
860 | obj["example"] = testObj;
861 | return obj;
862 | }
863 |
864 | objToTest(obj) {
865 | const res = {};
866 | Object.keys(obj).forEach((key) => {
867 | if (obj[key]["type"] === "object") {
868 | res[key] = this.objToTest(obj[key]["properties"]);
869 | } else if (obj[key]["type"] === "array") {
870 | if (obj[key]["items"]["type"] === "object") {
871 | res[key] = [this.objToTest(obj[key]["items"]["properties"])];
872 | } else {
873 | res[key] = [obj[key]["items"]["example"]];
874 | }
875 | } else {
876 | res[key] = obj[key]["example"];
877 | }
878 | });
879 | return res;
880 | }
881 |
882 | parseSchema(json, refs) {
883 | const obj = {};
884 | for (const p of json["properties"]) {
885 | let meta: {
886 | minimum?: number;
887 | maximum?: number;
888 | choices?: any;
889 | pattern?: string;
890 | } = {};
891 | for (const v of p["validations"]) {
892 | if (refs[v["ruleFnId"]].options?.min) {
893 | meta = { ...meta, minimum: refs[v["ruleFnId"]].options.min };
894 | }
895 | if (refs[v["ruleFnId"]].options?.max) {
896 | meta = { ...meta, maximum: refs[v["ruleFnId"]].options.max };
897 | }
898 | if (refs[v["ruleFnId"]].options?.choices) {
899 | meta = { ...meta, choices: refs[v["ruleFnId"]].options.choices };
900 | }
901 | if (refs[v["ruleFnId"]].options?.toString().includes("/")) {
902 | meta = { ...meta, pattern: refs[v["ruleFnId"]].options.toString() };
903 | }
904 | }
905 |
906 | // console.dir(p, { depth: null });
907 | // console.dir(validations, { depth: null });
908 | // console.log(min, max, choices, regex);
909 |
910 | obj[p["fieldName"]] =
911 | p["type"] === "object"
912 | ? { type: "object", properties: this.parseSchema(p, refs) }
913 | : p["type"] === "array"
914 | ? {
915 | type: "array",
916 | items:
917 | p["each"]["type"] === "object"
918 | ? {
919 | type: "object",
920 | properties: this.parseSchema(p["each"], refs),
921 | }
922 | : {
923 | type: "number",
924 | example: meta.minimum
925 | ? meta.minimum
926 | : this.exampleGenerator.exampleByType("number"),
927 | ...meta,
928 | },
929 | }
930 | : {
931 | type: "number",
932 | example: meta.minimum
933 | ? meta.minimum
934 | : this.exampleGenerator.exampleByType("number"),
935 | ...meta,
936 | };
937 | if (!p["isOptional"]) obj[p["fieldName"]]["required"] = true;
938 | }
939 | return obj;
940 | }
941 | }
942 |
943 | export class InterfaceParser {
944 | exampleGenerator: ExampleGenerator;
945 | snakeCase: boolean;
946 | schemas: any = {};
947 |
948 | constructor(snakeCase: boolean, schemas: any = {}) {
949 | this.snakeCase = snakeCase;
950 | this.exampleGenerator = new ExampleGenerator({});
951 | this.schemas = schemas;
952 | }
953 |
954 | objToExample(obj) {
955 | let example = {};
956 | Object.entries(obj).map(([key, value]) => {
957 | if (typeof value === "object") {
958 | example[key] = this.objToExample(value);
959 | } else {
960 | example[key] = this.exampleGenerator.exampleByType(value as string);
961 | if (example[key] === null) {
962 | example[key] = this.exampleGenerator.exampleByField(key);
963 | }
964 | }
965 | });
966 | return example;
967 | }
968 |
969 | parseProps(obj) {
970 | const no = {};
971 | Object.entries(obj).map(([f, value]) => {
972 | if (typeof value === "object") {
973 | no[f.replaceAll("?", "")] = {
974 | type: "object",
975 | nullable: f.includes("?"),
976 | properties: this.parseProps(value),
977 | example: this.objToExample(value),
978 | };
979 | } else {
980 | no[f.replaceAll("?", "")] = {
981 | ...this.parseType(value, f),
982 | };
983 | }
984 | });
985 | return no;
986 | }
987 |
988 | getInheritedProperties(baseType: string): any {
989 |
990 | if (this.schemas[baseType]?.properties) {
991 | return {
992 | properties: this.schemas[baseType].properties,
993 | required: this.schemas[baseType].required || []
994 | };
995 | }
996 |
997 | const cleanType = baseType
998 | .split('/')
999 | .pop()
1000 | ?.replace('.ts', '')
1001 | ?.replace(/^[#@]/, '');
1002 |
1003 | if (!cleanType) return { properties: {}, required: [] };
1004 |
1005 | if (this.schemas[cleanType]?.properties) {
1006 | return {
1007 | properties: this.schemas[cleanType].properties,
1008 | required: this.schemas[cleanType].required || []
1009 | };
1010 | }
1011 |
1012 | const variations = [
1013 | cleanType,
1014 | `#models/${cleanType}`,
1015 | cleanType.replace(/Model$/, ''),
1016 | `${cleanType}Model`
1017 | ];
1018 |
1019 | for (const variation of variations) {
1020 | if (this.schemas[variation]?.properties) {
1021 | return {
1022 | properties: this.schemas[variation].properties,
1023 | required: this.schemas[variation].required || []
1024 | };
1025 | }
1026 | }
1027 |
1028 | return { properties: {}, required: [] };
1029 | }
1030 |
1031 | parseInterfaces(data) {
1032 | data = data.replace(/\t/g, "").replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "");
1033 |
1034 | let currentInterface = null;
1035 | const interfaces = {};
1036 | const interfaceDefinitions = new Map();
1037 |
1038 | const lines = data.split("\n");
1039 | for (let i = 0; i < lines.length; i++) {
1040 | const line = lines[i].trim();
1041 | const isDefault = line.startsWith("export default interface")
1042 |
1043 | if (line.startsWith("interface") || line.startsWith("export interface") || isDefault) {
1044 | const sp = line.split(/\s+/)
1045 | const idx = line.endsWith("}") ? sp.length - 1 : sp.length - 2
1046 | const name = sp[idx].split(/[{\s]/)[0];
1047 | const extendedTypes = this.parseExtends(line);
1048 | interfaceDefinitions.set(name, {
1049 | extends: extendedTypes,
1050 | properties: {},
1051 | required: [],
1052 | startLine: i
1053 | });
1054 | currentInterface = name;
1055 | continue;
1056 | }
1057 |
1058 | if (currentInterface && line === "}") {
1059 | currentInterface = null;
1060 | continue;
1061 | }
1062 |
1063 | if (currentInterface && line && !line.startsWith("//") && !line.startsWith("/*") && !line.startsWith("*")) {
1064 | const def = interfaceDefinitions.get(currentInterface);
1065 | if (def) {
1066 | const previousLine = i > 0 ? lines[i - 1].trim() : "";
1067 | const isRequired = previousLine.includes("@required");
1068 |
1069 | const [prop, type] = line.split(":").map(s => s.trim());
1070 | if (prop && type) {
1071 | const cleanProp = prop.replace("?", "");
1072 | def.properties[cleanProp] = type.replace(";", "");
1073 |
1074 |
1075 | if (isRequired || !prop.includes("?")) {
1076 | def.required.push(cleanProp);
1077 | }
1078 | }
1079 | }
1080 | }
1081 | }
1082 |
1083 | for (const [name, def] of interfaceDefinitions) {
1084 | let allProperties = {};
1085 | let requiredFields = new Set(def.required);
1086 |
1087 | for (const baseType of def.extends) {
1088 | const baseSchema = this.schemas[baseType];
1089 | if (baseSchema) {
1090 | if (baseSchema.properties) {
1091 | Object.assign(allProperties, baseSchema.properties);
1092 | }
1093 |
1094 | if (baseSchema.required) {
1095 | baseSchema.required.forEach(field => requiredFields.add(field));
1096 | }
1097 | }
1098 | }
1099 |
1100 | Object.assign(allProperties, def.properties);
1101 |
1102 | const parsedProperties = {};
1103 | for (const [key, value] of Object.entries(allProperties)) {
1104 | if (typeof value === 'object' && value !== null && 'type' in value) {
1105 | parsedProperties[key] = value;
1106 | } else {
1107 | parsedProperties[key] = this.parseType(value, key);
1108 | }
1109 | }
1110 |
1111 | const schema = {
1112 | type: "object",
1113 | properties: parsedProperties,
1114 | required: Array.from(requiredFields),
1115 | description: `${name}${def.extends.length ? ` extends ${def.extends.join(", ")}` : ""} (Interface)`
1116 | };
1117 |
1118 | if (schema.required.length === 0) {
1119 | delete schema.required;
1120 | }
1121 |
1122 | interfaces[name] = schema;
1123 | }
1124 |
1125 | return interfaces;
1126 | }
1127 |
1128 | parseExtends(line: string): string[] {
1129 | const matches = line.match(/extends\s+([^{]+)/);
1130 | if (!matches) return [];
1131 |
1132 | return matches[1]
1133 | .split(",")
1134 | .map(type => type.trim())
1135 | .map(type => {
1136 | const cleanType = type.split('/').pop();
1137 | return cleanType?.replace(/\.ts$/, '') || type;
1138 | });
1139 | }
1140 |
1141 | parseType(type: string | any, field: string) {
1142 | if (typeof type === 'object' && type !== null && 'type' in type) {
1143 | return type;
1144 | }
1145 |
1146 | let isArray = false;
1147 | if (typeof type === 'string' && type.includes("[]")) {
1148 | type = type.replace("[]", "");
1149 | isArray = true;
1150 | }
1151 |
1152 | if (typeof type === 'string') {
1153 | type = type.replace(/[;\r\n]/g, '').trim();
1154 | }
1155 |
1156 | let prop: any = { type: type };
1157 | let notRequired = field.includes("?");
1158 | prop.nullable = notRequired;
1159 |
1160 | if (typeof type === 'string' && type.toLowerCase() === "datetime") {
1161 | prop.type = "string";
1162 | prop.format = "date-time";
1163 | prop.example = "2021-03-23T16:13:08.489+01:00";
1164 | } else if (typeof type === 'string' && type.toLowerCase() === "date") {
1165 | prop.type = "string";
1166 | prop.format = "date";
1167 | prop.example = "2021-03-23";
1168 | } else {
1169 | const standardTypes = ["string", "number", "boolean", "integer"];
1170 | if (typeof type === 'string' && !standardTypes.includes(type.toLowerCase())) {
1171 | delete prop.type;
1172 | prop.$ref = `#/components/schemas/${type}`;
1173 | } else {
1174 | if (typeof type === 'string') {
1175 | prop.type = type.toLowerCase();
1176 | }
1177 | prop.example = this.exampleGenerator.exampleByType(type) ||
1178 | this.exampleGenerator.exampleByField(field);
1179 | }
1180 | }
1181 |
1182 | if (isArray) {
1183 | return {
1184 | type: "array",
1185 | items: prop
1186 | };
1187 | }
1188 |
1189 | return prop;
1190 | }
1191 | }
1192 |
1193 | export class EnumParser {
1194 | constructor() { }
1195 |
1196 | parseEnums(data: string): Record {
1197 | const enums: Record = {};
1198 | const lines = data.split("\n");
1199 | let currentEnum: string | null = null;
1200 | let description: string | null = null;
1201 |
1202 | for (const line of lines) {
1203 | const trimmedLine = line.trim();
1204 |
1205 | if (trimmedLine.startsWith("//")) {
1206 | description = trimmedLine.slice(2).trim();
1207 | continue;
1208 | }
1209 |
1210 | if (
1211 | trimmedLine.startsWith("enum") ||
1212 | trimmedLine.startsWith("export enum")
1213 | ) {
1214 | const match = trimmedLine.match(/(?:export\s+)?enum\s+(\w+)/);
1215 | if (match) {
1216 | currentEnum = match[1];
1217 | enums[currentEnum] = {
1218 | type: "string",
1219 | enum: [],
1220 | properties: {},
1221 | description: description || `${startCase(currentEnum)} enumeration`,
1222 | };
1223 | description = null;
1224 | }
1225 | continue;
1226 | }
1227 |
1228 | if (currentEnum && trimmedLine !== "{" && trimmedLine !== "}") {
1229 | const [key, value] = trimmedLine.split("=").map((s) => s.trim());
1230 | if (key) {
1231 | const enumValue = value ? this.parseEnumValue(value) : key;
1232 | enums[currentEnum].enum.push(enumValue);
1233 | }
1234 | }
1235 |
1236 | if (trimmedLine === "}") {
1237 | currentEnum = null;
1238 | }
1239 | }
1240 |
1241 |
1242 | return enums;
1243 | }
1244 |
1245 | private parseEnumValue(value: string): string {
1246 | // Remove quotes and comma
1247 | return value.replace(/['",]/g, "").trim();
1248 | }
1249 | }
1250 |
--------------------------------------------------------------------------------
/src/scalarCustomCss.ts:
--------------------------------------------------------------------------------
1 | export const scalarCustomCss = `
2 | /* basic theme */
3 | .light-mode {
4 | --theme-background-1: #fff;
5 | --theme-background-2: #fafaf9;
6 | --theme-background-3: rgb(245 245 245);
7 |
8 | --theme-color-1: #21201c;
9 | --theme-color-2: #63635d;
10 | --theme-color-3: #8e8e8e;
11 |
12 | --theme-color-accent: #5a45ff;
13 | --theme-background-accent: #5a45ff1f;
14 |
15 | --theme-border-color: color(display-p3 0.913 0.912 0.903);
16 | --theme-code-language-color-supersede: var(--theme-color-1);
17 | --theme-code-languages-background-supersede: var(--theme-background-3);
18 | }
19 | .dark-mode {
20 | --theme-background-1: #0f0f0f;
21 | --theme-background-2: #222222;
22 | --theme-background-3: #272727;
23 |
24 | --theme-color-1: #e2e4e8;
25 | --theme-color-2: rgba(255, 255, 255, 0.62);
26 | --theme-color-3: #6a737d;
27 |
28 | --theme-color-accent: #e2ddfe;
29 | --theme-background-accent: #3c2d6a;
30 |
31 | --theme-border-color: rgba(255, 255, 255, 0.1);
32 | --theme-code-language-color-supersede: var(--theme-color-1);
33 | --theme-code-languages-background-supersede: var(--theme-background-3);
34 | }
35 |
36 | /* Document Sidebar */
37 | .light-mode .t-doc__sidebar,
38 | .dark-mode .t-doc__sidebar {
39 | --sidebar-background-1: var(--theme-background-1);
40 | --sidebar-color-1: var(--theme-color-1);
41 | --sidebar-color-2: var(--theme-color-2);
42 | --sidebar-border-color: var(--theme-border-color);
43 |
44 | --sidebar-item-hover-background: var(--theme-background-2);
45 | --sidebar-item-hover-color: currentColor;
46 |
47 | --sidebar-item-active-background: var(--theme-background-accent);
48 | --sidebar-color-active: var(--theme-color-accent);
49 |
50 | --sidebar-search-background: var(--theme-background-2);
51 | --sidebar-search-color: var(--theme-color-3);
52 | --sidebar-search-border-color: var(--theme-border-color);
53 | }
54 |
55 | /* advanced */
56 | .light-mode {
57 | --theme-color-green: #0e766e;
58 | --theme-color-red: #e53935;
59 | --theme-color-yellow: #e2931d;
60 | --theme-color-blue: #0f766e;
61 | --theme-color-orange: #f76d47;
62 | --theme-color-purple: #4338ca;
63 | }
64 | .dark-mode {
65 | --theme-color-green: #0ad8b6;
66 | --theme-color-red: #e5484d;
67 | --theme-color-yellow: #eac063;
68 | --theme-color-blue: #6abaff;
69 | --theme-color-orange: #ff9b52;
70 | --theme-color-purple: #6550b9;
71 | }
72 | /* custom-theme */
73 | .show-api-client-button:before {
74 | background: white !important;
75 | }
76 | .show-api-client-button span,
77 | .show-api-client-button svg {
78 | color: var(--theme-background-1) !important;
79 | }
80 | .section:not(:last-of-type),
81 | .section-container {
82 | border-top: none !important;
83 | border-bottom: none !important;
84 | }
85 | .section-container:after,
86 | .tag-section-container section.section:after {
87 | content: "";
88 | width: 100%;
89 | height: 1px;
90 | position: absolute;
91 | top: 0;
92 | left: 0;
93 | background: repeating-linear-gradient(
94 | 90deg,
95 | var(--theme-border-color) 0 4px,
96 | transparent 0 8px
97 | );
98 | }
99 | .section-container:nth-of-type(2):after {
100 | display: none;
101 | }
102 | .tag-section-container .section:first-of-type:after {
103 | display: none;
104 | }
105 | .sidebar {
106 | border-right: none !important;
107 | }
108 | .t-doc__sidebar {
109 | position: relative;
110 | }
111 | .t-doc__sidebar:after {
112 | content: "";
113 | width: 1px;
114 | height: 100%;
115 | position: absolute;
116 | right: 0;
117 | top: 0;
118 | background: repeating-linear-gradient(
119 | 0deg,
120 | var(--theme-border-color) 0 4px,
121 | transparent 0 8px
122 | );
123 | display: block;
124 | }
125 | .download-cta .download-button,
126 | .scalar-api-reference .section .markdown a {
127 | --theme-color-accent: var(--theme-color-1) !important;
128 | text-decoration: underline !important;
129 | cursor: pointer;
130 | }`
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Autoswagger interfaces
4 | */
5 | export interface options {
6 | title?: string;
7 | ignore: string[];
8 | version?: string;
9 | description?: string;
10 | path: string;
11 | tagIndex: number;
12 | snakeCase: boolean;
13 | common: common;
14 | fileNameInSummary?: boolean;
15 | preferredPutPatch?: string;
16 | persistAuthorization?: boolean;
17 | appPath?: string;
18 | debug?: boolean;
19 | info?: any;
20 | securitySchemes?: any;
21 | productionEnv?: string;
22 | authMiddlewares?: string[];
23 | defaultSecurityScheme?: string;
24 | }
25 |
26 | export interface common {
27 | headers: any;
28 | parameters: any;
29 | }
30 |
31 | /**
32 | * Adonis routes
33 | */
34 | export interface AdonisRouteMeta {
35 | resolvedHandler: {
36 | type: string;
37 | namespace?: string;
38 | method?: string;
39 | };
40 | resolvedMiddleware: Array<{
41 | type: string;
42 | args?: any[];
43 | }>;
44 | }
45 |
46 | export interface v6Handler {
47 | method?: string;
48 | moduleNameOrPath?: string;
49 | reference: string | any[];
50 | name: string;
51 | }
52 |
53 | export interface AdonisRoute {
54 | methods: string[];
55 | pattern: string;
56 | meta: AdonisRouteMeta;
57 | middleware: string[] | any;
58 | name?: string;
59 | params: string[];
60 | handler?: string | v6Handler;
61 | }
62 |
63 | export interface AdonisRoutes {
64 | root: AdonisRoute[];
65 | }
66 |
67 | export const standardTypes = [
68 | "string",
69 | "number",
70 | "integer",
71 | "datetime",
72 | "date",
73 | "boolean",
74 | "any",
75 | ]
76 | .map((type) => [type, type + "[]"])
77 | .flat();
78 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2022"],
4 | "target": "ES2022",
5 | "module": "Node16",
6 | "declaration": true,
7 | "moduleResolution": "Node16",
8 | "outDir": "./dist",
9 | "esModuleInterop": true
10 | },
11 | "skipLibCheck": true,
12 | "include": ["src/**/*"]
13 | }
14 |
--------------------------------------------------------------------------------