Skip to content

Commit

Permalink
feat: add --x-nullable-as-nullable option to treat x-nullable the sam…
Browse files Browse the repository at this point in the history
…e as nullable (#1576)
  • Loading branch information
mitchell-merry authored Mar 4, 2024
1 parent 4c88d9d commit 77d92a3
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 8 deletions.
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);
});
});
});

0 comments on commit 77d92a3

Please sign in to comment.