├── .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 | [![Version](https://img.shields.io/github/tag/ad-on-is/adonis-autoswagger.svg?style=flat?branch=main)]() 7 | [![GitHub stars](https://img.shields.io/github/stars/ad-on-is/adonis-autoswagger.svg?style=social&label=Star)]() 8 | [![GitHub watchers](https://img.shields.io/github/watchers/ad-on-is/adonis-autoswagger.svg?style=social&label=Watch)]() 9 | [![GitHub forks](https://img.shields.io/github/forks/ad-on-is/adonis-autoswagger.svg?style=social&label=Fork)]() 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 | ![paths](https://i.imgur.com/EnPw6xT.png) 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 | ![alt](https://i.imgur.com/FEdLplp.png) 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 | --------------------------------------------------------------------------------