Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V7 Preview #1363

Merged
merged 10 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
bin
coverage
dist
examples
39 changes: 37 additions & 2 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,49 @@ module.exports = {
parserOptions: {
project: ["./tsconfig.json"],
},
extends: ["eslint:recommended", "plugin:@typescript-eslint/strict", "plugin:vitest/recommended"],
plugins: ["@typescript-eslint", "no-only-tests", "prettier", "vitest"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/strict",
"plugin:vitest/recommended",
],
plugins: [
"@typescript-eslint",
"import",
"no-only-tests",
"prettier",
"vitest",
],
rules: {
"@typescript-eslint/consistent-indexed-object-style": "off", // sometimes naming keys is more user-friendly
"@typescript-eslint/no-dynamic-delete": "off", // delete is OK
"@typescript-eslint/no-non-null-assertion": "off", // this is better than "as"
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/no-unnecessary-condition": "off", // this gives bad advice
"arrow-body-style": ["error", "as-needed"],
"dot-notation": "error",
"import/newline-after-import": "error",
"import/order": [
"error",
{
alphabetize: {
order: "asc",
orderImportKind: "asc",
caseInsensitive: true,
},
groups: [
["builtin", "external"],
"internal",
"parent",
"index",
"sibling",
],
},
],
curly: "error",
"object-shorthand": "error", // don’t use foo["bar"]
"no-console": "error",
"no-global-assign": "error",
"no-undef": "off", // handled by TS
"no-unused-vars": "off",
},
overrides: [
Expand Down
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"printWidth": 240
"singleAttributePerLine": true
}
26 changes: 13 additions & 13 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@
"update-contributors": "node scripts/update-contributors.js"
},
"dependencies": {
"@algolia/client-search": "^4.19.1",
"@algolia/client-search": "^4.20.0",
"@astrojs/preact": "^2.2.2",
"@astrojs/react": "^2.2.2",
"@docsearch/css": "^3.5.1",
"@docsearch/react": "^3.5.1",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"astro": "^2.10.9",
"preact": "^10.17.0",
"@astrojs/react": "^3.0.2",
"@docsearch/css": "^3.5.2",
"@docsearch/react": "^3.5.2",
"@types/react": "^18.2.24",
"@types/react-dom": "^18.2.8",
"astro": "^3.2.2",
"preact": "^10.18.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.65.1"
"sass": "^1.68.0"
},
"devDependencies": {
"@astrojs/sitemap": "^2.0.2",
"@cobalt-ui/cli": "^1.4.1",
"@cobalt-ui/plugin-sass": "^1.2.3",
"@types/node": "^20.5.0",
"@cobalt-ui/cli": "^1.6.0",
"@cobalt-ui/plugin-sass": "^1.3.0",
"@types/node": "^20.8.2",
"html-escaper": "^3.0.3",
"typescript": "^5.2.2",
"vite-plugin-sass-dts": "^1.3.9"
"vite-plugin-sass-dts": "^1.3.11"
}
}
1 change: 0 additions & 1 deletion docs/src/content/docs/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ description: Additional info about this project

1. Support converting any valid OpenAPI schema to TypeScript types, no matter how complicated.
1. Generated types should be statically-analyzable and runtime-free (with minor exceptions like <a href="https://www.typescriptlang.org/docs/handbook/enums.html" target="_blank" rel="noopener noreferrer">enums</a>).
1. Don’t validate schemas; there are existing libraries for that like <a href="https://redocly.com/docs/cli/commands/lint/" target="_blank" rel="noopener noreferrer">Redocly</a>.
1. Generated types should match your original schema as closely as possible, preserving original capitalization, etc.
1. Typegen only needs Node.js to run (no Java, Python, etc.) and works in any environment.
1. Support fetching OpenAPI schemas from files as well as local and remote servers.
Expand Down
47 changes: 38 additions & 9 deletions docs/src/content/docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ const BASE_URL = "https://myapi.com/v1";
// End Settings

// type helpers — ignore these; these just make TS lookups better
type FilterKeys<Obj, Matchers> = { [K in keyof Obj]: K extends Matchers ? Obj[K] : never }[keyof Obj];
type FilterKeys<Obj, Matchers> = {
[K in keyof Obj]: K extends Matchers ? Obj[K] : never;
}[keyof Obj];
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
type MediaType = `${string}/${string}`;
Expand All @@ -107,26 +109,39 @@ type MockedResponse<T, Status extends keyof T = keyof T> = FilterKeys<
*/
export function mockResponses(responses: {
[Path in keyof Partial<paths>]: {
[Method in keyof Partial<paths[Path]>]: MockedResponse<PathResponses<paths[Path][Method]>>;
[Method in keyof Partial<paths[Path]>]: MockedResponse<
PathResponses<paths[Path][Method]>
>;
};
}) {
fetchMock.mockResponse((req) => {
const mockedPath = findPath(req.url.replace(BASE_URL, ""), Object.keys(responses))!;
const mockedPath = findPath(
req.url.replace(BASE_URL, ""),
Object.keys(responses),
)!;
// note: we get lazy with the types here, because the inference is bad anyway and this has a `void` return signature. The important bit is the parameter signature.
if (!mockedPath || (!responses as any)[mockedPath]) throw new Error(`No mocked response for ${req.url}`); // throw error if response not mocked (remove or modify if you’d like different behavior)
if (!mockedPath || (!responses as any)[mockedPath])
throw new Error(`No mocked response for ${req.url}`); // throw error if response not mocked (remove or modify if you’d like different behavior)
const method = req.method.toLowerCase();
if (!(responses as any)[mockedPath][method]) throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // likewise throw error if other parts of response aren’t mocked
if (!(responses as any)[mockedPath][method])
throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // likewise throw error if other parts of response aren’t mocked
if (!(responses as any)[mockedPath][method]) {
throw new Error(`${req.method} called but not mocked on ${mockedPath}`);
}
const { status, body } = (responses as any)[mockedPath][method];
return { status, body: JSON.stringify(body) };
})
});
}

// helper function that matches a realistic URL (/users/123) to an OpenAPI path (/users/{user_id}
export function findPath(actual: string, testPaths: string[]): string | undefined {
const url = new URL(actual, actual.startsWith("http") ? undefined : "http://testapi.com");
export function findPath(
actual: string,
testPaths: string[],
): string | undefined {
const url = new URL(
actual,
actual.startsWith("http") ? undefined : "http://testapi.com",
);
const actualParts = url.pathname.split("/");
for (const p of testPaths) {
let matched = true;
Expand All @@ -149,7 +164,9 @@ export function findPath(actual: string, testPaths: string[]): string | undefine
```ts
export function mockResponses(responses: {
[Path in keyof Partial<paths>]: {
[Method in keyof Partial<paths[Path]>]: MockedResponse<PathResponses<paths[Path][Method]>>;
[Method in keyof Partial<paths[Path]>]: MockedResponse<
PathResponses<paths[Path][Method]>
>;
};
});
```
Expand All @@ -158,6 +175,18 @@ export function mockResponses(responses: {

Now, whenever your schema updates, **all your mock data will be typechecked correctly** 🎉. This is a huge step in ensuring resilient, accurate tests.

## Debugging

To enable debugging, set `DEBUG=openapi-ts:*` as an env var like so:

```sh
$ DEBUG=openapi-ts:* npx openapi-typescript schema.yaml -o my-types.ts
```

To only see certain types of debug messages, you can set `DEBUG=openapi-ts:[scope]` instead. Valid scopes are `redoc`, `lint`, `bundle`, and `ts`.

Note that debug messages will be suppressed if the output is `stdout`.

## Tips

In no particular order, here are a few best practices to make life easier when working with OpenAPI-derived types.
Expand Down
109 changes: 73 additions & 36 deletions docs/src/content/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,97 @@ title: CLI
description: openapi-typescript CLI usage
---

The CLI is the most common way to use openapi-typescript. The CLI can parse JSON and YAML (via <a href="https://www.npmjs.com/package/js-yaml" target="_blank" rel="noopener noreferrer">js-yaml</a>). It can parse local and remote schemas (and even supports basic auth).
The CLI is the most common way to use openapi-typescript. The CLI can parse JSON and YAML, and even validates your schemas using the [Redocly CLI](https://redocly.com/docs/cli/commands/lint/). It can parse local and remote schemas (and even supports basic auth).

## Reading schemas
## Transforming an OpenAPI schema to TypeScript

### Single schema

The simplest way to transform schemas is by specifying an input schema (JSON or YAML), followed by `--output` (`-o`) where you’d like the output to be saved:

```bash
npx openapi-typescript schema.yaml -o schema.ts

# 🚀 schema.yaml -> schema.ts [7ms]
# 🚀 schema.yaml -> schema.ts [50ms]
```

### Globbing local schemas

```bash
npx openapi-typescript "specs/**/*.yaml" -o schemas/
npx openapi-typescript https://petstore3.swagger.io/api/v3/openapi.yaml -o petstore.d.ts

# 🚀 specs/one.yaml -> schemas/specs/one.ts [7ms]
# 🚀 specs/two.yaml -> schemas/specs/two.ts [7ms]
# 🚀 specs/three.yaml -> schemas/specs/three.ts [7ms]
# 🚀 https://petstore3.swagger.io/api/v3/openapi.yaml -> petstore.d.ts [250ms]
```

_Thanks, [@sharmarajdaksh](https://github.com/sharmarajdaksh)!_
### Multiple schemas

### Remote schemas
To transform multiple schemas, create a `redocly.yaml` file in the root of your project with [APIs defined](https://redocly.com/docs/cli/configuration/). Under `apis`, give each schema a unique name and optionally a version (the name doesn’t matter, so long as it’s unique). Set the `root` value to your schema’s entry point—this will act as the main input. For the output, set it with `openapi-ts.output`:

```yaml
apis:
core@v2:
root: ./openapi/openapi.yaml
openapi-ts:
output: ./openapi/openapi.ts
external@v1:
root: ./openapi/external.yaml
openapi-ts:
output: ./openapi/openapi.ts
```

Whenver you have a `redocly.yaml` file in your project with `apis`, you can omit the input/output parameters in the CLI:

```bash
npx openapi-typescript https://petstore3.swagger.io/api/v3/openapi.yaml -o petstore.d.ts
npx openapi-typescript
```

# 🚀 https://petstore3.swagger.io/api/v3/openapi.yaml -> petstore.d.ts [250ms]
> ⚠️ In previous versions globbing was supported, but that has been **deprecated** in v7 in favor of `redocly.yaml`. You’ll be able to control per-schema output locations better, as well as getting unique per-schema settings.

## Redoc config

A `redocly.yaml` file isn’t required to use openapi-typescript. By default it extends the `"minimal"` built-in config. But it is recommended if you want to have custom validation rules (or build types for [multiple schemas](#multiple-schemas)). The CLI will try to automatically find a `redocly.yaml` in the root of your project, but you can also provide its location with the `--redoc` flag:

```bash
npx openapi-typescript --redoc ./path/to/redocly.yaml
```

You can read more about the Redoc’s configuration options [in their docs](https://redocly.com/docs/cli/configuration/).

## Auth

Authentication for non-public schemas is handled in your [Redocly config](https://redocly.com/docs/cli/configuration/#resolve-non-public-or-non-remote-urls). You can add headers and basic authentication like so:

```yaml
resolve:
http:
headers:
- matches: https://api.example.com/v2/**
name: X-API-KEY
envVariable: SECRET_KEY
- matches: https://example.com/*/test.yaml
name: Authorization
envVariable: SECRET_AUTH
```

_Thanks, [@psmyrdek](https://github.com/psmyrdek)!_
Refer to the [Redocly docs](https://redocly.com/docs/cli/configuration/#resolve-non-public-or-non-remote-urls) for additional options.

## Options

| Option | Alias | Default | Description |
| :------------------------ | :---- | :------: | :--------------------------------------------------------------------------------------------------------------------------- |
| `--help` | | | Display inline help message and exit |
| `--version` | | | Display this library’s version and exit |
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
| `--auth [token]` | | | Provide an auth token to be passed along in the request (only if accessing a private schema) |
| `--header` | `-x` | | Provide an array of or singular headers as an alternative to a JSON object. Each header must follow the `key: value` pattern |
| `--headers-object="{…}"` | `-h` | | Provide a JSON object as string of HTTP headers for remote schema request. This will take priority over `--header` |
| `--http-method` | `-m` | `GET` | Provide the HTTP Verb/Method for fetching a schema from a remote URL |
| `--immutable-types` | | `false` | Generates immutable types (readonly properties and readonly array) |
| `--additional-properties` | | `false` | Allow arbitrary properties for all schema objects without `additionalProperties: false` |
| `--empty-objects-unknown` | | `false` | Allow arbitrary properties for schema objects with no specified properties, and no specified `additionalProperties` |
| `--default-non-nullable` | | `false` | Treat schema objects with default values as non-nullable |
| `--export-type` | `-t` | `false` | Export `type` instead of `interface` |
| `--path-params-as-types` | | `false` | Allow dynamic string lookups on the `paths` object |
| `--support-array-length` | | `false` | Generate tuples using array `minItems` / `maxItems` |
| `--alphabetize` | | `false` | Sort types alphabetically |
| `--exclude-deprecated` | | `false` | Exclude deprecated fields from types |

### `--path-params-as-types`
| Option | Alias | Default | Description |
| :------------------------ | :---- | :------: | :------------------------------------------------------------------------------------------------------------------ |
| `--help` | | | Display inline help message and exit |
| `--version` | | | Display this library’s version and exit |
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
| `--redoc [location]` | | | Path to a `redocly.yaml` file (see [Multiple schemas](#multiple-schemas)) |
| `--immutable` | | `false` | Generates immutable types (readonly properties and readonly array) |
| `--additional-properties` | | `false` | Allow arbitrary properties for all schema objects without `additionalProperties: false` |
| `--empty-objects-unknown` | | `false` | Allow arbitrary properties for schema objects with no specified properties, and no specified `additionalProperties` |
| `--default-non-nullable` | | `false` | Treat schema objects with default values as non-nullable |
| `--export-type` | `-t` | `false` | Export `type` instead of `interface` |
| `--path-params-as-types` | | `false` | Allow dynamic string lookups on the `paths` object |
| `--array-length` | | `false` | Generate tuples using array `minItems` / `maxItems` |
| `--alphabetize` | | `false` | Sort types alphabetically |
| `--exclude-deprecated` | | `false` | Exclude deprecated fields from types |

### pathParamsAsTypes flag

By default, your URLs are preserved exactly as-written in your schema:

Expand Down Expand Up @@ -88,7 +125,7 @@ Though this is a contrived example, you could use this feature to automatically

_Thanks, [@Powell-v2](https://github.com/Powell-v2)!_

### `--support-array-length`
### arrayLength flag

This option is useful for generating tuples if an array type specifies `minItems` or `maxItems`.

Expand All @@ -105,7 +142,7 @@ components:
maxItems: 2
```

Enabling `--support-array-length` would change the typing like so:
Enabling `--array-length` would change the typing like so:

```diff
export interface components {
Expand Down
12 changes: 6 additions & 6 deletions docs/src/content/docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ npm i -D openapi-typescript

## Basic usage

First, generate a local type file by running `npx openapi-typescript`:
First, generate a local type file by running `npx openapi-typescript`, first specifying your input schema (JSON or YAML), and where you’d like the `--output` (`-o`) to be saved:

```bash
# Local schema
Expand All @@ -46,9 +46,7 @@ npx openapi-typescript https://myapi.dev/api/v1/openapi.yaml -o ./path/to/my/sch
# 🚀 https://myapi.dev/api/v1/openapi.yaml -> ./path/to/my/schema.d.ts [250ms]
```

> ⚠️ Be sure to <a href="https://redocly.com/docs/cli/commands/lint/" target="_blank" rel="noopener noreferrer">validate your schemas</a>! openapi-typescript will err on invalid schemas.

Then, import schemas from the generated file like so:
Then in your TypeScript project, import types where needed:

```ts
import { paths, components } from "./path/to/my/schema"; // <- generated by openapi-typescript
Expand All @@ -60,8 +58,10 @@ type MyType = components["schemas"]["MyType"];
type EndpointParams = paths["/my/endpoint"]["parameters"];

// Response obj
type SuccessResponse = paths["/my/endpoint"]["get"]["responses"][200]["content"]["application/json"]["schema"];
type ErrorResponse = paths["/my/endpoint"]["get"]["responses"][500]["content"]["application/json"]["schema"];
type SuccessResponse =
paths["/my/endpoint"]["get"]["responses"][200]["content"]["application/json"]["schema"];
type ErrorResponse =
paths["/my/endpoint"]["get"]["responses"][500]["content"]["application/json"]["schema"];
```

From here, you can use these types for any of the following (but not limited to):
Expand Down
Loading
Loading