diff --git a/src/internal/OpenApiTools/components/Schema.ts b/src/internal/OpenApiTools/components/Schema.ts index 49bfe8d6..689fa528 100644 --- a/src/internal/OpenApiTools/components/Schema.ts +++ b/src/internal/OpenApiTools/components/Schema.ts @@ -1,4 +1,4 @@ -import ts, { skipPartiallyEmittedExpressions } from "typescript"; +import ts from "typescript"; import type { OpenApi } from "../../../types"; import { FeatureDevelopmentError } from "../../Exception"; @@ -10,6 +10,20 @@ import type { AnySchema, ArraySchema, ObjectSchema, PrimitiveSchema } from "../t import type * as Walker from "../Walker"; import * as ExternalDocumentation from "./ExternalDocumentation"; +const nullable = (factory: Factory.Type, typeNode: ts.TypeNode, nullable: boolean): ts.TypeNode => { + if (nullable) { + return factory.UnionTypeNode.create({ + typeNodes: [ + typeNode, + factory.TypeNode.create({ + type: "null", + }), + ], + }); + } + return typeNode; +}; + export const generatePropertySignatures = ( entryPoint: string, currentPoint: string, @@ -42,6 +56,41 @@ export const generatePropertySignatures = ( }); }; +export const generateTypeAliasDeclarationForObject = ( + entryPoint: string, + currentPoint: string, + factory: Factory.Type, + name: string, + schema: ObjectSchema, + context: ToTypeNode.Context, + convertContext: ConvertContext.Types, +): ts.TypeAliasDeclaration => { + if (schema.type !== "object") { + throw new FeatureDevelopmentError("Please use generateTypeAlias"); + } + let members: ts.TypeElement[] = []; + const propertySignatures = generatePropertySignatures(entryPoint, currentPoint, factory, schema, context, convertContext); + if (Guard.isObjectSchemaWithAdditionalProperties(schema)) { + const additionalProperties = ToTypeNode.convertAdditionalProperties(entryPoint, currentPoint, factory, schema, context, convertContext); + if (schema.additionalProperties === true) { + members = members.concat(additionalProperties); + } else { + members = [...propertySignatures, additionalProperties]; + } + } else { + members = propertySignatures; + } + const typeNode = factory.TypeLiteralNode.create({ + members, + }); + return factory.TypeAliasDeclaration.create({ + export: true, + name: convertContext.escapeDeclarationText(name), + comment: [schema.title, schema.description].filter(v => !!v).join("\n\n"), + type: nullable(factory, typeNode, schema.nullable === true), + }); +}; + export const generateInterface = ( entryPoint: string, currentPoint: string, @@ -164,11 +213,10 @@ export const generateTypeAlias = ( type: schema.type, }); } - return factory.TypeAliasDeclaration.create({ export: true, name: convertContext.escapeDeclarationText(name), - type, + type: nullable(factory, type, schema.nullable === true), comment: [schema.title, schema.description].filter(v => !!v).join("\n\n"), }); }; @@ -239,11 +287,19 @@ export const addSchema = ( value: generateArrayTypeAlias(entryPoint, currentPoint, factory, declarationName, schema, context, convertContext), }); } else if (Guard.isObjectSchema(schema)) { - store.addStatement(targetPoint, { - kind: "interface", - name: convertContext.escapeDeclarationText(declarationName), - value: generateInterface(entryPoint, currentPoint, factory, declarationName, schema, context, convertContext), - }); + if (schema.nullable) { + store.addStatement(targetPoint, { + kind: "typeAlias", + name: convertContext.escapeDeclarationText(declarationName), + value: generateTypeAliasDeclarationForObject(entryPoint, currentPoint, factory, declarationName, schema, context, convertContext), + }); + } else { + store.addStatement(targetPoint, { + kind: "interface", + name: convertContext.escapeDeclarationText(declarationName), + value: generateInterface(entryPoint, currentPoint, factory, declarationName, schema, context, convertContext), + }); + } } else if (Guard.isPrimitiveSchema(schema)) { store.addStatement(targetPoint, { kind: "typeAlias", diff --git a/src/internal/OpenApiTools/components/Schemas.ts b/src/internal/OpenApiTools/components/Schemas.ts index 48f066b6..5dfa40c7 100644 --- a/src/internal/OpenApiTools/components/Schemas.ts +++ b/src/internal/OpenApiTools/components/Schemas.ts @@ -137,17 +137,17 @@ export const generateNamespace = ( ); } if (Guard.isObjectSchema(schema)) { - return store.addStatement( - path, - { - kind: "interface", - name: convertContext.escapeDeclarationText(name), - value: Schema.generateInterface(entryPoint, currentPoint, factory, name, schema, context, convertContext), - }, - { override: true }, - ); - } - if (Guard.isObjectSchema(schema)) { + if (schema.nullable) { + return store.addStatement( + path, + { + kind: "typeAlias", + name: convertContext.escapeDeclarationText(name), + value: Schema.generateTypeAliasDeclarationForObject(entryPoint, currentPoint, factory, name, schema, context, convertContext), + }, + { override: true }, + ); + } return store.addStatement( path, { diff --git a/test/__tests__/__snapshots__/spit-code-test.ts.snap b/test/__tests__/__snapshots__/spit-code-test.ts.snap index 942e9f01..57d92e58 100644 --- a/test/__tests__/__snapshots__/spit-code-test.ts.snap +++ b/test/__tests__/__snapshots__/spit-code-test.ts.snap @@ -340,6 +340,13 @@ export namespace Schemas { export type RemoteRefArray = Schemas.Level1.Level2.Level3.RemoteArray; /** Level 4 */ export type RemoteRefObject = Schemas.Level1.Level2.Level3.Level4.RemoteObject; + export type NullableString = string | null; + export type NullableBoolean = boolean | null; + export type NullableNumber = number | null; + export type NullableArray = string[] | null; + export type NullableObject = { + gt?: string; + } | null; export namespace DirectRef { export type ForHeader = string; export interface ForResponse { diff --git a/test/__tests__/__snapshots__/typedef-only-test.ts.snap b/test/__tests__/__snapshots__/typedef-only-test.ts.snap index cf757e5a..1e9af253 100644 --- a/test/__tests__/__snapshots__/typedef-only-test.ts.snap +++ b/test/__tests__/__snapshots__/typedef-only-test.ts.snap @@ -141,6 +141,13 @@ export namespace Schemas { export type RemoteRefArray = Schemas.Level1.Level2.Level3.RemoteArray; /** Level 4 */ export type RemoteRefObject = Schemas.Level1.Level2.Level3.Level4.RemoteObject; + export type NullableString = string | null; + export type NullableBoolean = boolean | null; + export type NullableNumber = number | null; + export type NullableArray = string[] | null; + export type NullableObject = { + gt?: string; + } | null; export namespace DirectRef { export type ForHeader = string; export interface ForResponse { diff --git a/test/__tests__/__snapshots__/typedef-with-template-test.ts.snap b/test/__tests__/__snapshots__/typedef-with-template-test.ts.snap index 69a9832f..bceb33c3 100644 --- a/test/__tests__/__snapshots__/typedef-with-template-test.ts.snap +++ b/test/__tests__/__snapshots__/typedef-with-template-test.ts.snap @@ -107,6 +107,13 @@ export namespace Schemas { export type RemoteRefArray = Schemas.Level1.Level2.Level3.RemoteArray; /** Level 4 */ export type RemoteRefObject = Schemas.Level1.Level2.Level3.Level4.RemoteObject; + export type NullableString = string | null; + export type NullableBoolean = boolean | null; + export type NullableNumber = number | null; + export type NullableArray = string[] | null; + export type NullableObject = { + gt?: string; + } | null; export namespace DirectRef { export type ForHeader = string; export interface ForResponse { @@ -650,6 +657,13 @@ export namespace Schemas { export type RemoteRefArray = Schemas.Level1.Level2.Level3.RemoteArray; /** Level 4 */ export type RemoteRefObject = Schemas.Level1.Level2.Level3.Level4.RemoteObject; + export type NullableString = string | null; + export type NullableBoolean = boolean | null; + export type NullableNumber = number | null; + export type NullableArray = string[] | null; + export type NullableObject = { + gt?: string; + } | null; export namespace DirectRef { export type ForHeader = string; export interface ForResponse { diff --git a/test/api.test.domain/index.yml b/test/api.test.domain/index.yml index a7fdd979..aae8ddd2 100644 --- a/test/api.test.domain/index.yml +++ b/test/api.test.domain/index.yml @@ -207,6 +207,26 @@ components: $ref: "./components/schemas/Level1/Level2/Level3/RemoteArray.yml" RemoteRefObject: $ref: "./components/schemas/Level1/Level2/Level3/Level4/RemoteObject.yml" + NullableString: + type: string + nullable: true + NullableBoolean: + type: boolean + nullable: true + NullableNumber: + type: number + nullable: true + NullableArray: + type: array + items: + type: string + nullable: true + NullableObject: + type: object + nullable: true + properties: + gt: + type: string headers: StringHeader: schema: