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

feat: add --x-nullable-as-nullable to treat schemas with x-nullable as being able to specify null #1576

Merged
merged 1 commit into from
Mar 4, 2024
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
3 changes: 3 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Options
--content-never (optional) If supplied, an omitted reponse \`content\` property will be generated as \`never\` instead of \`unknown\`
--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
--x-nullable-as-nullable (optional) If a schema object has \`x-nullable\` set, treat it as nullable (like \`nullable\` in OpenAPI 3.0.x)
--prettier-config, -c (optional) specify path to Prettier config file
--raw-schema (optional) Parse as partial schema (raw components)
--paths-enum, -pe (optional) Generate an enum containing all API paths.
Expand All @@ -48,6 +49,7 @@ const flags = parser(args, {
array: ["header"],
boolean: [
"defaultNonNullable",
"xNullableAsNullable",
"immutableTypes",
"contentNever",
"rawSchema",
Expand Down Expand Up @@ -98,6 +100,7 @@ async function generateSchema(pathToSpec) {
additionalProperties: flags.additionalProperties,
auth: flags.auth,
defaultNonNullable: flags.defaultNonNullable,
xNullableAsNullable: flags.xNullableAsNullable,
immutableTypes: flags.immutableTypes,
prettierConfig: flags.prettierConfig,
rawSchema: flags.rawSchema,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ async function openapiTS(
auth: options.auth,
commentHeader: typeof options.commentHeader === "string" ? options.commentHeader : COMMENT_HEADER,
defaultNonNullable: options.defaultNonNullable || false,
xNullableAsNullable: options.xNullableAsNullable || false,
formatter: options && typeof options.formatter === "function" ? options.formatter : undefined,
immutableTypes: options.immutableTypes || false,
contentNever: options.contentNever || false,
Expand Down
16 changes: 10 additions & 6 deletions src/transform/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ParsedSimpleValue,
} from "../utils.js";

interface TransformSchemaObjOptions extends GlobalContext {
export interface TransformSchemaObjOptions extends GlobalContext {
required: Set<string>;
}

Expand All @@ -32,7 +32,7 @@ export function transformSchemaObjMap(obj: Record<string, any>, options: Transfo
const v = obj[k];

// 1. Add comment in jsdoc notation
const comment = prepareComment(v);
const comment = prepareComment(v, options);
if (comment) output += comment;

// 2. name (with “?” if optional property)
Expand Down Expand Up @@ -86,6 +86,10 @@ export function transformOneOf(oneOf: any, options: TransformSchemaObjOptions):
return tsUnionOf(oneOf.map((value: any) => transformSchemaObj(value, options)));
}

export function isNodeNullable(node: any, options: TransformSchemaObjOptions): boolean {
return node.nullable || (options.xNullableAsNullable && node["x-nullable"]);
}

/** Convert schema object to TypeScript */
export function transformSchemaObj(node: any, options: TransformSchemaObjOptions): string {
const readonly = tsReadonly(options.immutableTypes);
Expand All @@ -96,7 +100,7 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
const overriddenType = options.formatter && options.formatter(node);

// open nullable
if (node.nullable) {
if (isNodeNullable(node, options)) {
output += "(";
}

Expand All @@ -117,13 +121,13 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
break;
}
case "const": {
output += parseSingleSimpleValue(node.const, node.nullable);
output += parseSingleSimpleValue(node.const, isNodeNullable(node, options));
break;
}
case "enum": {
const items: Array<ParsedSimpleValue> = [];
(node.enum as unknown[]).forEach((item) => {
const value = parseSingleSimpleValue(item, node.nullable);
const value = parseSingleSimpleValue(item, isNodeNullable(node, options));
items.push(value);
});
output += tsUnionOf(items);
Expand Down Expand Up @@ -221,7 +225,7 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
}

// close nullable
if (node.nullable) {
if (isNodeNullable(node, options)) {
output += ") | null";
}

Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export interface SwaggerToTSOptions {
contentNever?: boolean;
/** (optional) Treat schema objects with default values as non-nullable */
defaultNonNullable?: boolean;
/** (optional) Schemas with `x-nullable: true` should generate with `| null`, like `nullable` in OpenAPI 3.0.x */
xNullableAsNullable?: boolean;
/** (optional) Path to Prettier config */
prettierConfig?: string;
/** (optional) Parsing input document as raw schema rather than OpenAPI document */
Expand Down Expand Up @@ -180,6 +182,7 @@ export interface GlobalContext {
auth?: string;
commentHeader: string;
defaultNonNullable: boolean;
xNullableAsNullable: boolean;
formatter?: SchemaFormatter;
immutableTypes: boolean;
contentNever: boolean;
Expand Down
6 changes: 4 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { OpenAPI2, OpenAPI3, ReferenceObject } from "./types.js";
import { isNodeNullable, type TransformSchemaObjOptions } from "./transform/schema.js";

type CommentObject = {
const?: boolean; // jsdoc without value
Expand All @@ -9,6 +10,7 @@ type CommentObject = {
example?: string; // jsdoc with value
format?: string; // not jsdoc
nullable?: boolean; // Node information
"x-nullable"?: boolean; // Node information
title?: string; // not jsdoc
type: string; // Type of node
};
Expand All @@ -27,7 +29,7 @@ const FS_RE = /\//g;
* @see {comment} for output examples
* @returns void if not comments or jsdoc format comment string
*/
export function prepareComment(v: CommentObject): string | void {
export function prepareComment(v: CommentObject, options: TransformSchemaObjOptions): string | void {
const commentsArray: Array<string> = [];

// * Not JSDOC tags: [title, format]
Expand Down Expand Up @@ -58,7 +60,7 @@ export function prepareComment(v: CommentObject): string | void {

// * JSDOC 'Enum' with type
if (v.enum) {
const canBeNull = v.nullable ? `|${null}` : "";
const canBeNull = isNodeNullable(v, options) ? `|${null}` : "";
commentsArray.push(`@enum {${v.type}${canBeNull}}`);
}

Expand Down
21 changes: 21 additions & 0 deletions test/opts/expected/x-nullable-as-nullable.2.0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/

export interface paths {}

export interface definitions {
MyType: string;
/** @description Some value that has x-nullable set */
MyTypeXNullable: string | null;
/**
* @description Enum with x-nullable
* @enum {string|null}
*/
MyEnum: ("foo" | "bar") | null;
}

export interface operations {}

export interface external {}
23 changes: 23 additions & 0 deletions test/opts/expected/x-nullable-as-nullable.3.1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/

export interface paths {}

export interface components {
schemas: {
MyType: string;
/** @description Some value that has x-nullable set */
MyTypeXNullable: string | null;
/**
* @description Enum with x-nullable
* @enum {string|null}
*/
MyEnum: ("foo" | "bar") | null;
};
}

export interface operations {}

export interface external {}
67 changes: 67 additions & 0 deletions test/opts/x-nullable-as-nullable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect } from "chai";
import fs from "fs";
import eol from "eol";
import openapiTS from "../../dist/index.js";

describe("x-nullable-as-nullable", () => {
const cases = [
{
name: "swagger 2.0",
expectedFile: "x-nullable-as-nullable.2.0.ts",
schema: {
swagger: "2.0",
definitions: {
MyType: {
type: "string",
},
MyTypeXNullable: {
type: "string",
description: "Some value that has x-nullable set",
"x-nullable": true,
},
MyEnum: {
description: "Enum with x-nullable",
type: "string",
enum: ["foo", "bar"],
"x-nullable": true,
},
},
},
},
{
name: "openapi 3.1",
expectedFile: "x-nullable-as-nullable.3.1.ts",
schema: {
openapi: "3.1",
components: {
schemas: {
MyType: {
type: "string",
},
MyTypeXNullable: {
type: "string",
description: "Some value that has x-nullable set",
"x-nullable": true,
},
MyEnum: {
description: "Enum with x-nullable",
type: "string",
enum: ["foo", "bar"],
"x-nullable": true,
},
},
},
},
},
];

cases.forEach(({ name, expectedFile, schema }) => {
it(name, async () => {
const generated = await openapiTS(schema, {
xNullableAsNullable: true,
});
const expected = eol.lf(fs.readFileSync(new URL(`./expected/${expectedFile}`, import.meta.url), "utf8"));
expect(generated).to.equal(expected);
});
});
});
Loading