Skip to content

Commit 3923cb9

Browse files
drwpowericzorn93
andauthored
Feature add custom http headers (#764)
* feat: adding nvmrc file * feat: stronger typing with headers * feat: adding custom http types * test: snapshots fixing * test: many tests failing due to indentation * chore: remove log * test: update test script * fix: http headers append * fix: further header parsing if string * feat: allow headers and method to be sent in from CLI * chore: cleanup prettier * fix: cli help log for http headers * fix: formatting * chore: fix spacing * test: all unit tests passing * test: adding tests for isValidHTTPMethod * fix: isValidHTTPMethod and parsed header to use object.entries * chore: updating comments for index.ts * chore: cleaning up error messages * refactor: parseHTTPHeaders to ensure typeof object and not falsy * feat: allow multiple headers or json headers * feat: updating documentation for the CLI * chore: remove nvmrc * test: adding more utility type tests * test: adding intersection type generation * test: adding parse schema utility tests in load * test: adding isFile() tests * test: resolveSchema fully tested * test: adding more tests to parse headers * test: cleanup tests * test: cleanup tests for resolveSchema, due to windows CI process throwing error * fix: update generic for parseHttpHeader * fix: add default primitive val * chore: adding types * Remove some unit tests; add CLI test for new flags Co-authored-by: Eric Zorn <[email protected]>
1 parent 56cc872 commit 3923cb9

15 files changed

+807
-658
lines changed

README.md

+12-9
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,18 @@ npx openapi-typescript schema.yaml
9898

9999
#### CLI Options
100100

101-
| Option | Alias | Default | Description |
102-
| :----------------------------- | :---- | :------: | :------------------------------------------------------------------------------------------------------ |
103-
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
104-
| `--auth [token]` | | | (optional) Provide an auth token to be passed along in the request (only if accessing a private schema) |
105-
| `--immutable-types` | `-it` | `false` | (optional) Generates immutable types (readonly properties and readonly array) |
106-
| `--additional-properties` | `-ap` | `false` | (optional) Allow arbitrary properties for all schema objects without `additionalProperties: false` |
107-
| `--default-non-nullable` | | `false` | (optional) Treat schema objects with default values as non-nullable |
108-
| `--prettier-config [location]` | `-c` | | (optional) Path to your custom Prettier configuration for output |
109-
| `--raw-schema` | | `false` | Generate TS types from partial schema (e.g. having `components.schema` at the top level) |
101+
| Option | Alias | Default | Description |
102+
| :----------------------------- | :---- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------- |
103+
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
104+
| `--auth [token]` | | | (optional) Provide an auth token to be passed along in the request (only if accessing a private schema) |
105+
| `--immutable-types` | `-it` | `false` | (optional) Generates immutable types (readonly properties and readonly array) |
106+
| `--additional-properties` | `-ap` | `false` | (optional) Allow arbitrary properties for all schema objects without `additionalProperties: false` |
107+
| `--default-non-nullable` | | `false` | (optional) Treat schema objects with default values as non-nullable |
108+
| `--prettier-config [location]` | `-c` | | (optional) Path to your custom Prettier configuration for output |
109+
| `--raw-schema` | | `false` | Generate TS types from partial schema (e.g. having `components.schema` at the top level) |
110+
| `--httpMethod` | `-m` | `GET` | (optional) Provide the HTTP Verb/Method for fetching a schema from a remote URL |
111+
| `--headersObject` | `-h` | | (optional) Provide a JSON object as string of HTTP headers for remote schema request. This will take priority over `--header` |
112+
| `--header` | `-x` | | (optional) Provide an array of or singular headers as an alternative to a JSON object. Each header must follow the `key: value` pattern |
110113

111114
### 🐢 Node
112115

bin/cli.js

+41-6
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ const path = require("path");
77
const glob = require("tiny-glob");
88
const { default: openapiTS } = require("../dist/cjs/index.js");
99

10-
let cli = meow(
10+
const cli = meow(
1111
`Usage
1212
$ openapi-typescript [input] [options]
1313
1414
Options
1515
--help display this
1616
--output, -o Specify output file (default: stdout)
1717
--auth (optional) Provide an authentication token for private URL
18+
--headersObject, -h (optional) Provide a JSON object as string of HTTP headers for remote schema request
19+
--header, -x (optional) Provide an array of or singular headers as an alternative to a JSON object. Each header must follow the key: value pattern
20+
--httpMethod, -m (optional) Provide the HTTP Verb/Method for fetching a schema from a remote URL
1821
--immutable-types, -it (optional) Generates immutable types (readonly properties and readonly array)
1922
--additional-properties, -ap (optional) Allow arbitrary properties for all schema objects without "additionalProperties: false"
2023
--default-non-nullable (optional) If a schema object has a default value set, don’t mark it as nullable
@@ -31,6 +34,20 @@ Options
3134
auth: {
3235
type: "string",
3336
},
37+
headersObject: {
38+
type: "string",
39+
alias: "h",
40+
},
41+
header: {
42+
type: "string",
43+
alias: "x",
44+
isMultiple: true,
45+
},
46+
httpMethod: {
47+
type: "string",
48+
alias: "m",
49+
default: "GET",
50+
},
3451
immutableTypes: {
3552
type: "boolean",
3653
alias: "it",
@@ -69,6 +86,22 @@ function errorAndExit(errorMessage) {
6986
async function generateSchema(pathToSpec) {
7087
const output = cli.flags.output ? OUTPUT_FILE : OUTPUT_STDOUT; // FILE or STDOUT
7188

89+
// Parse incoming headers from CLI flags
90+
let httpHeaders = {};
91+
// prefer --headersObject if specified
92+
if (cli.flags.headersObject) {
93+
httpHeaders = JSON.parse(cli.flags.headersObject); // note: this will generate a recognizable error for the user to act on
94+
}
95+
// otherwise, parse --header
96+
else if (Array.isArray(cli.flags.header)) {
97+
cli.flags.header.forEach((header) => {
98+
const firstColon = header.indexOf(":");
99+
const k = header.substring(0, firstColon).trim();
100+
const v = header.substring(firstColon + 1).trim();
101+
httpHeaders[k] = v;
102+
});
103+
}
104+
72105
// generate schema
73106
const result = await openapiTS(pathToSpec, {
74107
additionalProperties: cli.flags.additionalProperties,
@@ -79,22 +112,24 @@ async function generateSchema(pathToSpec) {
79112
rawSchema: cli.flags.rawSchema,
80113
silent: output === OUTPUT_STDOUT,
81114
version: cli.flags.version,
115+
httpHeaders,
116+
httpMethod: cli.flags.httpMethod,
82117
});
83118

84119
// output
85120
if (output === OUTPUT_FILE) {
86-
let outputFile = path.resolve(process.cwd(), cli.flags.output); // note: may be directory
87-
const isDir = fs.existsSync(outputFile) && fs.lstatSync(outputFile).isDirectory();
121+
let outputFilePath = path.resolve(process.cwd(), cli.flags.output); // note: may be directory
122+
const isDir = fs.existsSync(outputFilePath) && fs.lstatSync(outputFilePath).isDirectory();
88123
if (isDir) {
89124
const filename = pathToSpec.replace(new RegExp(`${path.extname(pathToSpec)}$`), ".ts");
90-
outputFile = path.join(outputFile, filename);
125+
outputFilePath = path.join(outputFilePath, filename);
91126
}
92127

93-
await fs.promises.writeFile(outputFile, result, "utf8");
128+
await fs.promises.writeFile(outputFilePath, result, "utf8");
94129

95130
const timeEnd = process.hrtime(timeStart);
96131
const time = timeEnd[0] + Math.round(timeEnd[1] / 1e6);
97-
console.log(green(`🚀 ${pathToSpec} -> ${bold(outputFile)} [${time}ms]`));
132+
console.log(green(`🚀 ${pathToSpec} -> ${bold(outputFilePath)} [${time}ms]`));
98133
} else {
99134
process.stdout.write(result);
100135
// if stdout, (still) don’t log anything to console!

package-lock.json

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
"prepare": "npm run build",
5151
"pregenerate": "npm run build",
5252
"test": "npm run build && jest --no-cache",
53+
"test:watch": "npm run build && jest --no-cache --watch",
54+
"test:update": "npm run build && jest --no-cache --u",
5355
"test:coverage": "npm run build && jest --no-cache --coverage && codecov",
56+
"test:coverage:local": "npm run build && jest --no-cache --collectCoverage",
5457
"typecheck": "tsc --noEmit --project tsconfig.esm.json",
5558
"version": "npm run build"
5659
},
@@ -66,7 +69,7 @@
6669
"tiny-glob": "^0.2.9"
6770
},
6871
"devDependencies": {
69-
"@types/jest": "^27.0.0",
72+
"@types/jest": "^27.0.2",
7073
"@types/js-yaml": "^4.0.1",
7174
"@types/mime": "^2.0.3",
7275
"@types/node-fetch": "^2.5.12",

src/index.ts

+29-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,22 @@ export const WARNING_MESSAGE = `/**
1717
1818
`;
1919

20-
export default async function openapiTS(
20+
/**
21+
* This function is the entry to the program and allows the user to pass in a remote schema and/or local schema.
22+
* The URL or schema and headers can be passed in either programtically and/or via the CLI.
23+
* Remote schemas are fetched from a server that supplies JSON or YAML format via an HTTP GET request. File based schemas
24+
* are loaded in via file path, most commonly prefixed with the file:// format. Alternatively, the user can pass in
25+
* OpenAPI2 or OpenAPI3 schema objects that can be parsed directly by the function without reading the file system.
26+
*
27+
* Function overloading is utilized for generating stronger types for our different schema types and option types.
28+
*
29+
* @param {string} schema Root Swagger Schema HTTP URL, File URL, and/or JSON or YAML schema
30+
* @param {SwaggerToTSOptions<typeof schema>} [options] Options to specify to the parsing system
31+
* @return {Promise<string>} {Promise<string>} Parsed file schema
32+
*/
33+
async function openapiTS(
2134
schema: string | OpenAPI2 | OpenAPI3 | Record<string, SchemaObject>,
22-
options: SwaggerToTSOptions = {} as any
35+
options: SwaggerToTSOptions = {} as Partial<SwaggerToTSOptions>
2336
): Promise<string> {
2437
const ctx: GlobalContext = {
2538
additionalProperties: options.additionalProperties || false,
@@ -40,11 +53,15 @@ export default async function openapiTS(
4053
if (typeof schema === "string") {
4154
const schemaURL = resolveSchema(schema);
4255
if (options.silent === false) console.log(yellow(`🔭 Loading spec from ${bold(schemaURL.href)}…`));
56+
4357
await load(schemaURL, {
4458
...ctx,
4559
schemas: allSchemas,
4660
rootURL: schemaURL, // as it crawls schemas recursively, it needs to know which is the root to resolve everything relative to
61+
httpHeaders: options.httpHeaders,
62+
httpMethod: options.httpMethod,
4763
});
64+
4865
for (const k of Object.keys(allSchemas)) {
4966
if (k === schemaURL.href) {
5067
rootSchema = allSchemas[k];
@@ -53,7 +70,14 @@ export default async function openapiTS(
5370
}
5471
}
5572
} else {
56-
await load(schema, { ...ctx, schemas: allSchemas, rootURL: new URL(VIRTUAL_JSON_URL) });
73+
await load(schema, {
74+
...ctx,
75+
schemas: allSchemas,
76+
rootURL: new URL(VIRTUAL_JSON_URL),
77+
httpHeaders: options.httpHeaders,
78+
httpMethod: options.httpMethod,
79+
});
80+
5781
for (const k of Object.keys(allSchemas)) {
5882
if (k === VIRTUAL_JSON_URL) {
5983
rootSchema = allSchemas[k];
@@ -108,3 +132,5 @@ export default async function openapiTS(
108132
}
109133
return prettier.format(output, prettierOptions);
110134
}
135+
136+
export default openapiTS;

src/load.ts

+52-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { URL } from "url";
55
import slash from "slash";
66
import mime from "mime";
77
import yaml from "js-yaml";
8-
import { GlobalContext } from "./types";
8+
import { red } from "kleur";
9+
import { GlobalContext, Headers } from "./types";
910
import { parseRef } from "./utils";
1011

1112
type PartialSchema = Record<string, any>; // not a very accurate type, but this is easier to deal with before we know we’re dealing with a valid spec
@@ -43,17 +44,52 @@ export function resolveSchema(url: string): URL {
4344
const localPath = path.isAbsolute(url)
4445
? new URL("", `file://${slash(url)}`)
4546
: new URL(url, `file://${slash(process.cwd())}/`); // if absolute path is provided use that; otherwise search cwd\
47+
4648
if (!fs.existsSync(localPath)) {
4749
throw new Error(`Could not locate ${url}`);
4850
} else if (fs.statSync(localPath).isDirectory()) {
4951
throw new Error(`${localPath} is a directory not a file`);
5052
}
53+
5154
return localPath;
5255
}
5356

57+
/**
58+
* Accepts income HTTP headers and appends them to
59+
* the fetch request for the schema.
60+
*
61+
* @param {HTTPHeaderMap} httpHeaders
62+
* @return {Record<string, string>} {Record<string, string>} Final HTTP headers outcome.
63+
*/
64+
function parseHttpHeaders(httpHeaders: Record<string, any>): Headers {
65+
const finalHeaders: Record<string, string> = {};
66+
67+
// Obtain the header key
68+
for (const [k, v] of Object.entries(httpHeaders)) {
69+
// If the value of the header is already a string, we can move on, otherwise we have to parse it
70+
if (typeof v === "string") {
71+
finalHeaders[k] = v;
72+
} else {
73+
try {
74+
const stringVal = JSON.stringify(v);
75+
finalHeaders[k] = stringVal;
76+
} catch (err) {
77+
/* istanbul ignore next */
78+
console.error(
79+
red(`Cannot parse key: ${k} into JSON format. Continuing with the next HTTP header that is specified`)
80+
);
81+
}
82+
}
83+
}
84+
85+
return finalHeaders;
86+
}
87+
5488
interface LoadOptions extends GlobalContext {
5589
rootURL: URL;
5690
schemas: SchemaMap;
91+
httpHeaders?: Headers;
92+
httpMethod?: string;
5793
}
5894

5995
// temporary cache for load()
@@ -65,7 +101,7 @@ export default async function load(
65101
options: LoadOptions
66102
): Promise<{ [url: string]: PartialSchema }> {
67103
const isJSON = schema instanceof URL === false; // if this is dynamically-passed-in JSON, we’ll have to change a few things
68-
let schemaID = isJSON ? new URL(VIRTUAL_JSON_URL).href : schema.href;
104+
let schemaID = isJSON ? new URL(VIRTUAL_JSON_URL).href : (schema.href as string);
69105

70106
const schemas = options.schemas;
71107

@@ -88,13 +124,20 @@ export default async function load(
88124
contentType = mime.getType(schemaID) || "";
89125
} else {
90126
// load remote
91-
const res = await fetch(schemaID, {
92-
method: "GET",
93-
headers: {
94-
"User-Agent": "openapi-typescript",
95-
...(options.auth ? {} : { Authorization: options.auth }),
96-
},
97-
});
127+
const headers: Headers = {
128+
"User-Agent": "openapi-typescript",
129+
};
130+
if (options.auth) headers.Authorizaton = options.auth;
131+
132+
// Add custom parsed HTTP headers
133+
if (options.httpHeaders) {
134+
const parsedHeaders = parseHttpHeaders(options.httpHeaders);
135+
for (const [k, v] of Object.entries(parsedHeaders)) {
136+
headers[k] = v;
137+
}
138+
}
139+
140+
const res = await fetch(schemaID, { method: options.httpMethod || "GET", headers });
98141
contentType = res.headers.get("Content-Type") || "";
99142
contents = await res.text();
100143
}

src/types.ts

+17
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface OpenAPI3 {
2121
};
2222
}
2323

24+
export type Headers = Record<string, string>;
25+
2426
export interface HeaderObject {
2527
// note: this extends ParameterObject, minus "name" & "in"
2628
type?: string; // required
@@ -134,6 +136,21 @@ export interface SwaggerToTSOptions {
134136
silent?: boolean;
135137
/** (optional) OpenAPI version. Must be present if parsing raw schema */
136138
version?: number;
139+
/**
140+
* (optional) List of HTTP headers that will be sent with the fetch request to a remote schema. This is
141+
* in addition to the authorization header. In some cases, servers require headers such as Accept: application/json
142+
* or Accept: text/yaml to be sent in order to figure out how to properly fetch the OpenAPI/Swagger document as code.
143+
* These headers will only be sent in the case that the schema URL protocol is of type http or https.
144+
*/
145+
httpHeaders?: Headers;
146+
/**
147+
* HTTP verb used to fetch the schema from a remote server. This is only applied
148+
* when the schema is a string and has the http or https protocol present. By default,
149+
* the request will use the HTTP GET method to fetch the schema from the server.
150+
*
151+
* @default {string} GET
152+
*/
153+
httpMethod?: string;
137154
}
138155

139156
/** Context passed to all submodules */

0 commit comments

Comments
 (0)