Skip to content
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
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
36 changes: 25 additions & 11 deletions src/swagger-2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,6 +34,7 @@ export interface Swagger2Options {
camelcase?: boolean;
wrapper?: string | false;
injectWarning?: boolean;
propertyMapper?: (swaggerDefinition: Swagger2Definition, property: Property) => Property;
}

export const warningMessage = `
Expand Down Expand Up @@ -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};`);
Expand Down
109 changes: 87 additions & 22 deletions tests/swagger-2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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',
}
},
},
};

Expand Down Expand Up @@ -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!)
Expand Down