Full Code of ad-on-is/adonis-autoswagger for AI

main 86610b2ad17c cached
17 files
98.3 KB
25.0k tokens
77 symbols
1 requests
Download .txt
Repository: ad-on-is/adonis-autoswagger
Branch: main
Commit: 86610b2ad17c
Files: 17
Total size: 98.3 KB

Directory structure:
gitextract_ofo9iakp/

├── .github/
│   └── workflows/
│       └── deploy.yml
├── .gitignore
├── DocsGenerate.ts.example
├── DocsGeneratev6.ts.example
├── LICENSE
├── README.md
├── package.json
├── publish.sh
├── src/
│   ├── adonishelpers.ts
│   ├── autoswagger.ts
│   ├── example.ts
│   ├── helpers.ts
│   ├── index.ts
│   ├── parsers.ts
│   ├── scalarCustomCss.ts
│   └── types.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/deploy.yml
================================================
name: Deploy
run-name: Deploying to npmjs 🥹🤞
on:
  push:
    tags:
      - "*"
jobs:
  Deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v3
        with:
          node-version: '20.x'
          registry-url: "https://registry.npmjs.com"
      - uses: pnpm/action-setup@v4
      - run: pnpm install
      - run: pnpm build
      - run: git config user.email "deploy@github" && git config user.name "GitHub Action"
      - run: pnpm publish --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}


================================================
FILE: .gitignore
================================================
node_modules
dist/*

================================================
FILE: DocsGenerate.ts.example
================================================
import { BaseCommand } from '@adonisjs/core/build/standalone'
import AutoSwagger from 'adonis-autoswagger'
import swagger from '../config/swagger'
export default class DocsGenerate extends BaseCommand {
  public static commandName = 'docs:generate'

  public static description = ''

  public static settings = {
    loadApp: true,

    stayAlive: false,
  }

  public async run() {
    const Router = await this.application.container.use('Adonis/Core/Route')
    Router.commit()
    await AutoSwagger.writeFile(await Router.toJSON(), swagger)
  }
}


================================================
FILE: DocsGeneratev6.ts.example
================================================
import { BaseCommand } from '@adonisjs/core/ace'
import { CommandOptions } from '@adonisjs/core/types/ace'
import AutoSwagger from 'adonis-autoswagger'
import swagger from '#config/swagger'
export default class DocsGenerate extends BaseCommand {
  static commandName = 'docs:generate'

  static options: CommandOptions = {
    startApp: true,
    allowUnknownFlags: false,
    staysAlive: false,
  }

  async run() {
    const Router = await this.app.container.make('router')
    Router.commit()
    await AutoSwagger.default.writeFile(Router.toJSON(), swagger)
  }
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Adis Durakovic

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
<h1 align="center">
Adonis AutoSwagger <br />
<img src="https://upload.wikimedia.org/wikipedia/commons/a/ab/Swagger-logo.png" height="50" />
</h1>

[![Version](https://img.shields.io/github/tag/ad-on-is/adonis-autoswagger.svg?style=flat?branch=main)]()
[![GitHub stars](https://img.shields.io/github/stars/ad-on-is/adonis-autoswagger.svg?style=social&label=Star)]()
[![GitHub watchers](https://img.shields.io/github/watchers/ad-on-is/adonis-autoswagger.svg?style=social&label=Watch)]()
[![GitHub forks](https://img.shields.io/github/forks/ad-on-is/adonis-autoswagger.svg?style=social&label=Fork)]()

### Auto-Generate swagger docs for AdonisJS

## 💻️ Install

```bash
pnpm i adonis-autoswagger #using pnpm
```

---

## ⭐️ Features

- Creates **paths** automatically based on `routes.ts`
- Creates **schemas** automatically based on `app/Models/*`
- Creates **schemas** automatically based on `app/Interfaces/*`
- Creates **schemas** automatically based on `app/Validators/*` (only for adonisJS v6)
- Creates **schemas** automatically based on `app/Types/*` (only for adonisJS v6)
- **Rich configuration** via comments
- Works also in **production** mode
- `node ace docs:generate` command

---

## ✌️Usage

Create a file `/config/swagger.ts`

```ts
// for AdonisJS v6
import path from "node:path";
import url from "node:url";
// ---

export default {
  // path: __dirname + "/../", for AdonisJS v5
  path: path.dirname(url.fileURLToPath(import.meta.url)) + "/../", // for AdonisJS v6
  title: "Foo", // use info instead
  version: "1.0.0", // use info instead
  description: "", // use info instead
  tagIndex: 2,
  productionEnv: "production", // optional
  info: {
    title: "title",
    version: "1.0.0",
    description: "",
  },
  snakeCase: true,

  debug: false, // set to true, to get some useful debug output
  ignore: ["/swagger", "/docs"],
  preferredPutPatch: "PUT", // if PUT/PATCH are provided for the same route, prefer PUT
  common: {
    parameters: {}, // OpenAPI conform parameters that are commonly used
    headers: {}, // OpenAPI conform headers that are commonly used
  },
  securitySchemes: {}, // optional
  authMiddlewares: ["auth", "auth:api"], // optional
  defaultSecurityScheme: "BearerAuth", // optional
  persistAuthorization: true, // persist authorization between reloads on the swagger page
  showFullPath: false, // the path displayed after endpoint summary
};
```

In your `routes.ts`

## 6️⃣ for AdonisJS v6

```js
import AutoSwagger from "adonis-autoswagger";
import swagger from "#config/swagger";
// returns swagger in YAML
router.get("/swagger", async () => {
  return AutoSwagger.default.docs(router.toJSON(), swagger);
});

// Renders Swagger-UI and passes YAML-output of /swagger
router.get("/docs", async () => {
  return AutoSwagger.default.ui("/swagger", swagger);
  // return AutoSwagger.default.scalar("/swagger"); to use Scalar instead. If you want, you can pass proxy url as second argument here.
  // return AutoSwagger.default.rapidoc("/swagger", "view"); to use RapiDoc instead (pass "view" default, or "read" to change the render-style)
});
```

## 5️⃣ for AdonisJS v5

```js
import AutoSwagger from "adonis-autoswagger";
import swagger from "Config/swagger";
// returns swagger in YAML
Route.get("/swagger", async () => {
  return AutoSwagger.docs(Route.toJSON(), swagger);
});

// Renders Swagger-UI and passes YAML-output of /swagger
Route.get("/docs", async () => {
  return AutoSwagger.ui("/swagger", swagger);
});
```

### 👍️ Done

Visit `http://localhost:3333/docs` to see AutoSwagger in action.

### Functions

- `async docs(routes, conf)`: get the specification in YAML format
- `async json(routes, conf)`: get the specification in JSON format
- `ui(path, conf)`: get default swagger UI
- `rapidoc(path, style)`: get rapidoc UI
- `scalar(path, proxyUrl)`: get scalar UI
- `stoplight(path, theme)`: get stoplight elements UI
- `jsonToYaml(json)`: can be used to convert `json()` back to yaml

---

## 💡 Compatibility

For controllers to get detected properly, please load them lazily.

```ts
✅ const TestController = () => import('#controllers/test_controller')
❌ import TestController from '#controllers/test_controller'
```

## 🧑‍💻 Advanced usage

### Additional configuration

**info**
See [Swagger API General Info](https://swagger.io/docs/specification/api-general-info/) for details.

**securitySchemes**

Add/Overwrite security schemes [Swagger Authentication](https://swagger.io/docs/specification/authentication/) for details.

```ts
// example to override ApiKeyAuth
securitySchemes: {
  ApiKeyAuth: {
    type: "apiKey"
    in: "header",
    name: "X-API-Key"
  }
}
```

**defaultSecurityScheme**

Override the default security scheme.

- BearerAuth
- BasicAuth
- ApiKeyAuth
- your own defined under `securitySchemes`

**authMiddlewares**

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.

### Modify generated output

```ts
Route.get("/myswagger", async () => {
  const json = await AutoSwagger.json(Route.toJSON(), swagger);
  // modify json to your hearts content
  return AutoSwagger.jsonToYaml(json);
});

Route.get("/docs", async () => {
  return AutoSwagger.ui("/myswagger", swagger);
});
```

### Custom Paths in adonisJS v6

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.

```json
//...
"imports": {
  // ...
  "#interfaces/*": "./app/custom/path/interfaces/*.js"
  // ...
}
//...

```

---

## 📃 Configure

### `tagIndex`

Tags endpoints automatically

- If your routes are `/api/v1/products/...` then your tagIndex should be `3`
- If your routes are `/v1/products/...` then your tagIndex should be `2`
- If your routes are `/products/...` then your tagIndex should be `1`

### `ignore`

Ignores specified paths. When used with a wildcard (\*), AutoSwagger will ignore everything matching before/after the wildcard.
`/test/_`will ignore everything starting with`/test/`, whereas `\*/test`will ignore everything ending with`/test`.

### `common`

Sometimes you want to use specific parameters or headers on multiple responses.

_Example:_ Some resources use the same filter parameters or return the same headers.

Here's where you can set these and use them with `@paramUse()` and `@responseHeader() @use()`. See practical example for further details.

---

# 💫 Extend Controllers

## Add additional documentation to your Controller-files

**@summary** (only one)
A summary of what the action does

**@tag** (only one)
Set a custom tag for this action

**@description** (only one)
A detailed description of what the action does.

**@operationId** (only one)
An optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API..

**@responseBody** (multiple)

Format: `<status> - <return> - <description>`

`<return>` can be either a `<Schema>`, `<Schema[]>/` or a custom JSON `{}`

**@responseHeader** (multiple)

Format: `<status> - <name> - <description> - <meta>`

**@param`Type`** (multiple)

`Type` can be one of [Parameter Types](https://swagger.io/docs/specification/describing-parameters/) (first letter in uppercase)

**@requestBody** (only one)
A definition of the expected requestBody

Format: `<body>`

`<body>` can be either a `<Schema>`, `<Schema[]>/`, or a custom JSON `{}`

**@requestFormDataBody** (only one)
A definition of the expected requestBody that will be sent with formData format.

**Schema**
A model or a validator.
Format: `<Schema>`

**Custom format**

Format: `{"fieldname": {"type":"string", "format": "email"}}`
This format should be a valid openapi 3.x json.

---

# 🤘Examples

## `@responseBody` examples

```ts
@responseBody <status> - Lorem ipsum Dolor sit amet

@responseBody <status> // returns standard <status> message

@responseBody <status> - <Model> // returns model specification

@responseBody <status> - <Model[]> // returns model-array specification

@responseBody <status> - <Model>.with(relations, property1, property2.relations, property3.subproperty.relations) // returns a model and a defined relation

@responseBody <status> - <Model[]>.with(relations).exclude(property1, property2, property3.subproperty) // returns model specification

@responseBody <status> - <Model[]>.append("some":"valid json") // append additional properties to a Model

@responseBody <status> - <Model[]>.paginated() // helper function to return adonisJS conform structure like {"data": [], "meta": {}}

@responseBody <status> - <Model[]>.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.

@responseBody <status> - <Model>.only(property1, property2) // pick only specific properties

@requestBody <status> <myCustomValidator> // returns a validator object

@responseBody <status> - {"foo": "bar", "baz": "<Model>"} //returns custom json object and also parses the model
@responseBody <status> - ["foo", "bar"] //returns custom json array
```

## `@paramPath` and `@paramQuery` examples

```ts
// basicaly same as @response, just without a status
@paramPath <paramName> - Description - (meta)
@paramQuery <paramName> - Description - (meta)

@paramPath id - The ID of the source - @type(number) @required
@paramPath slug - The ID of the source - @type(string)

@paramQuery q - Search term - @type(string) @required
@paramQuery page - the Page number - @type(number)

```

## `@requestBody` examples

```ts
// basicaly same as @response, just without a status
@requestBody <Model> // Expects model specification
@requestBody <myCustomValidator> // Expects validator specification
@requestBody <Model>.with(relations) // Expects model and its relations
@requestBody <Model[]>.append("some":"valid json") // append additional properties to a Model
@requestBody {"foo": "bar"} // Expects a specific JSON
```

## `@requestFormDataBody` examples

```ts
// Providing a raw JSON
@requestFormDataBody {"name":{"type":"string"},"picture":{"type":"string","format":"binary"}} // Expects a valid OpenAPI 3.x JSON
```

```ts
// Providing a Model, and adding additional fields
@requestFormDataBody <Model> // Expects a valid OpenAPI 3.x JSON
@requestFormDataBody <Model>.exclude(property1).append("picture":{"type":"string","format":"binary"}) // Expects a valid OpenAPI 3.x JSON
```

---

# **Practical example**

`config/swagger.ts`

```ts
export default {
  path: __dirname + "../",
  title: "YourProject",
  version: "1.0.0",
  tagIndex: 2,
  ignore: ["/swagger", "/docs", "/v1", "/", "/something/*", "*/something"],
  common: {
    parameters: {
      sortable: [
        {
          in: "query",
          name: "sortBy",
          schema: { type: "string", example: "foo" },
        },
        {
          in: "query",
          name: "sortType",
          schema: { type: "string", example: "ASC" },
        },
      ],
    },
    headers: {
      paginated: {
        "X-Total-Pages": {
          description: "Total amount of pages",
          schema: { type: "integer", example: 5 },
        },
        "X-Total": {
          description: "Total amount of results",
          schema: { type: "integer", example: 100 },
        },
        "X-Per-Page": {
          description: "Results per page",
          schema: { type: "integer", example: 20 },
        },
      },
    },
  },
};
```

`app/Controllers/Http/SomeController.ts`

```ts
export default class SomeController {
  /**
   * @index
   * @operationId getProducts
   * @description Returns array of producs and it's relations
   * @responseBody 200 - <Product[]>.with(relations)
   * @paramUse(sortable, filterable)
   * @responseHeader 200 - @use(paginated)
   * @responseHeader 200 - X-pages - A description of the header - @example(test)
   */
  public async index({ request, response }: HttpContextContract) {}

  /**
   * @show
   * @paramPath id - Describe the path param - @type(string) @required
   * @paramQuery foo - Describe the query param - @type(string) @required
   * @description Returns a product with it's relation on user and user relations
   * @responseBody 200 - <Product>.with(user, user.relations)
   * @responseBody 404
   */
  public async show({ request, response }: HttpContextContract) {}

  /**
   * @update
   * @responseBody 200
   * @responseBody 404 - Product could not be found
   * @requestBody <Product>
   */
  public async update({ request, response }: HttpContextContract) {}

  /**
   * @myCustomFunction
   * @summary Lorem ipsum dolor sit amet
   * @paramPath provider - The login provider to be used - @enum(google, facebook, apple)
   * @responseBody 200 - {"token": "xxxxxxx"}
   * @requestBody {"code": "xxxxxx"}
   */
  public async myCustomFunction({ request, response }: HttpContextContract) {}
}
```

---

## What does it do?

AutoSwagger tries to extracat as much information as possible to generate swagger-docs for you.

## Paths

Automatically generates swagger path-descriptions, based on your application routes. It also detects endpoints, protected by the auth-middlware.

![paths](https://i.imgur.com/EnPw6xT.png)

### Responses and RequestBody

Generates responses and requestBody based on your simple Controller-Annotation (see Examples)

---

## Schemas

### Models

Automatically generates swagger schema-descriptions based on your models

![alt](https://i.imgur.com/FEdLplp.png)

### Interfaces

Instead of using `param: any` you can now use custom interfaces `param: UserDetails`. The interfaces files need to be located at `app/Interfaces/`

### Enums

If you use enums in your models, AutoSwagger will detect them from `app/Types/` folder and add them to the schema.
If you want to add enum on ExampleValue, you can use `.append(enumFieldExample)`

Example:

```ts
 @responseBody 200 - <Model>.with(relations).append(enumFieldExample)
```

## Extend Models

Add additional documentation to your Models properties.

### SoftDelete

Either use `compose(BaseModel, SoftDeletes)` or add a line `@swagger-softdeletes` to your Model.

## Attention

The below comments MUST be placed **1 line** above the property.

---

**@no-swagger**
Although, autoswagger detects `serializeAs: null` fields automatically, and does not show them. You can use @no-swagger for other fields.

**@enum(foo, bar)**
If a field has defined values, you can add them into an enum. This is usesfull for something like a status field.

**@format(string)**
Specify a format for that field, i.e. uuid, email, binary, etc...

**@example(foo bar)**
Use this field to provide own example values for specific fields

**@props({"minLength": 10, "foo": "bar"})**
Use this field to provide additional properties to a field, like minLength, maxLength, etc. Needs to bee valid JSON.

**@required**
Specify that the field is required

```ts
// SomeModel.js
@hasMany(() => ProductView)
// @no-swagger
public views: HasMany<typeof ProductView>


@column()
// @enum(pending, active, deleted)
public status: string

@column()
// @example(johndoe@example.com)
public email: string

@column()
// @props({"minLength": 10})
public age: number

```

---

## Production environment

> [!WARNING]
> Make sure **NODE_ENV=production** in your production environment or whatever you set in `options.productionEnv`

To make it work in production environments, additional steps are required

- Create a new command for `docs:generate` [See official documentation](https://docs.adonisjs.com/guides/ace/creating-commands)

  - This should create a new file in `commands/DocsGenerate.ts`

- 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`

- Modify `/start/env.ts` as follows

```ts
//...
// this is necessary to make sure that the `DocsGenerate` command will run in CI/CD pipelines without setting environment variables
const isNodeAce = process.argv.some(
  (arg) => arg.endsWith("/ace") || arg === "ace"
);

export default await Env.create(
  new URL("../", import.meta.url),
  isNodeAce
    ? {}
    : {
        // leave other settings as is
        NODE_ENV: Env.schema.enum([
          "development",
          "production",
          "test",
        ] as const),
        PORT: Env.schema.number(),
      }
);

//...
```

- Execute the following

```bash
node ace docs:generate
node ace build --production
cp swagger.yml build/
```

## Known Issues

- Interfaces with objects are not working like `interface Test {foo: {bar: string}}`
  - Solution, just extract the object as it's own interface


================================================
FILE: package.json
================================================
{
  "name": "adonis-autoswagger",
  "version": "3.73.0",
  "description": "Auto-Generate swagger docs for AdonisJS",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "watchexec -e ts -- tsc",
    "build": "tsc",
    "prepare": "npm run build"
  },
  "author": "Adis Durakovic <adis.durakovic@gmail.com>",
  "repository": {
    "type": "git",
    "url": "https://github.com/ad-on-is/adonis-autoswagger"
  },
  "license": "MIT",
  "dependencies": {
    "@vinejs/vine": "^2.1.0",
    "abstract-syntax-tree": "^2.20.5",
    "change-case": "^4.1.2",
    "espree": "^9.3.1",
    "extract-comments": "^1.1.0",
    "http-status-code": "^2.1.0",
    "json-to-pretty-yaml": "^1.2.2",
    "lodash": "^4.17.21",
    "parse-imports": "^1.1.2",
    "typescript": "^5.8.2",
    "typescript-parser": "^2.6.1"
  },
  "devDependencies": {
    "@types/lodash": "^4.14.202",
    "@types/node": "^20.10.4"
  },
  "files": [
    "/dist"
  ],
  "prettier": {
    "trailingComma": "es5",
    "semi": true,
    "useTabs": false,
    "quoteProps": "consistent",
    "bracketSpacing": true,
    "arrowParens": "always"
  },
  "packageManager": "pnpm@8.11.0"
}


================================================
FILE: publish.sh
================================================
#!/bin/bash
pnpm version minor
git push && git push --tags

================================================
FILE: src/adonishelpers.ts
================================================
export function serializeV6Middleware(mw: any): string[] {
  return [...mw.all()].reduce<string[]>((result, one) => {
    if (typeof one === "function") {
      result.push(one.name || "closure");
      return result;
    }

    if ("name" in one && one.name) {
      result.push(one.name);
    }

    return result;
  }, []);
}

export async function serializeV6Handler(handler: any): Promise<any> {
  /**
   * Value is a controller reference
   */
  if ("reference" in handler) {
    return {
      type: "controller" as const,
      ...(await parseBindingReference(handler.reference)),
    };
  }

  /**
   * Value is an inline closure
   */
  return {
    type: "closure" as const,
    name: handler.name || "closure",
  };
}

export async function parseBindingReference(
  binding: string | [any | any, any]
): Promise<{ moduleNameOrPath: string; method: string }> {
  const parseImports = (await import("parse-imports")).default;
  /**
   * The binding reference is a magic string. It might not have method
   * name attached to it. Therefore we split the string and attempt
   * to find the method or use the default method name "handle".
   */
  if (typeof binding === "string") {
    const tokens = binding.split(".");
    if (tokens.length === 1) {
      return { moduleNameOrPath: binding, method: "handle" };
    }
    return { method: tokens.pop()!, moduleNameOrPath: tokens.join(".") };
  }

  const [bindingReference, method] = binding;

  /**
   * Parsing the binding reference for dynamic imports and using its
   * import value.
   */
  const imports = [...(await parseImports(bindingReference.toString()))];
  const importedModule = imports.find(
    ($import) => $import.isDynamicImport && $import.moduleSpecifier.value
  );
  if (importedModule) {
    return {
      moduleNameOrPath: importedModule.moduleSpecifier.value!,
      method: method || "handle",
    };
  }

  /**
   * Otherwise using the name of the binding reference.
   */
  return {
    moduleNameOrPath: bindingReference.name,
    method: method || "handle",
  };
}


================================================
FILE: src/autoswagger.ts
================================================
import YAML from "json-to-pretty-yaml";
import fs from "fs";
import path from "path";
import util from "util";
import HTTPStatusCode from "http-status-code";
import _ from "lodash";
import { isEmpty, isUndefined } from "lodash";
import { existsSync } from "fs";
import { scalarCustomCss } from "./scalarCustomCss";
import { serializeV6Middleware, serializeV6Handler } from "./adonishelpers";
import {
  InterfaceParser,
  ModelParser,
  CommentParser,
  RouteParser,
  ValidatorParser,
  EnumParser,
} from "./parsers";

import type { options, AdonisRoutes, v6Handler, AdonisRoute } from "./types";

import { mergeParams, formatOperationId } from "./helpers";
import ExampleGenerator, { ExampleInterfaces } from "./example";
// @ts-expect-error moduleResolution:nodenext issue 54523
import { VineValidator } from "@vinejs/vine";

export class AutoSwagger {
  private options: options;
  private schemas = {};
  private commentParser: CommentParser;
  private modelParser: ModelParser;
  private interfaceParser: InterfaceParser;
  private enumParser: EnumParser;
  private routeParser: RouteParser;
  private validatorParser: ValidatorParser;
  private customPaths = {};

  ui(url: string, options?: options) {
    const persistAuthString = options?.persistAuthorization
      ? "persistAuthorization: true,"
      : "";
    return `<!DOCTYPE html>
		<html lang="en">
		<head>
				<meta charset="UTF-8">
				<meta name="viewport" content="width=device-width, initial-scale=1.0">
				<meta http-equiv="X-UA-Compatible" content="ie=edge">
				<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.3/swagger-ui-standalone-preset.js"></script>
				<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.3/swagger-ui-bundle.js"></script>
				<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.3/swagger-ui.css" />
				<title>Documentation</title>
		</head>
		<body>
				<div id="swagger-ui"></div>
				<script>
						window.onload = function() {
							SwaggerUIBundle({
								url: "${url}",
								dom_id: '#swagger-ui',
								presets: [
									SwaggerUIBundle.presets.apis,
									SwaggerUIStandalonePreset
								],
								layout: "BaseLayout",
                ${persistAuthString}
							})
						}
				</script>
		</body>
		</html>`;
  }

  rapidoc(url: string, style = "view") {
    return (
      `
    <!doctype html> <!-- Important: must specify -->
    <html>
      <head>
        <meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 characters -->
        <script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
        <title>Documentation</title>
      </head>
      <body>
        <rapi-doc
          spec-url = "` +
      url +
      `"
      theme = "dark"
      bg-color = "#24283b"
      schema-style="tree"
      schema-expand-level = "10"
      header-color = "#1a1b26"
      allow-try = "true"
      nav-hover-bg-color = "#1a1b26"
      nav-bg-color = "#24283b"
      text-color = "#c0caf5"
      nav-text-color = "#c0caf5"
      primary-color = "#9aa5ce"
      heading-text = "Documentation"
      sort-tags = "true"
      render-style = "` +
      style +
      `"
      default-schema-tab = "example"
      show-components = "true"
      allow-spec-url-load = "false"
      allow-spec-file-load = "false"
      sort-endpoints-by = "path"

        > </rapi-doc>
      </body>
    </html>
    `
    );
  }

  scalar(url: string, proxyUrl: string = "https://proxy.scalar.com") {
    return `
      <!doctype html>
      <html>
        <head>
          <title>API Reference</title>
          <meta charset="utf-8" />
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1" />
          <style>
          ${scalarCustomCss}
          </style>
        </head>
        <body>
          <script
            id="api-reference"
            data-url="${url}"
            data-proxy-url="${proxyUrl}"></script>
          <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
        </body>
      </html>
    `;
  }

  stoplight(url: string, theme: "light" | "dark" = "dark") {
    return `
      <!doctype html>
      <html data-theme="${theme}">
        <head>
          <title>API Documentation - Stoplight</title>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		  <script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
          <link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
        </head>
        <body style="min-height:100vh">
	      <elements-api
		    style="display:block;height:100vh;width:100%;"
		    apiDescriptionUrl=${url}
		    router="hash"
		    layout="sidebar"
		  />
        </body>
      </html>
    `;
  }

  jsonToYaml(json: any) {
    return YAML.stringify(json);
  }

  async json(routes: any, options: options) {
    if (process.env.NODE_ENV === (options.productionEnv || "production")) {
      const str = await this.readFile(options.path, "json");
      return JSON.parse(str);
    }
    return await this.generate(routes, options);
  }

  async writeFile(routes: any, options: options) {
    const json = await this.generate(routes, options);
    const contents = this.jsonToYaml(json);
    const filePath = options.path + "swagger.yml";
    const filePathJson = options.path + "swagger.json";

    fs.writeFileSync(filePath, contents);
    fs.writeFileSync(filePathJson, JSON.stringify(json, null, 2));
  }

  private async readFile(rootPath, type = "yml") {
    const filePath = rootPath + "swagger." + type;
    const data = fs.readFileSync(filePath, "utf-8");
    if (!data) {
      console.error("Error reading file");
      return;
    }
    return data;
  }

  async docs(routes: any, options: options) {
    if (process.env.NODE_ENV === (options.productionEnv || "production")) {
      return this.readFile(options.path);
    }
    return this.jsonToYaml(await this.generate(routes, options));
  }

  private async generate(adonisRoutes: AdonisRoutes, options: options) {
    this.options = {
      ...{
        snakeCase: true,
        preferredPutPatch: "PUT",
        debug: false,
      },
      ...options,
    };

    const routes = adonisRoutes.root;
    this.options.appPath = this.options.path + "app";

    try {
      const pj = fs.readFileSync(path.join(this.options.path, "package.json"));

      const pjson = JSON.parse(pj.toString());
      if (pjson.imports) {
        Object.entries(pjson.imports).forEach(([key, value]) => {
          const k = (key as string).replaceAll("/*", "");
          this.customPaths[k] = (value as string)
            .replaceAll("/*.js", "")
            .replaceAll("./", "");
        });
      }
    } catch (e) {
      console.error(e);
    }

    this.commentParser = new CommentParser(this.options);
    this.routeParser = new RouteParser(this.options);
    this.modelParser = new ModelParser(this.options.snakeCase);
    this.interfaceParser = new InterfaceParser(this.options.snakeCase);
    this.validatorParser = new ValidatorParser();
    this.enumParser = new EnumParser();
    this.schemas = await this.getSchemas();
    if (this.options.debug) {
      console.log(this.options);
      console.log("Found Schemas", Object.keys(this.schemas));
      console.log("Using custom paths", this.customPaths);
    }
    this.commentParser.exampleGenerator = new ExampleGenerator(this.schemas);

    const docs = {
      openapi: "3.0.0",
      info: options.info || {
        title: options.title,
        version: options.version,
        description:
          options.description ||
          "Generated by AdonisJS AutoSwagger https://github.com/ad-on-is/adonis-autoswagger",
      },

      components: {
        responses: {
          Forbidden: {
            description: "Access token is missing or invalid",
          },
          Accepted: {
            description: "The request was accepted",
          },
          Created: {
            description: "The resource has been created",
          },
          NotFound: {
            description: "The resource has been created",
          },
          NotAcceptable: {
            description: "The resource has been created",
          },
        },
        securitySchemes: {
          BearerAuth: {
            type: "http",
            scheme: "bearer",
          },
          BasicAuth: {
            type: "http",
            scheme: "basic",
          },
          ApiKeyAuth: {
            type: "apiKey",
            in: "header",
            name: "X-API-Key",
          },
          ...this.options.securitySchemes,
        },
        schemas: this.schemas,
      },
      paths: {},
      tags: [],
    };
    let paths = {};

    let sscheme = "BearerAuth";
    if (this.options.defaultSecurityScheme) {
      sscheme = this.options.defaultSecurityScheme;
    }

    let securities = {
      "auth": { [sscheme]: ["access"] },
      "auth:api": { [sscheme]: ["access"] },
      ...this.options.authMiddlewares
        ?.map((am) => ({
          [am]: { [sscheme]: ["access"] },
        }))
        .reduce((acc, val) => ({ ...acc, ...val }), {}),
    };

    let globalTags = [];

    if (this.options.debug) {
      console.log("Route annotations:");
      console.log("Checking if controllers have propper comment annotations");
      console.log("-----");
    }

    for await (const route of routes) {
      let ignore = false;
      for (const i of options.ignore) {
        if (
          route.pattern == i ||
          (i.endsWith("*") && route.pattern.startsWith(i.slice(0, -1))) ||
          (i.startsWith("*") && route.pattern.endsWith(i.slice(1)))
        ) {
          ignore = true;
          break;
        }
      }
      if (ignore) continue;

      let security = [];
      const responseCodes = {
        GET: "200",
        POST: "201",
        DELETE: "202",
        PUT: "204",
      };

      if (!Array.isArray(route.middleware)) {
        route.middleware = serializeV6Middleware(route.middleware) as string[];
      }

      (route.middleware as string[]).forEach((m) => {
        if (typeof securities[m] !== "undefined") {
          security.push(securities[m]);
        }
      });

      let { tags, parameters, pattern } = this.routeParser.extractInfos(
        route.pattern
      );

      tags.forEach((tag) => {
        if (globalTags.filter((e) => e.name === tag).length > 0) return;
        if (tag === "") return;
        globalTags.push({
          name: tag,
          description: "Everything related to " + tag,
        });
      });

      const { sourceFile, action, customAnnotations, operationId } =
        await this.getDataBasedOnAdonisVersion(route);

      route.methods.forEach((method) => {
        let responses = {};
        if (method === "HEAD") return;

        if (
          route.methods.includes("PUT") &&
          route.methods.includes("PATCH") &&
          method !== this.options.preferredPutPatch
        )
          return;

        let description = "";
        let summary = "";
        let tag = "";
        let operationId: string;

        if (security.length > 0) {
          responses["401"] = {
            description: `Returns **401** (${HTTPStatusCode.getMessage(401)})`,
          };
          responses["403"] = {
            description: `Returns **403** (${HTTPStatusCode.getMessage(403)})`,
          };
        }

        let requestBody = {
          content: {
            "application/json": {},
          },
        };

        let actionParams = {};

        if (action !== "" && typeof customAnnotations[action] !== "undefined") {
          description = customAnnotations[action].description;
          summary = customAnnotations[action].summary;
          operationId = customAnnotations[action].operationId;
          responses = { ...responses, ...customAnnotations[action].responses };
          requestBody = customAnnotations[action].requestBody;
          actionParams = customAnnotations[action].parameters;
          tag = customAnnotations[action].tag;
        }
        parameters = mergeParams(parameters, actionParams);

        if (tag != "") {
          globalTags.push({
            name: tag.toUpperCase(),
            description: "Everything related to " + tag.toUpperCase(),
          });
          tags = [tag.toUpperCase()];
        }

        if (isEmpty(responses)) {
          responses[responseCodes[method]] = {
            description: HTTPStatusCode.getMessage(responseCodes[method]),
            content: {
              "application/json": {},
            },
          };
        } else {
          if (
            typeof responses[responseCodes[method]] !== "undefined" &&
            typeof responses[responseCodes[method]]["summary"] !== "undefined"
          ) {
            if (summary === "") {
              summary = responses[responseCodes[method]]["summary"];
            }
            delete responses[responseCodes[method]]["summary"];
          }
          if (
            typeof responses[responseCodes[method]] !== "undefined" &&
            typeof responses[responseCodes[method]]["description"] !==
              "undefined"
          ) {
            description = responses[responseCodes[method]]["description"];
          }
        }

        if (action !== "" && summary === "") {
          // Solve toLowerCase undefined exception
          // https://github.com/ad-on-is/adonis-autoswagger/issues/28
          tags[0] = tags[0] ?? "";

          switch (action) {
            case "index":
              summary = "Get a list of " + tags[0].toLowerCase();
              break;
            case "show":
              summary = "Get a single instance of " + tags[0].toLowerCase();
              break;
            case "update":
              summary = "Update " + tags[0].toLowerCase();
              break;
            case "destroy":
              summary = "Delete " + tags[0].toLowerCase();
              break;
            case "store":
              summary = "Create " + tags[0].toLowerCase();
              break;
            // frontend defaults
            case "create":
              summary = "Create (Frontend) " + tags[0].toLowerCase();
              break;
            case "edit":
              summary = "Update (Frontend) " + tags[0].toLowerCase();
              break;
          }
        }

        const sf = sourceFile.split("/").at(-1).replace(".ts", "");
        let m = {
          summary: `${summary}${action !== "" ? ` (${action})` : "route"}`,
          description:
            description + "\n\n _" + sourceFile + "_ - **" + action + "**",
          operationId: operationId,
          parameters: parameters,
          tags: tags,
          responses: responses,
          security: security,
        };

        if (method !== "GET" && method !== "DELETE") {
          m["requestBody"] = requestBody;
        }

        pattern = pattern.slice(1);
        if (pattern === "") {
          pattern = "/";
        }

        paths = {
          ...paths,
          [pattern]: { ...paths[pattern], [method.toLowerCase()]: m },
        };
      });
    }

    // filter unused tags
    const usedTags = _.uniq(
      Object.entries(paths)
        .map(([p, val]) => Object.entries(val)[0][1].tags)
        .flat()
    );

    docs.tags = globalTags.filter((tag) => usedTags.includes(tag.name));
    docs.paths = paths;
    return docs;
  }

  private async getDataBasedOnAdonisVersion(route: AdonisRoute) {
    let sourceFile = "";
    let action = "";
    let customAnnotations;
    let operationId = "";
    if (
      route.meta.resolvedHandler !== null &&
      route.meta.resolvedHandler !== undefined
    ) {
      if (
        typeof route.meta.resolvedHandler.namespace !== "undefined" &&
        route.meta.resolvedHandler.method !== "handle"
      ) {
        sourceFile = route.meta.resolvedHandler.namespace;

        action = route.meta.resolvedHandler.method;
        // If not defined by an annotation, use the combination of "controllerNameMethodName"
        if (action !== "" && isUndefined(operationId) && route.handler) {
          operationId = formatOperationId(route.handler as string);
        }
      }
    }

    let v6handler = <v6Handler>route.handler;
    if (
      v6handler.reference !== null &&
      v6handler.reference !== undefined &&
      v6handler.reference !== ""
    ) {
      if (!Array.isArray(v6handler.reference)) {
        // handles magic strings
        // router.resource('/test', '#controllers/test_controller')
        [sourceFile, action] = v6handler.reference.split(".");
        const split = sourceFile.split("/");

        if (split[0].includes("#")) {
          sourceFile = sourceFile.replaceAll(
            split[0],
            this.customPaths[split[0]]
          );
        } else {
          sourceFile = this.options.appPath + "/controllers/" + sourceFile;
        }
        operationId = formatOperationId(v6handler.reference);
      } else {
        // handles lazy import
        // const TestController = () => import('#controllers/test_controller')
        v6handler = await serializeV6Handler(v6handler);
        action = v6handler.method;
        sourceFile = v6handler.moduleNameOrPath;
        operationId = formatOperationId(sourceFile + "." + action);
        const split = sourceFile.split("/");
        if (split[0].includes("#")) {
          sourceFile = sourceFile.replaceAll(
            split[0],
            this.customPaths[split[0]]
          );
        } else {
          sourceFile = this.options.appPath + "/" + sourceFile;
        }
      }
    }

    if (sourceFile !== "" && action !== "") {
      sourceFile = sourceFile.replace("App/", "app/") + ".ts";
      sourceFile = sourceFile.replace(".js", "");

      customAnnotations = await this.commentParser.getAnnotations(
        sourceFile,
        action
      );
    }
    if (
      typeof customAnnotations !== "undefined" &&
      typeof customAnnotations.operationId !== "undefined" &&
      customAnnotations.operationId !== ""
    ) {
      operationId = customAnnotations.operationId;
    }
    if (this.options.debug) {
      if (sourceFile !== "") {
        console.log(
          typeof customAnnotations !== "undefined" &&
            !_.isEmpty(customAnnotations)
            ? `\x1b[32m✓ FOUND for ${action}\x1b[0m`
            : `\x1b[33m✗ MISSING for ${action}\x1b[0m`,

          `${sourceFile} (${route.methods[0].toUpperCase()} ${route.pattern})`
        );
      }
    }
    return { sourceFile, action, customAnnotations, operationId };
  }

  private async getSchemas() {
    let schemas = {
      Any: {
        description: "Any JSON object not defined as schema",
      },
    };

    schemas = {
      ...schemas,
      ...(await this.getInterfaces()),
      ...(await this.getSerializers()),
      ...(await this.getModels()),
      ...(await this.getValidators()),
      ...(await this.getEnums()),
    };

    return schemas;
  }

  private async getValidators() {
    const validators = {};
    let p6 = path.join(this.options.appPath, "validators");

    if (typeof this.customPaths["#validators"] !== "undefined") {
      // it's v6
      p6 = p6.replaceAll("app/validators", this.customPaths["#validators"]);
      p6 = p6.replaceAll("app\\validators", this.customPaths["#validators"]);
    }

    if (!existsSync(p6)) {
      if (this.options.debug) {
        console.log("Validators paths don't exist", p6);
      }
      return validators;
    }

    const files = await this.getFiles(p6, []);
    if (this.options.debug) {
      console.log("Found validator files", files);
    }

    try {
      for (let file of files) {
        if (/^[a-zA-Z]:/.test(file)) {
          file = "file:///" + file;
        }

        const val = await import(file);
        for (const [key, value] of Object.entries(val)) {
          if (value.constructor.name.includes("VineValidator")) {
            validators[key] = await this.validatorParser.validatorToObject(
              value as VineValidator<any, any>
            );
            validators[key].description = key + " (Validator)";
          }
        }
      }
    } catch (e) {
      console.log(
        "**You are probably using 'node ace serve --hmr', which is not supported yet. Use 'node ace serve --watch' instead.**"
      );
      console.error(e.message);
    }

    return validators;
  }

  private async getSerializers() {
    const serializers = {};
    let p6 = path.join(this.options.appPath, "serializers");

    if (typeof this.customPaths["#serializers"] !== "undefined") {
      // it's v6
      p6 = p6.replaceAll("app/serializers", this.customPaths["#serializers"]);
      p6 = p6.replaceAll("app\\serializers", this.customPaths["#serializers"]);
    }

    if (!existsSync(p6)) {
      if (this.options.debug) {
        console.log("Serializers paths don't exist", p6);
      }
      return serializers;
    }

    const files = await this.getFiles(p6, []);
    if (this.options.debug) {
      console.log("Found serializer files", files);
    }

    for (let file of files) {
      if (/^[a-zA-Z]:/.test(file)) {
        file = "file:///" + file;
      }

      const val = await import(file);

      for (const [key, value] of Object.entries(val)) {
        if (key.indexOf("Serializer") > -1) {
          serializers[key] = value;
        }
      }
    }

    return serializers;
  }

  private async getModels() {
    const models = {};
    let p = path.join(this.options.appPath, "Models");
    let p6 = path.join(this.options.appPath, "models");

    if (typeof this.customPaths["#models"] !== "undefined") {
      // it's v6
      p6 = p6.replaceAll("app/models", this.customPaths["#models"]);
      p6 = p6.replaceAll("app\\models", this.customPaths["#models"]);
    }

    if (!existsSync(p) && !existsSync(p6)) {
      if (this.options.debug) {
        console.log("Model paths don't exist", p, p6);
      }
      return models;
    }
    if (existsSync(p6)) {
      p = p6;
    }
    const files = await this.getFiles(p, []);
    const readFile = util.promisify(fs.readFile);
    if (this.options.debug) {
      console.log("Found model files", files);
    }
    for (let file of files) {
      file = file.replace(".js", "");
      const data = await readFile(file, "utf8");
      file = file.replace(".ts", "");
      const split = file.split("/");
      let name = split[split.length - 1].replace(".ts", "");
      file = file.replace("app/", "/app/");
      const parsed = this.modelParser.parseModelProperties(data);
      if (parsed.name !== "") {
        name = parsed.name;
      }
      let schema = {
        type: "object",
        required: parsed.required,
        properties: parsed.props,
        description: name + " (Model)",
      };
      models[name] = schema;
    }
    return models;
  }

  private async getInterfaces() {
    let interfaces = {
      ...ExampleInterfaces.paginationInterface(),
    };
    let p = path.join(this.options.appPath, "Interfaces");
    let p6 = path.join(this.options.appPath, "interfaces");

    if (typeof this.customPaths["#interfaces"] !== "undefined") {
      // it's v6
      p6 = p6.replaceAll("app/interfaces", this.customPaths["#interfaces"]);
      p6 = p6.replaceAll("app\\interfaces", this.customPaths["#interfaces"]);
    }

    if (!existsSync(p) && !existsSync(p6)) {
      if (this.options.debug) {
        console.log("Interface paths don't exist", p, p6);
      }
      return interfaces;
    }
    if (existsSync(p6)) {
      p = p6;
    }
    const files = await this.getFiles(p, []);
    if (this.options.debug) {
      console.log("Found interfaces files", files);
    }
    const readFile = util.promisify(fs.readFile);
    for (let file of files) {
      file = file.replace(".js", "");
      const data = await readFile(file, "utf8");
      file = file.replace(".ts", "");
      interfaces = {
        ...interfaces,
        ...this.interfaceParser.parseInterfaces(data),
      };
    }

    return interfaces;
  }

  private async getFiles(dir, files_) {
    const fs = require("fs");
    files_ = files_ || [];
    var files = await fs.readdirSync(dir);
    for (let i in files) {
      var name = dir + "/" + files[i];
      if (fs.statSync(name).isDirectory()) {
        await this.getFiles(name, files_);
      } else {
        files_.push(name);
      }
    }
    return files_;
  }

  private async getEnums() {
    let enums = {};

    const enumParser = new EnumParser();

    let p = path.join(this.options.appPath, "Types");
    let p6 = path.join(this.options.appPath, "types");

    if (typeof this.customPaths["#types"] !== "undefined") {
      // it's v6
      p6 = p6.replaceAll("app/types", this.customPaths["#types"]);
      p6 = p6.replaceAll("app\\types", this.customPaths["#types"]);
    }

    if (!existsSync(p) && !existsSync(p6)) {
      if (this.options.debug) {
        console.log("Enum paths don't exist", p, p6);
      }
      return enums;
    }

    if (existsSync(p6)) {
      p = p6;
    }

    const files = await this.getFiles(p, []);
    if (this.options.debug) {
      console.log("Found enum files", files);
    }

    const readFile = util.promisify(fs.readFile);
    for (let file of files) {
      file = file.replace(".js", "");
      const data = await readFile(file, "utf8");
      file = file.replace(".ts", "");
      const split = file.split("/");
      const name = split[split.length - 1].replace(".ts", "");
      file = file.replace("app/", "/app/");

      const parsedEnums = enumParser.parseEnums(data);
      enums = {
        ...enums,
        ...parsedEnums,
      };
    }

    return enums;
  }
}


================================================
FILE: src/example.ts
================================================
import { snakeCase } from "lodash";
import { getBetweenBrackets } from "./helpers";
export default class ExampleGenerator {
  public schemas = {};
  constructor(schemas: any) {
    this.schemas = schemas;
  }

  jsonToRef(json) {
    const jsonObjectIsArray = Array.isArray(json);
    let out = {};
    let outArr = [];
    for (let [k, v] of Object.entries(json)) {
      if (typeof v === "object") {
        if (!Array.isArray(v)) {
          v = this.jsonToRef(v);
        }
      }
      if (typeof v === "string") {
        v = this.parseRef(v, true);
      }

      if (jsonObjectIsArray) {
        outArr.push(v);
      } else {
        out[k] = v;
      }
    }
    return outArr.length > 0 ? outArr.flat() : out;
  }

  parseRef(line: string, exampleOnly = false) {
    let rawRef = line.substring(line.indexOf("<") + 1, line.lastIndexOf(">"));

    if (rawRef === "") {
      if (exampleOnly) {
        return line;
      }
      // No format valid, returning the line as text/plain
      return {
        content: {
          "text/plain": {
            example: line,
          },
        },
      };
    }

    let inc = getBetweenBrackets(line, "with");
    let exc = getBetweenBrackets(line, "exclude");
    const append = getBetweenBrackets(line, "append");
    let only = getBetweenBrackets(line, "only");
    const paginated = getBetweenBrackets(line, "paginated");
    const serializer = getBetweenBrackets(line, "serialized");

    if (serializer) {
      // we override to be sure
      inc = "";
      exc = "";
      only = "";

      if (this.schemas[serializer].fields.pick) {
        only += this.schemas[serializer].fields.pick.join(",");
      }
      if (this.schemas[serializer].fields.omit) {
        exc += this.schemas[serializer].fields.omit.join(",");
      }
      if (this.schemas[serializer].relations) {
        // get relations names and add them to inc
        const relations = Object.keys(this.schemas[serializer].relations);
        inc = relations.join(",");

        // we need to add the relation name to only and also we add the relation fields we want to only
        // ex : comment,comment.id,comment.createdAt
        relations.forEach((relation) => {
          const relationFields = this.schemas[serializer].relations[
            relation
          ].map((field) => relation + "." + field);

          only += "," + relation + "," + relationFields.join(",");
        });
      }
    }

    let app = {};
    try {
      app = JSON.parse("{" + append + "}");
    } catch {}

    const cleanedRef = rawRef.replace("[]", "");

    let ex = {};
    try {
      ex = Object.assign(
        this.getSchemaExampleBasedOnAnnotation(cleanedRef, inc, exc, only),
        app
      );
    } catch (e) {
      console.error("Error", cleanedRef);
    }

    const { dataName, metaName } = this.getPaginatedData(line);

    const paginatedEx = {
      [dataName]: [ex],
      [metaName]: this.getSchemaExampleBasedOnAnnotation("PaginationMeta"),
    };

    const paginatedSchema = {
      type: "object",
      properties: {
        [dataName]: {
          type: "array",
          items: { $ref: "#/components/schemas/" + cleanedRef },
        },
        [metaName]: { $ref: "#/components/schemas/PaginationMeta" },
      },
    };

    const normalArraySchema = {
      type: "array",
      items: { $ref: "#/components/schemas/" + cleanedRef },
    };

    if (rawRef.includes("[]")) {
      if (exampleOnly) {
        return paginated === "true" ? paginatedEx : [ex];
      }
      return {
        content: {
          "application/json": {
            schema: paginated === "true" ? paginatedSchema : normalArraySchema,
            example: paginated === "true" ? paginatedEx : [ex],
          },
        },
      };
    }
    if (exampleOnly) {
      return ex;
    }

    return {
      content: {
        "application/json": {
          schema: {
            $ref: "#/components/schemas/" + rawRef,
          },
          example: ex,
        },
      },
    };
  }

  exampleByValidatorRule(rule: string) {
    switch (rule) {
      case "email":
        return "user@example.com";
      default:
        return "Some string";
    }
  }

  getSchemaExampleBasedOnAnnotation(
    schema: string,
    inc = "",
    exc = "",
    onl = "",
    first = "",
    parent = "",
    deepRels = [""]
  ) {
    let props = {};
    if (!this.schemas[schema]) {
      return props;
    }
    if (this.schemas[schema].example) {
      return this.schemas[schema].example;
    }

    let properties = this.schemas[schema].properties;
    let include = inc.toString().split(",");
    let exclude = exc.toString().split(",");
    let only = onl.toString().split(",");
    only = only.length === 1 && only[0] === "" ? [] : only;

    if (typeof properties === "undefined") return null;

    // skip nested if not requested
    if (
      parent !== "" &&
      schema !== "" &&
      parent.includes(".") &&
      this.schemas[schema].description.includes("Model") &&
      !inc.includes("relations") &&
      !inc.includes(parent) &&
      !inc.includes(parent + ".relations") &&
      !inc.includes(first + ".relations")
    ) {
      return null;
    }

    deepRels.push(schema);

    for (const [key, value] of Object.entries(properties)) {
      let isArray = false;
      if (exclude.includes(key)) continue;
      if (exclude.includes(parent + "." + key)) continue;

      if (
        key === "password" &&
        !include.includes("password") &&
        !only.includes("password")
      )
        continue;
      if (
        key === "password_confirmation" &&
        !include.includes("password_confirmation") &&
        !only.includes("password_confirmation")
      )
        continue;
      if (
        (key === "created_at" ||
          key === "updated_at" ||
          key === "deleted_at") &&
        exc.includes("timestamps")
      )
        continue;

      let rel = "";
      let example = value["example"];

      if (parent === "" && only.length > 0 && !only.includes(key)) continue;

      // for relations we can select the fields we want with this syntax
      // ex : comment.id,comment.createdAt
      if (
        parent !== "" &&
        only.length > 0 &&
        !only.includes(parent + "." + key)
      )
        continue;

      if (typeof value["$ref"] !== "undefined") {
        rel = value["$ref"].replace("#/components/schemas/", "");
      }

      if (
        typeof value["items"] !== "undefined" &&
        typeof value["items"]["$ref"] !== "undefined"
      ) {
        rel = value["items"]["$ref"].replace("#/components/schemas/", "");
      }

      if (typeof value["items"] !== "undefined") {
        isArray = true;
        example = value["items"]["example"];
      }

      if (rel !== "") {
        // skip related models of main schema
        if (
          parent === "" &&
          typeof this.schemas[rel] !== "undefined" &&
          this.schemas[rel].description?.includes("Model") &&
          !include.includes("relations") &&
          !include.includes(key)
        ) {
          continue;
        }

        if (
          parent !== "" &&
          !include.includes(parent + ".relations") &&
          !include.includes(parent + "." + key)
        ) {
          continue;
        }

        if (
          typeof value["items"] !== "undefined" &&
          typeof value["items"]["$ref"] !== "undefined"
        ) {
          rel = value["items"]["$ref"].replace("#/components/schemas/", "");
        }
        if (rel == "") {
          return;
        }

        let propdata: any = "";

        // if (!deepRels.includes(rel)) {
        // deepRels.push(rel);
        propdata = this.getSchemaExampleBasedOnAnnotation(
          rel,
          inc,
          exc,
          onl,
          parent,
          parent === "" ? key : parent + "." + key,
          deepRels
        );

        if (propdata === null) {
          continue;
        }

        props[key] = isArray ? [propdata] : propdata;
      } else {
        props[key] = isArray ? [example] : example;
      }
    }

    return props;
  }

  exampleByType(type) {
    switch (type) {
      case "string":
        return this.exampleByField("title");
      case "number":
        return Math.floor(Math.random() * 1000);
      case "integer":
        return Math.floor(Math.random() * 1000);
      case "boolean":
        return true;
      case "DateTime":
        return this.exampleByField("datetime");
      case "datetime":
        return this.exampleByField("datetime");
      case "date":
        return this.exampleByField("date");
      case "object":
        return {};
      default:
        return null;
    }
  }

  exampleByField(field, type: string = "") {
    const ex = {
      datetime: "2021-03-23T16:13:08.489+01:00",
      DateTime: "2021-03-23T16:13:08.489+01:00",
      date: "2021-03-23",
      title: "Lorem Ipsum",
      year: 2023,
      description: "Lorem ipsum dolor sit amet",
      name: "John Doe",
      full_name: "John Doe",
      first_name: "John",
      last_name: "Doe",
      email: "johndoe@example.com",
      address: "1028 Farland Street",
      street: "1028 Farland Street",
      country: "United States of America",
      country_code: "US",
      zip: 60617,
      city: "Chicago",
      password: "S3cur3P4s5word!",
      password_confirmation: "S3cur3P4s5word!",
      lat: 41.705,
      long: -87.475,
      price: 10.5,
      avatar: "https://example.com/avatar.png",
      url: "https://example.com",
    };
    if (typeof ex[field] !== "undefined") {
      return ex[field];
    }
    if (typeof ex[snakeCase(field)] !== "undefined") {
      return ex[snakeCase(field)];
    }
    return null;
  }

  getPaginatedData(line: string): { dataName: string; metaName: string } {
    const match = line.match(/<.*>\.paginated\((.*)\)/);
    if (!match) {
      return { dataName: "data", metaName: "meta" };
    }

    const params = match[1].split(",").map((s) => s.trim());
    const dataName = params[0] || "data";
    const metaName = params[1] || "meta";

    return { dataName, metaName };
  }
}

export abstract class ExampleInterfaces {
  public static paginationInterface() {
    return {
      PaginationMeta: {
        type: "object",
        properties: {
          total: { type: "number", example: 100, nullable: false },
          page: { type: "number", example: 2, nullable: false },
          perPage: { type: "number", example: 10, nullable: false },
          currentPage: { type: "number", example: 3, nullable: false },
          lastPage: { type: "number", example: 10, nullable: false },
          firstPage: { type: "number", example: 1, nullable: false },
          lastPageUrl: {
            type: "string",
            example: "/?page=10",
            nullable: false,
          },
          firstPageUrl: {
            type: "string",
            example: "/?page=1",
            nullable: false,
          },
          nextPageUrl: { type: "string", example: "/?page=6", nullable: false },
          previousPageUrl: {
            type: "string",
            example: "/?page=5",
            nullable: false,
          },
        },
      },
    };
  }
}


================================================
FILE: src/helpers.ts
================================================
/**
 * Check if a string is a valid JSON
 */
import { camelCase, isEmpty, isUndefined, snakeCase, startCase } from "lodash";
export function isJSONString(str: string): boolean {
  try {
    JSON.parse(str);
    return true;
  } catch (error) {
    return false;
  }
}

export function getBetweenBrackets(value: string, start: string) {
  let match = value.match(new RegExp(start + "\\(([^()]*)\\)", "g"));

  if (match !== null) {
    let m = match[0].replace(start + "(", "").replace(")", "");

    if (start !== "example") {
      m = m.replace(/ /g, "");
    }
    if (start === "paginated") {
      return "true";
    }
    return m;
  }

  return "";
}

export function mergeParams(initial, custom) {
  let merge = Object.assign(initial, custom);
  let params = [];
  for (const [key, value] of Object.entries(merge)) {
    params.push(value);
  }

  return params;
}

/**
 * Helpers
 */

export function formatOperationId(inputString: string): string {
  // Remove non-alphanumeric characters and split the string into words
  const cleanedWords = inputString.replace(/[^a-zA-Z0-9]/g, " ").split(" ");

  // Pascal casing words
  const pascalCasedWords = cleanedWords.map((word) =>
    startCase(camelCase(word))
  );

  // Generate operationId by joining every parts
  const operationId = pascalCasedWords.join();

  // CamelCase the operationId
  return camelCase(operationId);
}


================================================
FILE: src/index.ts
================================================
import { AutoSwagger } from "./autoswagger";
export default new AutoSwagger();


================================================
FILE: src/parsers.ts
================================================
import HTTPStatusCode from "http-status-code";
import { isJSONString, getBetweenBrackets } from "./helpers";
import util from "util";
import extract from "extract-comments";
import fs from "fs";
import {
  camelCase,
  isEmpty,
  isUndefined,
  max,
  min,
  snakeCase,
  startCase,
} from "lodash";
import ExampleGenerator from "./example";
import type { options, AdonisRoutes, v6Handler } from "./types";
import { standardTypes } from "./types";
import _ from "lodash";
// @ts-expect-error moduleResolution:nodenext issue 54523
import { VineValidator } from "@vinejs/vine";

export class CommentParser {
  private parsedFiles: { [file: string]: string } = {};
  public exampleGenerator: ExampleGenerator;

  options: options;

  constructor(options: options) {
    this.options = options;
  }

  private parseAnnotations(lines: string[]) {
    let summary = "";
    let tag = "";
    let description = "";
    let operationId;
    let responses = {};
    let requestBody;
    let parameters = {};
    let headers = {};
    lines.forEach((line) => {
      if (line.startsWith("@summary")) {
        summary = line.replace("@summary ", "");
      }
      if (line.startsWith("@tag")) {
        tag = line.replace("@tag ", "");
      }

      if (line.startsWith("@description")) {
        description = line.replace("@description ", "");
      }

      if (line.startsWith("@operationId")) {
        operationId = line.replace("@operationId ", "");
      }

      if (line.startsWith("@responseBody")) {
        responses = {
          ...responses,
          ...this.parseResponseBody(line),
        };
      }
      if (line.startsWith("@responseHeader")) {
        const header = this.parseResponseHeader(line);
        if (header === null) {
          console.error("Error with line: " + line);
          return;
        }
        headers[header["status"]] = {
          ...headers[header["status"]],
          ...header["header"],
        };
      }
      if (line.startsWith("@requestBody")) {
        requestBody = this.parseBody(line, "requestBody");
      }
      if (line.startsWith("@requestFormDataBody")) {
        const parsedBody = this.parseRequestFormDataBody(line);
        if (parsedBody) {
          requestBody = parsedBody;
        }
      }
      if (line.startsWith("@param")) {
        parameters = { ...parameters, ...this.parseParam(line) };
      }
    });

    for (const [key, value] of Object.entries(responses)) {
      if (typeof headers[key] !== undefined) {
        responses[key]["headers"] = headers[key];
      }
      if (!responses[key]["description"]) {
        responses[key][
          "description"
        ] = `Returns **${key}** (${HTTPStatusCode.getMessage(key)}) as **${Object.entries(responses[key]["content"])[0][0]
        }**`;
      }
    }

    return {
      description,
      responses,
      requestBody,
      parameters,
      summary,
      operationId,
      tag,
    };
  }

  private parseParam(line: string) {
    let where = "path";
    let required = true;
    let type = "string";
    let example: any = null;
    let enums = [];

    if (line.startsWith("@paramUse")) {
      let use = getBetweenBrackets(line, "paramUse");
      const used = use.split(",");
      let h = [];
      used.forEach((u) => {
        if (typeof this.options.common.parameters[u] === "undefined") {
          return;
        }
        const common = this.options.common.parameters[u];
        h = [...h, ...common];
      });

      return h;
    }

    if (line.startsWith("@paramPath")) {
      required = false;
    }
    if (line.startsWith("@paramQuery")) {
      required = false;
    }

    let m = line.match("@param([a-zA-Z]*)");
    if (m !== null) {
      where = m[1].toLowerCase();
      line = line.replace(m[0] + " ", "");
    }

    let [param, des, meta] = line.split(" - ");
    if (typeof param === "undefined") {
      return;
    }
    if (typeof des === "undefined") {
      des = "";
    }

    if (typeof meta !== "undefined") {
      if (meta.includes("@required")) {
        required = true;
      }
      let en = getBetweenBrackets(meta, "enum");
      example = getBetweenBrackets(meta, "example");
      const mtype = getBetweenBrackets(meta, "type");
      if (mtype !== "") {
        type = mtype;
      }
      if (en !== "") {
        enums = en.split(",");
        example = enums[0];
      }
    }

    let p = {
      in: where,
      name: param,
      description: des,
      schema: {
        example: example,
        type: type,
      },
      required: required,
    };

    if (enums.length > 1) {
      p["schema"]["enum"] = enums;
    }

    return { [param]: p };
  }

  private parseResponseHeader(responseLine: string) {
    let description = "";
    let example: any = "";
    let type = "string";
    let enums = [];
    const line = responseLine.replace("@responseHeader ", "");
    let [status, name, desc, meta] = line.split(" - ");

    if (typeof status === "undefined" || typeof name === "undefined") {
      return null;
    }

    if (typeof desc !== "undefined") {
      description = desc;
    }

    if (name.includes("@use")) {
      let use = getBetweenBrackets(name, "use");
      const used = use.split(",");
      let h = {};
      used.forEach((u) => {
        if (typeof this.options.common.headers[u] === "undefined") {
          return;
        }
        const common = this.options.common.headers[u];
        h = { ...h, ...common };
      });

      return {
        status: status,
        header: h,
      };
    }

    if (typeof meta !== "undefined") {
      example = getBetweenBrackets(meta, "example");
      const mtype = getBetweenBrackets(meta, "type");
      if (mtype !== "") {
        type = mtype;
      }
    }

    if (example === "" || example === null) {
      switch (type) {
        case "string":
          example = "string";
          break;
        case "integer":
          example = 1;
          break;
        case "float":
          example = 1.5;
          break;
      }
    }

    let h = {
      schema: { type: type, example: example },
      description: description,
    };

    if (enums.length > 1) {
      h["schema"]["enum"] = enums;
    }
    return {
      status: status,
      header: {
        [name]: h,
      },
    };
  }

  private parseResponseBody(responseLine: string) {
    let responses = {};
    const line = responseLine.replace("@responseBody ", "");
    let [status, res, desc] = line.split(" - ");
    if (typeof status === "undefined") return;
    responses[status] = this.parseBody(res, "responseBody");
    responses[status]["description"] = desc;
    return responses;
  }

  private parseRequestFormDataBody(rawLine: string) {
    const line = rawLine.replace("@requestFormDataBody ", "");
    let json = {},
      required = [];
    const isJson = isJSONString(line);
    if (!isJson) {
      // try to get json from reference
      let rawRef = line.substring(line.indexOf("<") + 1, line.lastIndexOf(">"));

      const cleandRef = rawRef.replace("[]", "");
      if (cleandRef === "") {
        return;
      }
      const parsedRef = this.exampleGenerator.parseRef(line, true);
      let props = [];
      const ref = this.exampleGenerator.schemas[cleandRef];
      const ks = [];
      if (ref.required && Array.isArray(ref.required))
        required.push(...ref.required);
      Object.entries(ref.properties).map(([key, value]) => {
        if (typeof parsedRef[key] === "undefined") {
          return;
        }
        ks.push(key);
        if (value["required"]) required.push(key);
        props.push({
          [key]: {
            type:
              typeof value["type"] === "undefined" ? "string" : value["type"],
            format:
              typeof value["format"] === "undefined"
                ? "string"
                : value["format"],
          },
        });
      });
      const p = props.reduce((acc, curr) => ({ ...acc, ...curr }), {});
      const appends = Object.keys(parsedRef).filter((k) => !ks.includes(k));
      json = p;
      if (appends.length > 0) {
        appends.forEach((a) => {
          json[a] = parsedRef[a];
        });
      }
    } else {
      json = JSON.parse(line);
      for (let key in json) {
        if (json[key].required === "true") {
          required.push(key);
        }
      }
    }
    // No need to try/catch this JSON.parse as we already did that in the isJSONString function

    return {
      content: {
        "multipart/form-data": {
          schema: {
            type: "object",
            properties: json,
            required,
          },
        },
      },
    };
  }

  private parseBody(rawLine: string, type: string) {
    let line = rawLine.replace(`@${type} `, "");

    const isJson = isJSONString(line);

    if (isJson) {
      // No need to try/catch this JSON.parse as we already did that in the isJSONString function
      const json = JSON.parse(line);
      const o = this.jsonToObj(json);
      return {
        content: {
          "application/json": {
            schema: {
              type: Array.isArray(json) ? "array" : "object",
              ...(Array.isArray(json) ? { items: this.arrayItems(json) } : o),
            },

            example: this.exampleGenerator.jsonToRef(json),
          },
        },
      };
    }
    return this.exampleGenerator.parseRef(line);
  }

  arrayItems(json) {
    const oneOf = [];

    const t = typeof json[0];

    if (t === "string") {
      json.forEach((j) => {
        const value = this.exampleGenerator.parseRef(j);

        if (_.has(value, "content.application/json.schema.$ref")) {
          oneOf.push({
            $ref: value["content"]["application/json"]["schema"]["$ref"],
          });
        }
      });
    }

    if (oneOf.length > 0) {
      return { oneOf: oneOf };
    }
    return { type: typeof json[0] };
  }

  jsonToObj(json) {
    const o = {
      type: "object",
      properties: Object.keys(json)
        .map((key) => {
          const t = typeof json[key];
          const v = json[key];
          let value = v;
          if (t === "object") {
            value = this.jsonToObj(json[key]);
          }
          if (t === "string" && v.includes("<") && v.includes(">")) {
            value = this.exampleGenerator.parseRef(v);
            if (v.includes("[]")) {
              let ref = "";
              if (_.has(value, "content.application/json.schema.$ref")) {
                ref = value["content"]["application/json"]["schema"]["$ref"];
              }
              if (_.has(value, "content.application/json.schema.items.$ref")) {
                ref =
                  value["content"]["application/json"]["schema"]["items"][
                  "$ref"
                  ];
              }
              value = {
                type: "array",
                items: {
                  $ref: ref,
                },
              };
            } else {
              value = {
                $ref: value["content"]["application/json"]["schema"]["$ref"],
              };
            }
          }
          return {
            [key]: value,
          };
        })
        .reduce((acc, curr) => ({ ...acc, ...curr }), {}),
    };
    // console.dir(o, { depth: null });
    // console.log(json);
    return o;
  }

  async getAnnotations(file: string, action: string) {
    let annotations = {};
    let newdata = "";
    if (typeof file === "undefined") return;

    if (typeof this.parsedFiles[file] !== "undefined") {
      newdata = this.parsedFiles[file];
    } else {
      try {
        const readFile = util.promisify(fs.readFile);
        const data = await readFile(file, "utf8");
        for (const line of data.split("\n")) {
          const l = line.trim();
          if (!l.startsWith("@")) {
            newdata += l + "\n";
          }
        }
        this.parsedFiles[file] = newdata;
      } catch (e) {
        console.error("\x1b[31m✗ File not found\x1b[0m", file)
      }
    }

    const comments = extract(newdata);
    if (comments.length > 0) {
      comments.forEach((comment) => {
        if (comment.type !== "BlockComment") return;
        let lines = comment.value.split("\n").filter((l) => l != "");
        // fix for decorators
        if (lines[0].trim() !== "@" + action) return;
        lines = lines.filter((l) => l != "");

        annotations[action] = this.parseAnnotations(lines);
      });
    }
    return annotations;
  }
}

export class RouteParser {
  options: options;
  constructor(options: options) {
    this.options = options;
  }

  /*
    extract path-variables, tags and the uri-pattern
  */
  extractInfos(p: string) {
    let parameters = {};
    let pattern = "";
    let tags = [];
    let required: boolean;

    const split = p.split("/");
    if (split.length > this.options.tagIndex) {
      tags = [split[this.options.tagIndex].toUpperCase()];
    }
    split.forEach((part) => {
      if (part.startsWith(":")) {
        required = !part.endsWith("?");
        const param = part.replace(":", "").replace("?", "");
        part = "{" + param + "}";
        parameters = {
          ...parameters,
          [param]: {
            in: "path",
            name: param,
            schema: {
              type: "string",
            },
            required: required,
          },
        };
      }
      pattern += "/" + part;
    });
    if (pattern.endsWith("/")) {
      pattern = pattern.slice(0, -1);
    }
    return { tags, parameters, pattern };
  }
}

export class ModelParser {
  exampleGenerator: ExampleGenerator;
  snakeCase: boolean;
  constructor(snakeCase: boolean) {
    this.snakeCase = snakeCase;
    this.exampleGenerator = new ExampleGenerator({});
  }

  parseModelProperties(data) {
    let props = {};
    let required = [];
    // remove empty lines
    data = data.replace(/\t/g, "").replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "");
    const lines = data.split("\n");
    let softDelete = false;
    let name = "";
    lines.forEach((line, index) => {
      line = line.trim();
      // skip comments
      if (line.startsWith("export default class")) {
        name = line.split(" ")[3];
      }
      if (
        line.includes("@swagger-softdelete") ||
        line.includes("SoftDeletes")
      ) {
        softDelete = true;
      }

      if (
        line.startsWith("//") ||
        line.startsWith("/*") ||
        line.startsWith("*") ||
        line.startsWith("public static ") ||
        line.startsWith("private static ") ||
        line.startsWith("static ")
      )
        return;

      if (index > 0 && lines[index - 1].includes("serializeAs: null")) return;
      if (index > 0 && lines[index - 1].includes("@no-swagger")) return;
      if (
        !line.startsWith("public ") &&
        !line.startsWith("public get") &&
        !line.includes("declare ")
      )
        return;

      let s = [];

      if (line.includes("declare ")) {
        s = line.split("declare ");
      }
      if (line.startsWith("public ")) {
        if (line.startsWith("public get")) {
          s = line.split("public get");
          let s2 = s[1].replace(/;/g, "").split(":");
        } else {
          s = line.split("public ");
        }
      }

      let s2 = s[1].replace(/;/g, "").split(":");

      let field = s2[0];
      let type = s2[1] || "";
      type = type.trim();
      let enums = [];
      let format = "";
      let keyprops = {};
      let example: any = null;

      if (index > 0 && lines[index - 1].includes("@enum")) {
        const l = lines[index - 1];
        let en = getBetweenBrackets(l, "enum");
        if (en !== "") {
          enums = en.split(",");
          example = enums[0];
        }
      }

      if (index > 0 && lines[index - 1].includes("@format")) {
        const l = lines[index - 1];
        let en = getBetweenBrackets(l, "format");
        if (en !== "") {
          format = en;
        }
      }

      if (index > 0 && lines[index - 1].includes("@example")) {
        const l = lines[index - 1];
        let match = l.match(/example\(([^()]*)\)/g);
        if (match !== null) {
          const m = match[0].replace("example(", "").replace(")", "");
          example = m;
          if (type === "number") {
            example = parseInt(m);
          }
        }
      }

      if (index > 0 && lines[index - 1].includes("@required")) {
        required.push(field);
      }

      if (index > 0 && lines[index - 1].includes("@props")) {
        const l = lines[index - 1].replace("@props", "props");
        const j = getBetweenBrackets(l, "props");
        if (isJSONString(j)) {
          keyprops = JSON.parse(j);
        }
      }

      if (typeof type === "undefined") {
        type = "string";
        format = "";
      }

      field = field.trim();

      type = type.trim();

      //TODO: make oneOf
      if (type.includes(" | ")) {
        const types = type.split(" | ");
        type = types.filter((t) => t !== "null")[0];
      }

      field = field.replace("()", "");
      field = field.replace("get ", "");
      type = type.replace("{", "").trim();

      if (this.snakeCase) {
        field = snakeCase(field);
      }

      let indicator = "type";

      if (example === null) {
        example = "string";
      }

      // if relation to another model
      if (type.includes("typeof")) {
        s = type.split("typeof ");
        type = "#/components/schemas/" + s[1].slice(0, -1);
        indicator = "$ref";
      } else {
        if (standardTypes.includes(type.toLowerCase())) {
          type = type.toLowerCase();
        } else {
          // assume its a custom interface
          indicator = "$ref";
          type = "#/components/schemas/" + type;
        }
      }
      type = type.trim();
      let isArray = false;

      if (
        line.includes("HasMany") ||
        line.includes("ManyToMany") ||
        line.includes("HasManyThrough") ||
        type.includes("[]")
      ) {
        isArray = true;
        if (type.slice(type.length - 2, type.length) === "[]") {
          type = type.split("[]")[0];
        }
      }
      if (example === null || example === "string") {
        example =
          this.exampleGenerator.exampleByField(field) ||
          this.exampleGenerator.exampleByType(type);
      }

      if (type === "datetime") {
        indicator = "type";
        type = "string";
        format = "date-time";
      }

      if (type === "date") {
        indicator = "type";
        type = "string";
        format = "date";
      }

      if (field === "email") {
        indicator = "type";
        type = "string";
        format = "email";
      }
      if (field === "password") {
        indicator = "type";
        type = "string";
        format = "password";
      }

      if (enums.length > 0) {
        indicator = "type";
        type = "string";
      }

      if (type === "any") {
        indicator = "$ref";
        type = "#/components/schemas/Any";
      }

      let prop = {};
      if (type === "integer" || type === "number") {
        if (example === null || example === "string") {
          example = Math.floor(Math.random() * 1000);
        }
      }
      if (type === "boolean") {
        example = true;
      }

      prop[indicator] = type;
      prop["example"] = example;
      // if array
      if (isArray) {
        props[field] = { type: "array", items: prop };
      } else {
        props[field] = prop;
        if (format !== "") {
          props[field]["format"] = format;
        }
      }
      Object.entries(keyprops).map(([key, value]) => {
        props[field][key] = value;
      });
      if (enums.length > 0) {
        props[field]["enum"] = enums;
      }
    });

    if (softDelete) {
      props["deleted_at"] = {
        type: "string",
        format: "date-time",
        example: "2021-03-23T16:13:08.489+01:00",
      };
    }

    return { name: name, props: props, required: required };
  }
}

export class ValidatorParser {
  exampleGenerator: ExampleGenerator;
  constructor() {
    this.exampleGenerator = new ExampleGenerator({});
  }
  async validatorToObject(validator: VineValidator<any, any>) {
    // console.dir(validator.toJSON()["refs"], { depth: null });
    // console.dir(json, { depth: null });
    const obj = {
      type: "object",
      properties: this.parseSchema(
        validator.toJSON()["schema"]["schema"],
        validator.toJSON()["refs"]
      ),
    };
    // console.dir(obj, { depth: null });
    const testObj = this.objToTest(obj["properties"]);
    return await this.parsePropsAndMeta(obj, testObj, validator);
  }

  async parsePropsAndMeta(obj, testObj, validator: VineValidator<any, any>) {
    // console.log(Object.keys(errors));
    const { SimpleMessagesProvider } = await import("@vinejs/vine");
    const [e] = await validator.tryValidate(testObj, {
      messagesProvider: new SimpleMessagesProvider({
        required: "REQUIRED",
        string: "TYPE",
        object: "TYPE",
        number: "TYPE",
        boolean: "TYPE",
      }),
    });

    // if no errors, this means all object-fields are of type number (which we use by default)
    // and we can return the object
    if (e === null) {
      obj["example"] = testObj;
      return obj;
    }

    const msgs = e.messages;

    for (const m of msgs) {
      const err = m["message"];
      let objField = m["field"].replace(".", ".properties.");
      if (m["field"].includes(".0")) {
        objField = objField.replaceAll(`.0`, ".items");
      }
      if (err === "TYPE") {
        _.set(obj["properties"], objField, {
          ..._.get(obj["properties"], objField),
          type: m["rule"],
          example: this.exampleGenerator.exampleByType(m["rule"]),
        });
        if (m["rule"] === "string") {
          if (_.get(obj["properties"], objField)["minimum"]) {
            _.set(obj["properties"], objField, {
              ..._.get(obj["properties"], objField),
              minLength: _.get(obj["properties"], objField)["minimum"],
            });
            _.unset(obj["properties"], objField + ".minimum");
          }
          if (_.get(obj["properties"], objField)["maximum"]) {
            _.set(obj["properties"], objField, {
              ..._.get(obj["properties"], objField),
              maxLength: _.get(obj["properties"], objField)["maximum"],
            });
            _.unset(obj["properties"], objField + ".maximum");
          }
        }

        _.set(
          testObj,
          m["field"],
          this.exampleGenerator.exampleByType(m["rule"])
        );
      }

      if (err === "FORMAT") {
        _.set(obj["properties"], objField, {
          ..._.get(obj["properties"], objField),
          format: m["rule"],
          type: "string",
          example: this.exampleGenerator.exampleByValidatorRule(m["rule"]),
        });
        _.set(
          testObj,
          m["field"],
          this.exampleGenerator.exampleByValidatorRule(m["rule"])
        );
      }
    }

    // console.dir(obj, { depth: null });
    obj["example"] = testObj;
    return obj;
  }

  objToTest(obj) {
    const res = {};
    Object.keys(obj).forEach((key) => {
      if (obj[key]["type"] === "object") {
        res[key] = this.objToTest(obj[key]["properties"]);
      } else if (obj[key]["type"] === "array") {
        if (obj[key]["items"]["type"] === "object") {
          res[key] = [this.objToTest(obj[key]["items"]["properties"])];
        } else {
          res[key] = [obj[key]["items"]["example"]];
        }
      } else {
        res[key] = obj[key]["example"];
      }
    });
    return res;
  }

  parseSchema(json, refs) {
    const obj = {};
    for (const p of json["properties"]) {
      let meta: {
        minimum?: number;
        maximum?: number;
        choices?: any;
        pattern?: string;
      } = {};
      for (const v of p["validations"]) {
        if (refs[v["ruleFnId"]].options?.min) {
          meta = { ...meta, minimum: refs[v["ruleFnId"]].options.min };
        }
        if (refs[v["ruleFnId"]].options?.max) {
          meta = { ...meta, maximum: refs[v["ruleFnId"]].options.max };
        }
        if (refs[v["ruleFnId"]].options?.choices) {
          meta = { ...meta, choices: refs[v["ruleFnId"]].options.choices };
        }
        if (refs[v["ruleFnId"]].options?.toString().includes("/")) {
          meta = { ...meta, pattern: refs[v["ruleFnId"]].options.toString() };
        }
      }

      // console.dir(p, { depth: null });
      // console.dir(validations, { depth: null });
      // console.log(min, max, choices, regex);

      obj[p["fieldName"]] =
        p["type"] === "object"
          ? { type: "object", properties: this.parseSchema(p, refs) }
          : p["type"] === "array"
            ? {
              type: "array",
              items:
                p["each"]["type"] === "object"
                  ? {
                    type: "object",
                    properties: this.parseSchema(p["each"], refs),
                  }
                  : {
                    type: "number",
                    example: meta.minimum
                      ? meta.minimum
                      : this.exampleGenerator.exampleByType("number"),
                    ...meta,
                  },
            }
            : {
              type: "number",
              example: meta.minimum
                ? meta.minimum
                : this.exampleGenerator.exampleByType("number"),
              ...meta,
            };
      if (!p["isOptional"]) obj[p["fieldName"]]["required"] = true;
    }
    return obj;
  }
}

export class InterfaceParser {
  exampleGenerator: ExampleGenerator;
  snakeCase: boolean;
  schemas: any = {};

  constructor(snakeCase: boolean, schemas: any = {}) {
    this.snakeCase = snakeCase;
    this.exampleGenerator = new ExampleGenerator({});
    this.schemas = schemas;
  }

  objToExample(obj) {
    let example = {};
    Object.entries(obj).map(([key, value]) => {
      if (typeof value === "object") {
        example[key] = this.objToExample(value);
      } else {
        example[key] = this.exampleGenerator.exampleByType(value as string);
        if (example[key] === null) {
          example[key] = this.exampleGenerator.exampleByField(key);
        }
      }
    });
    return example;
  }

  parseProps(obj) {
    const no = {};
    Object.entries(obj).map(([f, value]) => {
      if (typeof value === "object") {
        no[f.replaceAll("?", "")] = {
          type: "object",
          nullable: f.includes("?"),
          properties: this.parseProps(value),
          example: this.objToExample(value),
        };
      } else {
        no[f.replaceAll("?", "")] = {
          ...this.parseType(value, f),
        };
      }
    });
    return no;
  }

  getInheritedProperties(baseType: string): any {

    if (this.schemas[baseType]?.properties) {
      return {
        properties: this.schemas[baseType].properties,
        required: this.schemas[baseType].required || []
      };
    }

    const cleanType = baseType
      .split('/')
      .pop()
      ?.replace('.ts', '')
      ?.replace(/^[#@]/, '');

    if (!cleanType) return { properties: {}, required: [] };

    if (this.schemas[cleanType]?.properties) {
      return {
        properties: this.schemas[cleanType].properties,
        required: this.schemas[cleanType].required || []
      };
    }

    const variations = [
      cleanType,
      `#models/${cleanType}`,
      cleanType.replace(/Model$/, ''),
      `${cleanType}Model`
    ];

    for (const variation of variations) {
      if (this.schemas[variation]?.properties) {
        return {
          properties: this.schemas[variation].properties,
          required: this.schemas[variation].required || []
        };
      }
    }

    return { properties: {}, required: [] };
  }

  parseInterfaces(data) {
    data = data.replace(/\t/g, "").replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "");

    let currentInterface = null;
    const interfaces = {};
    const interfaceDefinitions = new Map();

    const lines = data.split("\n");
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i].trim();
      const isDefault = line.startsWith("export default interface")

      if (line.startsWith("interface") || line.startsWith("export interface") || isDefault) {
        const sp = line.split(/\s+/)
        const idx = line.endsWith("}") ? sp.length - 1 : sp.length - 2
        const name = sp[idx].split(/[{\s]/)[0];
        const extendedTypes = this.parseExtends(line);
        interfaceDefinitions.set(name, {
          extends: extendedTypes,
          properties: {},
          required: [],
          startLine: i
        });
        currentInterface = name;
        continue;
      }

      if (currentInterface && line === "}") {
        currentInterface = null;
        continue;
      }

      if (currentInterface && line && !line.startsWith("//") && !line.startsWith("/*") && !line.startsWith("*")) {
        const def = interfaceDefinitions.get(currentInterface);
        if (def) {
          const previousLine = i > 0 ? lines[i - 1].trim() : "";
          const isRequired = previousLine.includes("@required");

          const [prop, type] = line.split(":").map(s => s.trim());
          if (prop && type) {
            const cleanProp = prop.replace("?", "");
            def.properties[cleanProp] = type.replace(";", "");


            if (isRequired || !prop.includes("?")) {
              def.required.push(cleanProp);
            }
          }
        }
      }
    }

    for (const [name, def] of interfaceDefinitions) {
      let allProperties = {};
      let requiredFields = new Set(def.required);

      for (const baseType of def.extends) {
        const baseSchema = this.schemas[baseType];
        if (baseSchema) {
          if (baseSchema.properties) {
            Object.assign(allProperties, baseSchema.properties);
          }

          if (baseSchema.required) {
            baseSchema.required.forEach(field => requiredFields.add(field));
          }
        }
      }

      Object.assign(allProperties, def.properties);

      const parsedProperties = {};
      for (const [key, value] of Object.entries(allProperties)) {
        if (typeof value === 'object' && value !== null && 'type' in value) {
          parsedProperties[key] = value;
        } else {
          parsedProperties[key] = this.parseType(value, key);
        }
      }

      const schema = {
        type: "object",
        properties: parsedProperties,
        required: Array.from(requiredFields),
        description: `${name}${def.extends.length ? ` extends ${def.extends.join(", ")}` : ""} (Interface)`
      };

      if (schema.required.length === 0) {
        delete schema.required;
      }

      interfaces[name] = schema;
    }

    return interfaces;
  }

  parseExtends(line: string): string[] {
    const matches = line.match(/extends\s+([^{]+)/);
    if (!matches) return [];

    return matches[1]
      .split(",")
      .map(type => type.trim())
      .map(type => {
        const cleanType = type.split('/').pop();
        return cleanType?.replace(/\.ts$/, '') || type;
      });
  }

  parseType(type: string | any, field: string) {
    if (typeof type === 'object' && type !== null && 'type' in type) {
      return type;
    }

    let isArray = false;
    if (typeof type === 'string' && type.includes("[]")) {
      type = type.replace("[]", "");
      isArray = true;
    }

    if (typeof type === 'string') {
      type = type.replace(/[;\r\n]/g, '').trim();
    }

    let prop: any = { type: type };
    let notRequired = field.includes("?");
    prop.nullable = notRequired;

    if (typeof type === 'string' && type.toLowerCase() === "datetime") {
      prop.type = "string";
      prop.format = "date-time";
      prop.example = "2021-03-23T16:13:08.489+01:00";
    } else if (typeof type === 'string' && type.toLowerCase() === "date") {
      prop.type = "string";
      prop.format = "date";
      prop.example = "2021-03-23";
    } else {
      const standardTypes = ["string", "number", "boolean", "integer"];
      if (typeof type === 'string' && !standardTypes.includes(type.toLowerCase())) {
        delete prop.type;
        prop.$ref = `#/components/schemas/${type}`;
      } else {
        if (typeof type === 'string') {
          prop.type = type.toLowerCase();
        }
        prop.example = this.exampleGenerator.exampleByType(type) ||
          this.exampleGenerator.exampleByField(field);
      }
    }

    if (isArray) {
      return {
        type: "array",
        items: prop
      };
    }

    return prop;
  }
}

export class EnumParser {
  constructor() { }

  parseEnums(data: string): Record<string, any> {
    const enums: Record<string, any> = {};
    const lines = data.split("\n");
    let currentEnum: string | null = null;
    let description: string | null = null;

    for (const line of lines) {
      const trimmedLine = line.trim();

      if (trimmedLine.startsWith("//")) {
        description = trimmedLine.slice(2).trim();
        continue;
      }

      if (
        trimmedLine.startsWith("enum") ||
        trimmedLine.startsWith("export enum")
      ) {
        const match = trimmedLine.match(/(?:export\s+)?enum\s+(\w+)/);
        if (match) {
          currentEnum = match[1];
          enums[currentEnum] = {
            type: "string",
            enum: [],
            properties: {},
            description: description || `${startCase(currentEnum)} enumeration`,
          };
          description = null;
        }
        continue;
      }

      if (currentEnum && trimmedLine !== "{" && trimmedLine !== "}") {
        const [key, value] = trimmedLine.split("=").map((s) => s.trim());
        if (key) {
          const enumValue = value ? this.parseEnumValue(value) : key;
          enums[currentEnum].enum.push(enumValue);
        }
      }

      if (trimmedLine === "}") {
        currentEnum = null;
      }
    }


    return enums;
  }

  private parseEnumValue(value: string): string {
    // Remove quotes and comma
    return value.replace(/['",]/g, "").trim();
  }
}


================================================
FILE: src/scalarCustomCss.ts
================================================
export const scalarCustomCss = `
/* basic theme */
.light-mode {
  --theme-background-1: #fff;
  --theme-background-2: #fafaf9;
  --theme-background-3: rgb(245 245 245);

  --theme-color-1: #21201c;
  --theme-color-2: #63635d;
  --theme-color-3: #8e8e8e;

  --theme-color-accent: #5a45ff;
  --theme-background-accent: #5a45ff1f;

  --theme-border-color: color(display-p3 0.913 0.912 0.903);
  --theme-code-language-color-supersede: var(--theme-color-1);
  --theme-code-languages-background-supersede: var(--theme-background-3);
}
.dark-mode {
  --theme-background-1: #0f0f0f;
  --theme-background-2: #222222;
  --theme-background-3: #272727;

  --theme-color-1: #e2e4e8;
  --theme-color-2: rgba(255, 255, 255, 0.62);
  --theme-color-3: #6a737d;

  --theme-color-accent: #e2ddfe;
  --theme-background-accent: #3c2d6a;

  --theme-border-color: rgba(255, 255, 255, 0.1);
  --theme-code-language-color-supersede: var(--theme-color-1);
  --theme-code-languages-background-supersede: var(--theme-background-3);
}

/* Document Sidebar */
.light-mode .t-doc__sidebar,
.dark-mode .t-doc__sidebar {
  --sidebar-background-1: var(--theme-background-1);
  --sidebar-color-1: var(--theme-color-1);
  --sidebar-color-2: var(--theme-color-2);
  --sidebar-border-color: var(--theme-border-color);

  --sidebar-item-hover-background: var(--theme-background-2);
  --sidebar-item-hover-color: currentColor;

  --sidebar-item-active-background: var(--theme-background-accent);
  --sidebar-color-active: var(--theme-color-accent);

  --sidebar-search-background: var(--theme-background-2);
  --sidebar-search-color: var(--theme-color-3);
  --sidebar-search-border-color: var(--theme-border-color);
}

/* advanced */
.light-mode {
  --theme-color-green: #0e766e;
  --theme-color-red: #e53935;
  --theme-color-yellow: #e2931d;
  --theme-color-blue: #0f766e;
  --theme-color-orange: #f76d47;
  --theme-color-purple: #4338ca;
}
.dark-mode {
  --theme-color-green: #0ad8b6;
  --theme-color-red: #e5484d;
  --theme-color-yellow: #eac063;
  --theme-color-blue: #6abaff;
  --theme-color-orange: #ff9b52;
  --theme-color-purple: #6550b9;
}
/* custom-theme */
.show-api-client-button:before {
  background: white !important;
}
.show-api-client-button span,
.show-api-client-button svg {
  color: var(--theme-background-1) !important;
}
.section:not(:last-of-type),
.section-container {
  border-top: none !important;
  border-bottom: none !important;
}
.section-container:after,
.tag-section-container section.section:after {
  content: "";
  width: 100%;
  height: 1px;
  position: absolute;
  top: 0;
  left: 0;
  background: repeating-linear-gradient(
    90deg,
    var(--theme-border-color) 0 4px,
    transparent 0 8px
  );
}
.section-container:nth-of-type(2):after {
  display: none;
}
.tag-section-container .section:first-of-type:after {
  display: none;
}
.sidebar {
  border-right: none !important;
}
.t-doc__sidebar {
  position: relative;
}
.t-doc__sidebar:after {
  content: "";
  width: 1px;
  height: 100%;
  position: absolute;
  right: 0;
  top: 0;
  background: repeating-linear-gradient(
    0deg,
    var(--theme-border-color) 0 4px,
    transparent 0 8px
  );
  display: block;
}
.download-cta .download-button,
.scalar-api-reference .section .markdown a {
  --theme-color-accent: var(--theme-color-1) !important;
  text-decoration: underline !important;
  cursor: pointer;
}`

================================================
FILE: src/types.ts
================================================

/**
 * Autoswagger interfaces
 */
export interface options {
  title?: string;
  ignore: string[];
  version?: string;
  description?: string;
  path: string;
  tagIndex: number;
  snakeCase: boolean;
  common: common;
  fileNameInSummary?: boolean;
  preferredPutPatch?: string;
  persistAuthorization?: boolean;
  appPath?: string;
  debug?: boolean;
  info?: any;
  securitySchemes?: any;
  productionEnv?: string;
  authMiddlewares?: string[];
  defaultSecurityScheme?: string;
}

export interface common {
  headers: any;
  parameters: any;
}

/**
 * Adonis routes
 */
export interface AdonisRouteMeta {
  resolvedHandler: {
    type: string;
    namespace?: string;
    method?: string;
  };
  resolvedMiddleware: Array<{
    type: string;
    args?: any[];
  }>;
}

export interface v6Handler {
  method?: string;
  moduleNameOrPath?: string;
  reference: string | any[];
  name: string;
}

export interface AdonisRoute {
  methods: string[];
  pattern: string;
  meta: AdonisRouteMeta;
  middleware: string[] | any;
  name?: string;
  params: string[];
  handler?: string | v6Handler;
}

export interface AdonisRoutes {
  root: AdonisRoute[];
}

export const standardTypes = [
  "string",
  "number",
  "integer",
  "datetime",
  "date",
  "boolean",
  "any",
]
  .map((type) => [type, type + "[]"])
  .flat();


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "lib": ["ES2022"],
    "target": "ES2022",
    "module": "Node16",
    "declaration": true,
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "esModuleInterop": true
  },
  "skipLibCheck": true,
  "include": ["src/**/*"]
}
Download .txt
gitextract_ofo9iakp/

├── .github/
│   └── workflows/
│       └── deploy.yml
├── .gitignore
├── DocsGenerate.ts.example
├── DocsGeneratev6.ts.example
├── LICENSE
├── README.md
├── package.json
├── publish.sh
├── src/
│   ├── adonishelpers.ts
│   ├── autoswagger.ts
│   ├── example.ts
│   ├── helpers.ts
│   ├── index.ts
│   ├── parsers.ts
│   ├── scalarCustomCss.ts
│   └── types.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (77 symbols across 6 files)

FILE: src/adonishelpers.ts
  function serializeV6Middleware (line 1) | function serializeV6Middleware(mw: any): string[] {
  function serializeV6Handler (line 16) | async function serializeV6Handler(handler: any): Promise<any> {
  function parseBindingReference (line 36) | async function parseBindingReference(

FILE: src/autoswagger.ts
  class AutoSwagger (line 27) | class AutoSwagger {
    method ui (line 38) | ui(url: string, options?: options) {
    method rapidoc (line 73) | rapidoc(url: string, style = "view") {
    method scalar (line 117) | scalar(url: string, proxyUrl: string = "https://proxy.scalar.com") {
    method stoplight (line 142) | stoplight(url: string, theme: "light" | "dark" = "dark") {
    method jsonToYaml (line 165) | jsonToYaml(json: any) {
    method json (line 169) | async json(routes: any, options: options) {
    method writeFile (line 177) | async writeFile(routes: any, options: options) {
    method readFile (line 187) | private async readFile(rootPath, type = "yml") {
    method docs (line 197) | async docs(routes: any, options: options) {
    method generate (line 204) | private async generate(adonisRoutes: AdonisRoutes, options: options) {
    method getDataBasedOnAdonisVersion (line 518) | private async getDataBasedOnAdonisVersion(route: AdonisRoute) {
    method getSchemas (line 612) | private async getSchemas() {
    method getValidators (line 631) | private async getValidators() {
    method getSerializers (line 679) | private async getSerializers() {
    method getModels (line 718) | private async getModels() {
    method getInterfaces (line 765) | private async getInterfaces() {
    method getFiles (line 805) | private async getFiles(dir, files_) {
    method getEnums (line 820) | private async getEnums() {

FILE: src/example.ts
  class ExampleGenerator (line 3) | class ExampleGenerator {
    method constructor (line 5) | constructor(schemas: any) {
    method jsonToRef (line 9) | jsonToRef(json) {
    method parseRef (line 32) | parseRef(line: string, exampleOnly = false) {
    method exampleByValidatorRule (line 154) | exampleByValidatorRule(rule: string) {
    method getSchemaExampleBasedOnAnnotation (line 163) | getSchemaExampleBasedOnAnnotation(
    method exampleByType (line 316) | exampleByType(type) {
    method exampleByField (line 339) | exampleByField(field, type: string = "") {
    method getPaginatedData (line 375) | getPaginatedData(line: string): { dataName: string; metaName: string } {
  method paginationInterface (line 390) | public static paginationInterface() {

FILE: src/helpers.ts
  function isJSONString (line 5) | function isJSONString(str: string): boolean {
  function getBetweenBrackets (line 14) | function getBetweenBrackets(value: string, start: string) {
  function mergeParams (line 32) | function mergeParams(initial, custom) {
  function formatOperationId (line 46) | function formatOperationId(inputString: string): string {

FILE: src/parsers.ts
  class CommentParser (line 22) | class CommentParser {
    method constructor (line 28) | constructor(options: options) {
    method parseAnnotations (line 32) | private parseAnnotations(lines: string[]) {
    method parseParam (line 111) | private parseParam(line: string) {
    method parseResponseHeader (line 188) | private parseResponseHeader(responseLine: string) {
    method parseResponseBody (line 260) | private parseResponseBody(responseLine: string) {
    method parseRequestFormDataBody (line 270) | private parseRequestFormDataBody(rawLine: string) {
    method parseBody (line 337) | private parseBody(rawLine: string, type: string) {
    method arrayItems (line 362) | arrayItems(json) {
    method jsonToObj (line 385) | jsonToObj(json) {
    method getAnnotations (line 432) | async getAnnotations(file: string, action: string) {
  class RouteParser (line 471) | class RouteParser {
    method constructor (line 473) | constructor(options: options) {
    method extractInfos (line 480) | extractInfos(p: string) {
  class ModelParser (line 516) | class ModelParser {
    method constructor (line 519) | constructor(snakeCase: boolean) {
    method parseModelProperties (line 524) | parseModelProperties(data) {
  class ValidatorParser (line 766) | class ValidatorParser {
    method constructor (line 768) | constructor() {
    method validatorToObject (line 771) | async validatorToObject(validator: VineValidator<any, any>) {
    method parsePropsAndMeta (line 786) | async parsePropsAndMeta(obj, testObj, validator: VineValidator<any, an...
    method objToTest (line 864) | objToTest(obj) {
    method parseSchema (line 882) | parseSchema(json, refs) {
  class InterfaceParser (line 943) | class InterfaceParser {
    method constructor (line 948) | constructor(snakeCase: boolean, schemas: any = {}) {
    method objToExample (line 954) | objToExample(obj) {
    method parseProps (line 969) | parseProps(obj) {
    method getInheritedProperties (line 988) | getInheritedProperties(baseType: string): any {
    method parseInterfaces (line 1031) | parseInterfaces(data) {
    method parseExtends (line 1128) | parseExtends(line: string): string[] {
    method parseType (line 1141) | parseType(type: string | any, field: string) {
  class EnumParser (line 1193) | class EnumParser {
    method constructor (line 1194) | constructor() { }
    method parseEnums (line 1196) | parseEnums(data: string): Record<string, any> {
    method parseEnumValue (line 1245) | private parseEnumValue(value: string): string {

FILE: src/types.ts
  type options (line 5) | interface options {
  type common (line 26) | interface common {
  type AdonisRouteMeta (line 34) | interface AdonisRouteMeta {
  type v6Handler (line 46) | interface v6Handler {
  type AdonisRoute (line 53) | interface AdonisRoute {
  type AdonisRoutes (line 63) | interface AdonisRoutes {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (108K chars).
[
  {
    "path": ".github/workflows/deploy.yml",
    "chars": 584,
    "preview": "name: Deploy\nrun-name: Deploying to npmjs 🥹🤞\non:\n  push:\n    tags:\n      - \"*\"\njobs:\n  Deploy:\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".gitignore",
    "chars": 19,
    "preview": "node_modules\ndist/*"
  },
  {
    "path": "DocsGenerate.ts.example",
    "chars": 550,
    "preview": "import { BaseCommand } from '@adonisjs/core/build/standalone'\nimport AutoSwagger from 'adonis-autoswagger'\nimport swagge"
  },
  {
    "path": "DocsGeneratev6.ts.example",
    "chars": 568,
    "preview": "import { BaseCommand } from '@adonisjs/core/ace'\nimport { CommandOptions } from '@adonisjs/core/types/ace'\nimport AutoSw"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2021 Adis Durakovic\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "README.md",
    "chars": 16958,
    "preview": "<h1 align=\"center\">\nAdonis AutoSwagger <br />\n<img src=\"https://upload.wikimedia.org/wikipedia/commons/a/ab/Swagger-logo"
  },
  {
    "path": "package.json",
    "chars": 1226,
    "preview": "{\n  \"name\": \"adonis-autoswagger\",\n  \"version\": \"3.73.0\",\n  \"description\": \"Auto-Generate swagger docs for AdonisJS\",\n  \""
  },
  {
    "path": "publish.sh",
    "chars": 58,
    "preview": "#!/bin/bash\npnpm version minor\ngit push && git push --tags"
  },
  {
    "path": "src/adonishelpers.ts",
    "chars": 2054,
    "preview": "export function serializeV6Middleware(mw: any): string[] {\n  return [...mw.all()].reduce<string[]>((result, one) => {\n  "
  },
  {
    "path": "src/autoswagger.ts",
    "chars": 25764,
    "preview": "import YAML from \"json-to-pretty-yaml\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport util from \"util\";\nimport HT"
  },
  {
    "path": "src/example.ts",
    "chars": 11205,
    "preview": "import { snakeCase } from \"lodash\";\nimport { getBetweenBrackets } from \"./helpers\";\nexport default class ExampleGenerato"
  },
  {
    "path": "src/helpers.ts",
    "chars": 1388,
    "preview": "/**\n * Check if a string is a valid JSON\n */\nimport { camelCase, isEmpty, isUndefined, snakeCase, startCase } from \"loda"
  },
  {
    "path": "src/index.ts",
    "chars": 79,
    "preview": "import { AutoSwagger } from \"./autoswagger\";\nexport default new AutoSwagger();\n"
  },
  {
    "path": "src/parsers.ts",
    "chars": 34177,
    "preview": "import HTTPStatusCode from \"http-status-code\";\nimport { isJSONString, getBetweenBrackets } from \"./helpers\";\nimport util"
  },
  {
    "path": "src/scalarCustomCss.ts",
    "chars": 3367,
    "preview": "export const scalarCustomCss = `\n/* basic theme */\n.light-mode {\n  --theme-background-1: #fff;\n  --theme-background-2: #"
  },
  {
    "path": "src/types.ts",
    "chars": 1320,
    "preview": "\n/**\n * Autoswagger interfaces\n */\nexport interface options {\n  title?: string;\n  ignore: string[];\n  version?: string;\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 264,
    "preview": "{\n  \"compilerOptions\": {\n    \"lib\": [\"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"Node16\",\n    \"declaration\": true,"
  }
]

About this extraction

This page contains the full source code of the ad-on-is/adonis-autoswagger GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (98.3 KB), approximately 25.0k tokens, and a symbol index with 77 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!