From ec3068d7a4a926f5cfdad9a03ef2a1c9157cd5ba Mon Sep 17 00:00:00 2001 From: Atle Frenvik Sveen Date: Wed, 15 Jan 2020 12:07:30 +0100 Subject: [PATCH 1/2] Implement propertyMapper to handle x-something properties (ref #77) --- src/index.ts | 2 + src/swagger-2.ts | 36 +++++++++---- tests/swagger-2.test.ts | 109 ++++++++++++++++++++++++++++++++-------- 3 files changed, 114 insertions(+), 33 deletions(-) diff --git a/src/index.ts b/src/index.ts index 20ec794ed..512250635 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import swagger2, { Swagger2, Swagger2Options } from './swagger-2'; +//re-export these from top-level as users may need thrm to create a propert5ymapper +export { Swagger2Definition, Property } from './swagger-2'; export interface Options extends Swagger2Options { swagger?: number; diff --git a/src/swagger-2.ts b/src/swagger-2.ts index 5eee6c171..6297503d5 100644 --- a/src/swagger-2.ts +++ b/src/swagger-2.ts @@ -12,6 +12,16 @@ export interface Swagger2Definition { additionalProperties?: boolean | Swagger2Definition; required?: string[]; type?: 'array' | 'boolean' | 'integer' | 'number' | 'object' | 'string'; + // use this construct to allow arbitrary x-something properties. Must be any, + // since we have no idea what they might be + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +export interface Property { + interfaceType: string; + optional: boolean; + description?: string; } export interface Swagger2 { @@ -24,6 +34,7 @@ export interface Swagger2Options { camelcase?: boolean; wrapper?: string | false; injectWarning?: boolean; + propertyMapper?: (swaggerDefinition: Swagger2Definition, property: Property) => Property; } export const warningMessage = ` @@ -185,21 +196,24 @@ function parse(spec: Swagger2, options: Swagger2Options = {}): string { // Populate interface Object.entries(allProperties).forEach(([key, value]): void => { - const optional = !Array.isArray(required) || required.indexOf(key) === -1; const formattedKey = shouldCamelCase ? camelCase(key) : key; - const name = `${sanitize(formattedKey)}${optional ? '?' : ''}`; const newID = `${ID}${capitalize(formattedKey)}`; - const interfaceType = getType(value, newID); + const interfaceType = Array.isArray(value.enum) + ? ` ${value.enum.map(option => JSON.stringify(option)).join(' | ')}` // Handle enums in the same definition + : getType(value, newID); - if (typeof value.description === 'string') { - // Print out descriptions as jsdoc comments, but only if there’s something there (.*) - output.push(`/**\n* ${value.description.replace(/\n$/, '').replace(/\n/g, '\n* ')}\n*/`); - } + let property: Property = { + interfaceType, + optional: !Array.isArray(required) || required.indexOf(key) === -1, + description: value.description, + }; + property = options.propertyMapper ? options.propertyMapper(value, property) : property; + + const name = `${sanitize(formattedKey)}${property.optional ? '?' : ''}`; - // Handle enums in the same definition - if (Array.isArray(value.enum)) { - output.push(`${name}: ${value.enum.map(option => JSON.stringify(option)).join(' | ')};`); - return; + if (typeof property.description === 'string') { + // Print out descriptions as jsdoc comments, but only if there’s something there (.*) + output.push(`/**\n* ${property.description.replace(/\n$/, '').replace(/\n/g, '\n* ')}\n*/`); } output.push(`${name}: ${interfaceType};`); diff --git a/tests/swagger-2.test.ts b/tests/swagger-2.test.ts index 004a90015..803f026ef 100644 --- a/tests/swagger-2.test.ts +++ b/tests/swagger-2.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; import * as yaml from 'js-yaml'; import * as prettier from 'prettier'; -import swaggerToTS from '../src'; +import swaggerToTS, { Swagger2Definition, Property } from '../src'; import { Swagger2, warningMessage } from '../src/swagger-2'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ @@ -390,39 +390,39 @@ describe('Swagger 2 spec', () => { definitions: { 'User 1': { properties: { - 'profile_image': { type: 'string' }, - 'address_line_1': { type: 'string' }, + profile_image: { type: 'string' }, + address_line_1: { type: 'string' }, }, type: 'object', }, 'User 1 Being Used': { properties: { - 'user': { $ref: '#/definitions/User 1' }, - 'user_array': { + user: { $ref: '#/definitions/User 1' }, + user_array: { type: 'array', items: { $ref: '#/definitions/User 1' }, }, - 'all_of_user': { - allOf: [ - { $ref: '#/definitions/User 1' }, - { - properties: { - other_field: { type: 'string' }, - }, - type: 'object', + all_of_user: { + allOf: [ + { $ref: '#/definitions/User 1' }, + { + properties: { + other_field: { type: 'string' }, }, - ], - type: 'object', - }, - 'wrapper': { - properties: { - user: { $ref: '#/definitions/User 1' }, + type: 'object', }, - type: 'object', - } + ], + type: 'object', + }, + wrapper: { + properties: { + user: { $ref: '#/definitions/User 1' }, + }, + type: 'object', + }, }, type: 'object', - } + }, }, }; @@ -656,6 +656,71 @@ describe('Swagger 2 spec', () => { }); }); + describe('properties mapper', () => { + const swagger: Swagger2 = { + definitions: { + Name: { + properties: { + first: { type: 'string' }, + last: { type: 'string', 'x-nullable': false }, + }, + type: 'object', + }, + }, + }; + + it('accepts a mapper in options', () => { + const propertyMapper = ( + swaggerDefinition: Swagger2Definition, + property: Property + ): Property => property; + swaggerToTS(swagger, { propertyMapper }); + }); + + it('passes definition to mapper', () => { + const propertyMapper = jest.fn((def, prop) => prop); + swaggerToTS(swagger, { propertyMapper }); + expect(propertyMapper).toBeCalledWith( + //@ts-ignore + swagger.definitions.Name.properties.first, + expect.any(Object) + ); + }); + + it('Uses result of mapper', () => { + const wrapper = 'declare module MyNamespace'; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getNullable = (d: { [key: string]: any }): boolean => { + const nullable = d['x-nullable']; + if (typeof nullable === 'boolean') { + return nullable; + } + return true; + }; + + const propertyMapper = ( + swaggerDefinition: Swagger2Definition, + property: Property + ): Property => ({ ...property, optional: getNullable(swaggerDefinition) }); + + swaggerToTS(swagger, { propertyMapper }); + + const ts = format( + ` + export interface Name { + first?: string; + last: string; + } + `, + wrapper, + false + ); + + expect(swaggerToTS(swagger, { wrapper, propertyMapper })).toBe(ts); + }); + }); + describe('snapshots', () => { // Basic snapshot test. // If changes are all good, run `npm run generate` to update (⚠️ This will cement your changes so be sure they’re 100% correct!) From 794c16b14d5036cc9d4dcf89919faf0043670ff1 Mon Sep 17 00:00:00 2001 From: Atle Frenvik Sveen Date: Wed, 15 Jan 2020 12:12:12 +0100 Subject: [PATCH 2/2] Update readme (ref #77) --- README.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 75ae2ba24..26bf1d1f4 100644 --- a/README.md +++ b/README.md @@ -131,11 +131,38 @@ If your specs are in YAML, you’ll have to convert them to JS objects using a l #### Node Options -| Name | Type | Default | Description | -| :---------- | :---------------: | :--------------------------: | :-------------------------------------------------------------------------- | -| `wrapper` | `string \| false` | `declare namespace OpenAPI2` | How should this export the types? Pass false to disable rendering a wrapper | -| `swagger` | `number` | `2` | Which Swagger version to use. Currently only supports `2`. | -| `camelcase` | `boolean` | `false` | Convert `snake_case` properties to `camelCase` | +| Name | Type | Default | Description | +| :--------------- | :---------------: | :--------------------------: | :-------------------------------------------------------------------------- | +| `wrapper` | `string \| false` | `declare namespace OpenAPI2` | How should this export the types? Pass false to disable rendering a wrapper | +| `swagger` | `number` | `2` | Which Swagger version to use. Currently only supports `2`. | +| `camelcase` | `boolean` | `false` | Convert `snake_case` properties to `camelCase` | +| `propertyMapper` | `function` | `undefined` | Allows you to further manipulate how properties are parsed. See below. | + + +#### PropertyMapper +In order to allow more control over how properties are parsed, and to specifically handle `x-something`-properties, the `propertyMapper` option may be specified. + +This is a function that, if specified, is called for each property and allows you to change how swagger-to-ts handles parsing of swagger files. + +An example on how to use the `x-nullable` property to control if a property is optional: + +``` + const getNullable = (d: { [key: string]: any }): boolean => { + const nullable = d['x-nullable']; + if (typeof nullable === 'boolean') { + return nullable; + } + return true; + }; + + const propertyMapper = ( + swaggerDefinition: Swagger2Definition, + property: Property + ): Property => ({ ...property, optional: getNullable(swaggerDefinition) }); + + const output = swaggerToTS(swagger, { propertyMapper }); +``` + [glob]: https://www.npmjs.com/package/glob [js-yaml]: https://www.npmjs.com/package/js-yaml