Skip to content

Feature add custom http headers #764

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

Merged
merged 35 commits into from
Sep 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
84706cc
feat: adding nvmrc file
ericzorn93 Aug 6, 2021
bf9f06c
feat: stronger typing with headers
ericzorn93 Aug 6, 2021
e0cbe7f
feat: adding custom http types
ericzorn93 Aug 6, 2021
2acdb2e
test: snapshots fixing
ericzorn93 Aug 6, 2021
c866a0a
test: many tests failing due to indentation
ericzorn93 Aug 6, 2021
a9c0ff6
chore: remove log
ericzorn93 Aug 6, 2021
1e1bfc7
test: update test script
ericzorn93 Aug 6, 2021
7c55976
fix: http headers append
ericzorn93 Aug 6, 2021
22402b1
fix: further header parsing if string
ericzorn93 Aug 6, 2021
51ef119
feat: allow headers and method to be sent in from CLI
ericzorn93 Aug 6, 2021
7a4557c
chore: cleanup prettier
ericzorn93 Aug 6, 2021
b5e2490
fix: cli help log for http headers
ericzorn93 Aug 6, 2021
beae345
fix: formatting
ericzorn93 Aug 6, 2021
a759e41
chore: fix spacing
ericzorn93 Aug 8, 2021
eadf75d
test: all unit tests passing
ericzorn93 Aug 8, 2021
53df7cb
test: adding tests for isValidHTTPMethod
ericzorn93 Aug 8, 2021
0945189
fix: isValidHTTPMethod and parsed header to use object.entries
ericzorn93 Aug 29, 2021
5cbdb05
chore: updating comments for index.ts
ericzorn93 Aug 29, 2021
94b2859
chore: cleaning up error messages
ericzorn93 Aug 29, 2021
84e0ab3
refactor: parseHTTPHeaders to ensure typeof object and not falsy
ericzorn93 Aug 29, 2021
9c7d489
feat: allow multiple headers or json headers
ericzorn93 Aug 29, 2021
4d1d74b
feat: updating documentation for the CLI
ericzorn93 Aug 29, 2021
9a2e530
chore: remove nvmrc
ericzorn93 Aug 29, 2021
a31eef4
test: adding more utility type tests
ericzorn93 Aug 29, 2021
24db309
test: adding intersection type generation
ericzorn93 Aug 29, 2021
bd27e94
test: adding parse schema utility tests in load
ericzorn93 Aug 30, 2021
b28b060
test: adding isFile() tests
ericzorn93 Aug 30, 2021
6d5e08f
test: resolveSchema fully tested
ericzorn93 Aug 30, 2021
12454dc
test: adding more tests to parse headers
ericzorn93 Aug 30, 2021
9d79d08
test: cleanup tests
ericzorn93 Aug 30, 2021
f8f419b
test: cleanup tests for resolveSchema, due to windows CI process thro…
ericzorn93 Aug 30, 2021
f9735d8
fix: update generic for parseHttpHeader
ericzorn93 Aug 30, 2021
e974bfd
fix: add default primitive val
ericzorn93 Aug 30, 2021
1b13ccf
chore: adding types
ericzorn93 Sep 7, 2021
4d9d18c
Remove some unit tests; add CLI test for new flags
drwpow Sep 30, 2021
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
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,18 @@ npx openapi-typescript schema.yaml

#### CLI Options

| Option | Alias | Default | Description |
| :----------------------------- | :---- | :------: | :------------------------------------------------------------------------------------------------------ |
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
| `--auth [token]` | | | (optional) Provide an auth token to be passed along in the request (only if accessing a private schema) |
| `--immutable-types` | `-it` | `false` | (optional) Generates immutable types (readonly properties and readonly array) |
| `--additional-properties` | `-ap` | `false` | (optional) Allow arbitrary properties for all schema objects without `additionalProperties: false` |
| `--default-non-nullable` | | `false` | (optional) Treat schema objects with default values as non-nullable |
| `--prettier-config [location]` | `-c` | | (optional) Path to your custom Prettier configuration for output |
| `--raw-schema` | | `false` | Generate TS types from partial schema (e.g. having `components.schema` at the top level) |
| Option | Alias | Default | Description |
| :----------------------------- | :---- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------- |
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
| `--auth [token]` | | | (optional) Provide an auth token to be passed along in the request (only if accessing a private schema) |
| `--immutable-types` | `-it` | `false` | (optional) Generates immutable types (readonly properties and readonly array) |
| `--additional-properties` | `-ap` | `false` | (optional) Allow arbitrary properties for all schema objects without `additionalProperties: false` |
| `--default-non-nullable` | | `false` | (optional) Treat schema objects with default values as non-nullable |
| `--prettier-config [location]` | `-c` | | (optional) Path to your custom Prettier configuration for output |
| `--raw-schema` | | `false` | Generate TS types from partial schema (e.g. having `components.schema` at the top level) |
| `--httpMethod` | `-m` | `GET` | (optional) Provide the HTTP Verb/Method for fetching a schema from a remote URL |
| `--headersObject` | `-h` | | (optional) Provide a JSON object as string of HTTP headers for remote schema request. This will take priority over `--header` |
| `--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 |

### 🐢 Node

Expand Down
47 changes: 41 additions & 6 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ const path = require("path");
const glob = require("tiny-glob");
const { default: openapiTS } = require("../dist/cjs/index.js");

let cli = meow(
const cli = meow(
`Usage
$ openapi-typescript [input] [options]

Options
--help display this
--output, -o Specify output file (default: stdout)
--auth (optional) Provide an authentication token for private URL
--headersObject, -h (optional) Provide a JSON object as string of HTTP headers for remote schema request
--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
--httpMethod, -m (optional) Provide the HTTP Verb/Method for fetching a schema from a remote URL
--immutable-types, -it (optional) Generates immutable types (readonly properties and readonly array)
--additional-properties, -ap (optional) Allow arbitrary properties for all schema objects without "additionalProperties: false"
--default-non-nullable (optional) If a schema object has a default value set, don’t mark it as nullable
Expand All @@ -31,6 +34,20 @@ Options
auth: {
type: "string",
},
headersObject: {
type: "string",
alias: "h",
},
header: {
type: "string",
alias: "x",
isMultiple: true,
},
httpMethod: {
type: "string",
alias: "m",
default: "GET",
},
immutableTypes: {
type: "boolean",
alias: "it",
Expand Down Expand Up @@ -69,6 +86,22 @@ function errorAndExit(errorMessage) {
async function generateSchema(pathToSpec) {
const output = cli.flags.output ? OUTPUT_FILE : OUTPUT_STDOUT; // FILE or STDOUT

// Parse incoming headers from CLI flags
let httpHeaders = {};
// prefer --headersObject if specified
if (cli.flags.headersObject) {
httpHeaders = JSON.parse(cli.flags.headersObject); // note: this will generate a recognizable error for the user to act on
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor change here: we had a getCLISchemaHeadersJSON() function that did a lot of setup around JSON.parse(), and in the end obscured the error from the user. Instead, I’d like to expose the error to the user so it’s more clear what’s not parsing correctly.

}
// otherwise, parse --header
else if (Array.isArray(cli.flags.header)) {
cli.flags.header.forEach((header) => {
const firstColon = header.indexOf(":");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was using a RegEx instead (/(\w+)(\:)\s((\w+)[\-|\:]?.+)/gi;), but in testing I found it to be brittle and a little awkward. Instead I opted for more of a curl-like approach where the first : designates the key–value relationship. It’s then up to the user to format their header correctly.

const k = header.substring(0, firstColon).trim();
const v = header.substring(firstColon + 1).trim();
httpHeaders[k] = v;
});
}

// generate schema
const result = await openapiTS(pathToSpec, {
additionalProperties: cli.flags.additionalProperties,
Expand All @@ -79,22 +112,24 @@ async function generateSchema(pathToSpec) {
rawSchema: cli.flags.rawSchema,
silent: output === OUTPUT_STDOUT,
version: cli.flags.version,
httpHeaders,
httpMethod: cli.flags.httpMethod,
});

// output
if (output === OUTPUT_FILE) {
let outputFile = path.resolve(process.cwd(), cli.flags.output); // note: may be directory
const isDir = fs.existsSync(outputFile) && fs.lstatSync(outputFile).isDirectory();
let outputFilePath = path.resolve(process.cwd(), cli.flags.output); // note: may be directory
const isDir = fs.existsSync(outputFilePath) && fs.lstatSync(outputFilePath).isDirectory();
if (isDir) {
const filename = pathToSpec.replace(new RegExp(`${path.extname(pathToSpec)}$`), ".ts");
outputFile = path.join(outputFile, filename);
outputFilePath = path.join(outputFilePath, filename);
}

await fs.promises.writeFile(outputFile, result, "utf8");
await fs.promises.writeFile(outputFilePath, result, "utf8");

const timeEnd = process.hrtime(timeStart);
const time = timeEnd[0] + Math.round(timeEnd[1] / 1e6);
console.log(green(`🚀 ${pathToSpec} -> ${bold(outputFile)} [${time}ms]`));
console.log(green(`🚀 ${pathToSpec} -> ${bold(outputFilePath)} [${time}ms]`));
} else {
process.stdout.write(result);
// if stdout, (still) don’t log anything to console!
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
"prepare": "npm run build",
"pregenerate": "npm run build",
"test": "npm run build && jest --no-cache",
"test:watch": "npm run build && jest --no-cache --watch",
"test:update": "npm run build && jest --no-cache --u",
"test:coverage": "npm run build && jest --no-cache --coverage && codecov",
"test:coverage:local": "npm run build && jest --no-cache --collectCoverage",
"typecheck": "tsc --noEmit --project tsconfig.esm.json",
"version": "npm run build"
},
Expand All @@ -66,7 +69,7 @@
"tiny-glob": "^0.2.9"
},
"devDependencies": {
"@types/jest": "^27.0.0",
"@types/jest": "^27.0.2",
"@types/js-yaml": "^4.0.1",
"@types/mime": "^2.0.3",
"@types/node-fetch": "^2.5.12",
Expand Down
32 changes: 29 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,22 @@ export const WARNING_MESSAGE = `/**

`;

export default async function openapiTS(
/**
* This function is the entry to the program and allows the user to pass in a remote schema and/or local schema.
* The URL or schema and headers can be passed in either programtically and/or via the CLI.
* Remote schemas are fetched from a server that supplies JSON or YAML format via an HTTP GET request. File based schemas
* are loaded in via file path, most commonly prefixed with the file:// format. Alternatively, the user can pass in
* OpenAPI2 or OpenAPI3 schema objects that can be parsed directly by the function without reading the file system.
*
* Function overloading is utilized for generating stronger types for our different schema types and option types.
*
* @param {string} schema Root Swagger Schema HTTP URL, File URL, and/or JSON or YAML schema
* @param {SwaggerToTSOptions<typeof schema>} [options] Options to specify to the parsing system
* @return {Promise<string>} {Promise<string>} Parsed file schema
*/
async function openapiTS(
schema: string | OpenAPI2 | OpenAPI3 | Record<string, SchemaObject>,
options: SwaggerToTSOptions = {} as any
options: SwaggerToTSOptions = {} as Partial<SwaggerToTSOptions>
): Promise<string> {
const ctx: GlobalContext = {
additionalProperties: options.additionalProperties || false,
Expand All @@ -40,11 +53,15 @@ export default async function openapiTS(
if (typeof schema === "string") {
const schemaURL = resolveSchema(schema);
if (options.silent === false) console.log(yellow(`🔭 Loading spec from ${bold(schemaURL.href)}…`));

await load(schemaURL, {
...ctx,
schemas: allSchemas,
rootURL: schemaURL, // as it crawls schemas recursively, it needs to know which is the root to resolve everything relative to
httpHeaders: options.httpHeaders,
httpMethod: options.httpMethod,
});

for (const k of Object.keys(allSchemas)) {
if (k === schemaURL.href) {
rootSchema = allSchemas[k];
Expand All @@ -53,7 +70,14 @@ export default async function openapiTS(
}
}
} else {
await load(schema, { ...ctx, schemas: allSchemas, rootURL: new URL(VIRTUAL_JSON_URL) });
await load(schema, {
...ctx,
schemas: allSchemas,
rootURL: new URL(VIRTUAL_JSON_URL),
httpHeaders: options.httpHeaders,
httpMethod: options.httpMethod,
});

for (const k of Object.keys(allSchemas)) {
if (k === VIRTUAL_JSON_URL) {
rootSchema = allSchemas[k];
Expand Down Expand Up @@ -108,3 +132,5 @@ export default async function openapiTS(
}
return prettier.format(output, prettierOptions);
}

export default openapiTS;
61 changes: 52 additions & 9 deletions src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { URL } from "url";
import slash from "slash";
import mime from "mime";
import yaml from "js-yaml";
import { GlobalContext } from "./types";
import { red } from "kleur";
import { GlobalContext, Headers } from "./types";
import { parseRef } from "./utils";

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
Expand Down Expand Up @@ -43,17 +44,52 @@ export function resolveSchema(url: string): URL {
const localPath = path.isAbsolute(url)
? new URL("", `file://${slash(url)}`)
: new URL(url, `file://${slash(process.cwd())}/`); // if absolute path is provided use that; otherwise search cwd\

if (!fs.existsSync(localPath)) {
throw new Error(`Could not locate ${url}`);
} else if (fs.statSync(localPath).isDirectory()) {
throw new Error(`${localPath} is a directory not a file`);
}

return localPath;
}

/**
* Accepts income HTTP headers and appends them to
* the fetch request for the schema.
*
* @param {HTTPHeaderMap} httpHeaders
* @return {Record<string, string>} {Record<string, string>} Final HTTP headers outcome.
*/
function parseHttpHeaders(httpHeaders: Record<string, any>): Headers {
const finalHeaders: Record<string, string> = {};

// Obtain the header key
for (const [k, v] of Object.entries(httpHeaders)) {
// If the value of the header is already a string, we can move on, otherwise we have to parse it
if (typeof v === "string") {
finalHeaders[k] = v;
} else {
try {
const stringVal = JSON.stringify(v);
finalHeaders[k] = stringVal;
} catch (err) {
/* istanbul ignore next */
console.error(
red(`Cannot parse key: ${k} into JSON format. Continuing with the next HTTP header that is specified`)
);
}
}
}

return finalHeaders;
}

interface LoadOptions extends GlobalContext {
rootURL: URL;
schemas: SchemaMap;
httpHeaders?: Headers;
httpMethod?: string;
}

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

const schemas = options.schemas;

Expand All @@ -88,13 +124,20 @@ export default async function load(
contentType = mime.getType(schemaID) || "";
} else {
// load remote
const res = await fetch(schemaID, {
method: "GET",
headers: {
"User-Agent": "openapi-typescript",
...(options.auth ? {} : { Authorization: options.auth }),
},
});
const headers: Headers = {
"User-Agent": "openapi-typescript",
};
if (options.auth) headers.Authorizaton = options.auth;

// Add custom parsed HTTP headers
if (options.httpHeaders) {
const parsedHeaders = parseHttpHeaders(options.httpHeaders);
for (const [k, v] of Object.entries(parsedHeaders)) {
headers[k] = v;
}
}

const res = await fetch(schemaID, { method: options.httpMethod || "GET", headers });
contentType = res.headers.get("Content-Type") || "";
contents = await res.text();
}
Expand Down
17 changes: 17 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface OpenAPI3 {
};
}

export type Headers = Record<string, string>;

export interface HeaderObject {
// note: this extends ParameterObject, minus "name" & "in"
type?: string; // required
Expand Down Expand Up @@ -134,6 +136,21 @@ export interface SwaggerToTSOptions {
silent?: boolean;
/** (optional) OpenAPI version. Must be present if parsing raw schema */
version?: number;
/**
* (optional) List of HTTP headers that will be sent with the fetch request to a remote schema. This is
* in addition to the authorization header. In some cases, servers require headers such as Accept: application/json
* or Accept: text/yaml to be sent in order to figure out how to properly fetch the OpenAPI/Swagger document as code.
* These headers will only be sent in the case that the schema URL protocol is of type http or https.
*/
httpHeaders?: Headers;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an opinionated change, but in the previous PR we allowed httpHeaders?: to be a polymorphic type, from null to a JS object to a Map. We then had to implement quite a bit of runtime logic to check to make sure everything lined up correctly.

Instead, I opted to go with just a plain JS object for headers because that’s what gets passed to fetch() anyway. Servers are lax in what they accept. And while I completely get the author’s original desire to strongly type their request headers, I believe that’s outside the scope of this library, and could only introduce possible bugs/unnecessary maintenance.

/**
* HTTP verb used to fetch the schema from a remote server. This is only applied
* when the schema is a string and has the http or https protocol present. By default,
* the request will use the HTTP GET method to fetch the schema from the server.
*
* @default {string} GET
*/
httpMethod?: string;
Copy link
Contributor Author

@drwpow drwpow Sep 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar approach: rather than maintain a discriminated union of all valid HTTP methods, I opted instead to allow a simple string like most every other library allows. Perhaps your server allows more!

Again, I know this was not the author’s original intent, and I respect their level of quality! But I feel this was one of many details which just added on a lot of maintenance and potential typing bugs without much benefit to the average user.

}

/** Context passed to all submodules */
Expand Down
Loading