Skip to content

Commit 439096d

Browse files
authored
Property mapper function (#118)
* Implement propertyMapper to handle x-something properties (ref #77) * Update readme (ref #77)
1 parent 2a4ebf3 commit 439096d

File tree

4 files changed

+146
-38
lines changed

4 files changed

+146
-38
lines changed

README.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,38 @@ If your specs are in YAML, you’ll have to convert them to JS objects using a l
131131

132132
#### Node Options
133133

134-
| Name | Type | Default | Description |
135-
| :---------- | :---------------: | :--------------------------: | :-------------------------------------------------------------------------- |
136-
| `wrapper` | `string \| false` | `declare namespace OpenAPI2` | How should this export the types? Pass false to disable rendering a wrapper |
137-
| `swagger` | `number` | `2` | Which Swagger version to use. Currently only supports `2`. |
138-
| `camelcase` | `boolean` | `false` | Convert `snake_case` properties to `camelCase` |
134+
| Name | Type | Default | Description |
135+
| :--------------- | :---------------: | :--------------------------: | :-------------------------------------------------------------------------- |
136+
| `wrapper` | `string \| false` | `declare namespace OpenAPI2` | How should this export the types? Pass false to disable rendering a wrapper |
137+
| `swagger` | `number` | `2` | Which Swagger version to use. Currently only supports `2`. |
138+
| `camelcase` | `boolean` | `false` | Convert `snake_case` properties to `camelCase` |
139+
| `propertyMapper` | `function` | `undefined` | Allows you to further manipulate how properties are parsed. See below. |
140+
141+
142+
#### PropertyMapper
143+
In order to allow more control over how properties are parsed, and to specifically handle `x-something`-properties, the `propertyMapper` option may be specified.
144+
145+
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.
146+
147+
An example on how to use the `x-nullable` property to control if a property is optional:
148+
149+
```
150+
const getNullable = (d: { [key: string]: any }): boolean => {
151+
const nullable = d['x-nullable'];
152+
if (typeof nullable === 'boolean') {
153+
return nullable;
154+
}
155+
return true;
156+
};
157+
158+
const propertyMapper = (
159+
swaggerDefinition: Swagger2Definition,
160+
property: Property
161+
): Property => ({ ...property, optional: getNullable(swaggerDefinition) });
162+
163+
const output = swaggerToTS(swagger, { propertyMapper });
164+
```
165+
139166

140167
[glob]: https://www.npmjs.com/package/glob
141168
[js-yaml]: https://www.npmjs.com/package/js-yaml

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import swagger2, { Swagger2, Swagger2Options } from './swagger-2';
2+
//re-export these from top-level as users may need thrm to create a propert5ymapper
3+
export { Swagger2Definition, Property } from './swagger-2';
24

35
export interface Options extends Swagger2Options {
46
swagger?: number;

src/swagger-2.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ export interface Swagger2Definition {
1212
additionalProperties?: boolean | Swagger2Definition;
1313
required?: string[];
1414
type?: 'array' | 'boolean' | 'integer' | 'number' | 'object' | 'string';
15+
// use this construct to allow arbitrary x-something properties. Must be any,
16+
// since we have no idea what they might be
17+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18+
[key: string]: any;
19+
}
20+
21+
export interface Property {
22+
interfaceType: string;
23+
optional: boolean;
24+
description?: string;
1525
}
1626

1727
export interface Swagger2 {
@@ -24,6 +34,7 @@ export interface Swagger2Options {
2434
camelcase?: boolean;
2535
wrapper?: string | false;
2636
injectWarning?: boolean;
37+
propertyMapper?: (swaggerDefinition: Swagger2Definition, property: Property) => Property;
2738
}
2839

2940
export const warningMessage = `
@@ -185,21 +196,24 @@ function parse(spec: Swagger2, options: Swagger2Options = {}): string {
185196

186197
// Populate interface
187198
Object.entries(allProperties).forEach(([key, value]): void => {
188-
const optional = !Array.isArray(required) || required.indexOf(key) === -1;
189199
const formattedKey = shouldCamelCase ? camelCase(key) : key;
190-
const name = `${sanitize(formattedKey)}${optional ? '?' : ''}`;
191200
const newID = `${ID}${capitalize(formattedKey)}`;
192-
const interfaceType = getType(value, newID);
201+
const interfaceType = Array.isArray(value.enum)
202+
? ` ${value.enum.map(option => JSON.stringify(option)).join(' | ')}` // Handle enums in the same definition
203+
: getType(value, newID);
193204

194-
if (typeof value.description === 'string') {
195-
// Print out descriptions as jsdoc comments, but only if there’s something there (.*)
196-
output.push(`/**\n* ${value.description.replace(/\n$/, '').replace(/\n/g, '\n* ')}\n*/`);
197-
}
205+
let property: Property = {
206+
interfaceType,
207+
optional: !Array.isArray(required) || required.indexOf(key) === -1,
208+
description: value.description,
209+
};
210+
property = options.propertyMapper ? options.propertyMapper(value, property) : property;
211+
212+
const name = `${sanitize(formattedKey)}${property.optional ? '?' : ''}`;
198213

199-
// Handle enums in the same definition
200-
if (Array.isArray(value.enum)) {
201-
output.push(`${name}: ${value.enum.map(option => JSON.stringify(option)).join(' | ')};`);
202-
return;
214+
if (typeof property.description === 'string') {
215+
// Print out descriptions as jsdoc comments, but only if there’s something there (.*)
216+
output.push(`/**\n* ${property.description.replace(/\n$/, '').replace(/\n/g, '\n* ')}\n*/`);
203217
}
204218

205219
output.push(`${name}: ${interfaceType};`);

tests/swagger-2.test.ts

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs';
22
import { resolve } from 'path';
33
import * as yaml from 'js-yaml';
44
import * as prettier from 'prettier';
5-
import swaggerToTS from '../src';
5+
import swaggerToTS, { Swagger2Definition, Property } from '../src';
66
import { Swagger2, warningMessage } from '../src/swagger-2';
77

88
/* eslint-disable @typescript-eslint/explicit-function-return-type */
@@ -390,39 +390,39 @@ describe('Swagger 2 spec', () => {
390390
definitions: {
391391
'User 1': {
392392
properties: {
393-
'profile_image': { type: 'string' },
394-
'address_line_1': { type: 'string' },
393+
profile_image: { type: 'string' },
394+
address_line_1: { type: 'string' },
395395
},
396396
type: 'object',
397397
},
398398
'User 1 Being Used': {
399399
properties: {
400-
'user': { $ref: '#/definitions/User 1' },
401-
'user_array': {
400+
user: { $ref: '#/definitions/User 1' },
401+
user_array: {
402402
type: 'array',
403403
items: { $ref: '#/definitions/User 1' },
404404
},
405-
'all_of_user': {
406-
allOf: [
407-
{ $ref: '#/definitions/User 1' },
408-
{
409-
properties: {
410-
other_field: { type: 'string' },
411-
},
412-
type: 'object',
405+
all_of_user: {
406+
allOf: [
407+
{ $ref: '#/definitions/User 1' },
408+
{
409+
properties: {
410+
other_field: { type: 'string' },
413411
},
414-
],
415-
type: 'object',
416-
},
417-
'wrapper': {
418-
properties: {
419-
user: { $ref: '#/definitions/User 1' },
412+
type: 'object',
420413
},
421-
type: 'object',
422-
}
414+
],
415+
type: 'object',
416+
},
417+
wrapper: {
418+
properties: {
419+
user: { $ref: '#/definitions/User 1' },
420+
},
421+
type: 'object',
422+
},
423423
},
424424
type: 'object',
425-
}
425+
},
426426
},
427427
};
428428

@@ -656,6 +656,71 @@ describe('Swagger 2 spec', () => {
656656
});
657657
});
658658

659+
describe('properties mapper', () => {
660+
const swagger: Swagger2 = {
661+
definitions: {
662+
Name: {
663+
properties: {
664+
first: { type: 'string' },
665+
last: { type: 'string', 'x-nullable': false },
666+
},
667+
type: 'object',
668+
},
669+
},
670+
};
671+
672+
it('accepts a mapper in options', () => {
673+
const propertyMapper = (
674+
swaggerDefinition: Swagger2Definition,
675+
property: Property
676+
): Property => property;
677+
swaggerToTS(swagger, { propertyMapper });
678+
});
679+
680+
it('passes definition to mapper', () => {
681+
const propertyMapper = jest.fn((def, prop) => prop);
682+
swaggerToTS(swagger, { propertyMapper });
683+
expect(propertyMapper).toBeCalledWith(
684+
//@ts-ignore
685+
swagger.definitions.Name.properties.first,
686+
expect.any(Object)
687+
);
688+
});
689+
690+
it('Uses result of mapper', () => {
691+
const wrapper = 'declare module MyNamespace';
692+
693+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
694+
const getNullable = (d: { [key: string]: any }): boolean => {
695+
const nullable = d['x-nullable'];
696+
if (typeof nullable === 'boolean') {
697+
return nullable;
698+
}
699+
return true;
700+
};
701+
702+
const propertyMapper = (
703+
swaggerDefinition: Swagger2Definition,
704+
property: Property
705+
): Property => ({ ...property, optional: getNullable(swaggerDefinition) });
706+
707+
swaggerToTS(swagger, { propertyMapper });
708+
709+
const ts = format(
710+
`
711+
export interface Name {
712+
first?: string;
713+
last: string;
714+
}
715+
`,
716+
wrapper,
717+
false
718+
);
719+
720+
expect(swaggerToTS(swagger, { wrapper, propertyMapper })).toBe(ts);
721+
});
722+
});
723+
659724
describe('snapshots', () => {
660725
// Basic snapshot test.
661726
// If changes are all good, run `npm run generate` to update (⚠️ This will cement your changes so be sure they’re 100% correct!)

0 commit comments

Comments
 (0)