diff --git a/__tests__/__snapshots__/runner.js.snap b/__tests__/__snapshots__/runner.js.snap index 45d4e05c..97baf961 100644 --- a/__tests__/__snapshots__/runner.js.snap +++ b/__tests__/__snapshots__/runner.js.snap @@ -738,6 +738,8 @@ export type Order = { 'shipDate' ? : string; 'status' ? : \\"placed\\" | \\"approved\\" | \\"delivered\\"; 'complete' ? : boolean; +} & { + [key: string]: any; }; export type User = { @@ -749,16 +751,22 @@ export type User = { 'password' ? : string; 'phone' ? : string; 'userStatus' ? : number; +} & { + [key: string]: any; }; export type Category = { 'id' ? : number; 'name' ? : string; +} & { + [key: string]: any; }; export type Tag = { 'id' ? : number; 'name' ? : string; +} & { + [key: string]: any; }; export type Pet = { @@ -770,12 +778,16 @@ export type Pet = { 'tags' ? : Array < Tag > ; 'status' ? : \\"available\\" | \\"pending\\" | \\"sold\\"; +} & { + [key: string]: any; }; export type ApiResponse = { 'code': number; 'type': string; 'message' ? : string; +} & { + [key: string]: any; }; export type Logger = { @@ -1425,9 +1437,7 @@ export class PetshopApi { * @method * @name PetshopApi#getInventory */ - getInventory(parameters: {} & CommonRequestOptions): Promise < ResponseWithBody < 200, { - [key: string]: number - } >> { + getInventory(parameters: {} & CommonRequestOptions): Promise < ResponseWithBody < 200, object >> { const domain = parameters.$domain ? parameters.$domain : this.domain; let path = '/store/inventory'; if (parameters.$path) { @@ -2464,6 +2474,270 @@ export class UsersApi { export default UsersApi;" `; +exports[`Should resolve "additionalProperties" 1`] = ` +"// tslint:disable + +import * as request from \\"superagent\\"; +import { + SuperAgentStatic, + SuperAgentRequest, + Response +} from \\"superagent\\"; + +export type RequestHeaders = { + [header: string]: string; +} +export type RequestHeadersHandler = (headers: RequestHeaders) => RequestHeaders; + +export type ConfigureAgentHandler = (agent: SuperAgentStatic) => SuperAgentStatic; + +export type ConfigureRequestHandler = (agent: SuperAgentRequest) => SuperAgentRequest; + +export type CallbackHandler = (err: any, res ? : request.Response) => void; + +export type some_def = { + 'some_def' ? : string; +}; + +export type test_add_props_01 = { + 'some_prop' ? : string; +} & { + [key: string]: any; +}; + +export type test_add_props_02 = { + 'some_prop' ? : string; +}; + +export type test_add_props_03 = { + 'some_prop' ? : string; +} & { + [key: string]: any; +}; + +export type test_add_props_04 = { + 'some_prop' ? : string; +} & { + [key: string]: string; +}; + +export type test_add_props_05 = { + 'some_prop' ? : string; +} & { + [key: string]: { + 'nested_prop' ? : string; + } & { + [key: string]: any; + }; +}; + +export type test_add_props_06 = { + 'some_prop' ? : string; +} & { + [key: string]: some_def; +}; + +export type test_add_props_07 = {} & { + [key: string]: any; +}; + +export type test_add_props_08 = {}; + +export type test_add_props_09 = {} & { + [key: string]: any; +}; + +export type test_add_props_10 = {} & { + [key: string]: string; +}; + +export type test_add_props_11 = {} & { + [key: string]: { + 'nested_prop' ? : string; + } & { + [key: string]: any; + }; +}; + +export type test_add_props_12 = {} & { + [key: string]: some_def; +}; + +export type Logger = { + log: (line: string) => any +}; + +export interface ResponseWithBody < S extends number, T > extends Response { + status: S; + body: T; +} + +export type QueryParameters = { + [param: string]: any +}; + +export interface CommonRequestOptions { + $queryParameters ? : QueryParameters; + $domain ? : string; + $path ? : string | ((path: string) => string); + $retries ? : number; // number of retries; see: https://github.com/visionmedia/superagent/blob/master/docs/index.md#retrying-requests + $timeout ? : number; // request timeout in milliseconds; see: https://github.com/visionmedia/superagent/blob/master/docs/index.md#timeouts + $deadline ? : number; // request deadline in milliseconds; see: https://github.com/visionmedia/superagent/blob/master/docs/index.md#timeouts +} + +/** + * + * @class AddpropsApi + * @param {(string)} [domainOrOptions] - The project domain. + */ +export class AddpropsApi { + + private domain: string = \\"\\"; + private errorHandlers: CallbackHandler[] = []; + private requestHeadersHandler ? : RequestHeadersHandler; + private configureAgentHandler ? : ConfigureAgentHandler; + private configureRequestHandler ? : ConfigureRequestHandler; + + constructor(domain ? : string, private logger ? : Logger) { + if (domain) { + this.domain = domain; + } + } + + getDomain() { + return this.domain; + } + + addErrorHandler(handler: CallbackHandler) { + this.errorHandlers.push(handler); + } + + setRequestHeadersHandler(handler: RequestHeadersHandler) { + this.requestHeadersHandler = handler; + } + + setConfigureAgentHandler(handler: ConfigureAgentHandler) { + this.configureAgentHandler = handler; + } + + setConfigureRequestHandler(handler: ConfigureRequestHandler) { + this.configureRequestHandler = handler; + } + + private request(method: string, url: string, body: any, headers: RequestHeaders, queryParameters: QueryParameters, form: any, reject: CallbackHandler, resolve: CallbackHandler, opts: CommonRequestOptions) { + if (this.logger) { + this.logger.log(\`Call \${method} \${url}\`); + } + + const agent = this.configureAgentHandler ? + this.configureAgentHandler(request.default) : + request.default; + + let req = agent(method, url); + if (this.configureRequestHandler) { + req = this.configureRequestHandler(req); + } + + req = req.query(queryParameters); + + if (body) { + req.send(body); + + if (typeof(body) === 'object' && !(body.constructor.name === 'Buffer')) { + headers['Content-Type'] = 'application/json'; + } + } + + if (Object.keys(form).length > 0) { + req.type('form'); + req.send(form); + } + + if (this.requestHeadersHandler) { + headers = this.requestHeadersHandler({ + ...headers + }); + } + + req.set(headers); + + if (opts.$retries && opts.$retries > 0) { + req.retry(opts.$retries); + } + + if (opts.$timeout && opts.$timeout > 0 || opts.$deadline && opts.$deadline > 0) { + req.timeout({ + deadline: opts.$deadline, + response: opts.$timeout + }); + } + + req.end((error, response) => { + // an error will also be emitted for a 4xx and 5xx status code + // the error object will then have error.status and error.response fields + // see superagent error handling: https://github.com/visionmedia/superagent/blob/master/docs/index.md#error-handling + if (error) { + reject(error); + this.errorHandlers.forEach(handler => handler(error)); + } else { + resolve(response); + } + }); + } + + get_personURL(parameters: {} & CommonRequestOptions): string { + let queryParameters: QueryParameters = {}; + const domain = parameters.$domain ? parameters.$domain : this.domain; + let path = '/persons'; + if (parameters.$path) { + path = (typeof(parameters.$path) === 'function') ? parameters.$path(path) : parameters.$path; + } + + if (parameters.$queryParameters) { + queryParameters = { + ...queryParameters, + ...parameters.$queryParameters + }; + } + + let keys = Object.keys(queryParameters); + return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : ''); + } + + /** + * Gets \`Person\` object. + * @method + * @name AddpropsApi#get_person + */ + get_person(parameters: {} & CommonRequestOptions): Promise < ResponseWithBody < 200, void >> { + const domain = parameters.$domain ? parameters.$domain : this.domain; + let path = '/persons'; + if (parameters.$path) { + path = (typeof(parameters.$path) === 'function') ? parameters.$path(path) : parameters.$path; + } + + let body: any; + let queryParameters: QueryParameters = {}; + let headers: RequestHeaders = {}; + let form: any = {}; + return new Promise((resolve, reject) => { + + if (parameters.$queryParameters) { + queryParameters = { + ...queryParameters, + ...parameters.$queryParameters + }; + } + + this.request('GET', domain + path, body, headers, queryParameters, form, reject, resolve, parameters); + }); + } + +} + +export default AddpropsApi;" +`; + exports[`Should resolve protected api 1`] = ` "// tslint:disable diff --git a/__tests__/fixtures/addProps/swagger.json b/__tests__/fixtures/addProps/swagger.json new file mode 100644 index 00000000..1af3bcc0 --- /dev/null +++ b/__tests__/fixtures/addProps/swagger.json @@ -0,0 +1,29 @@ +{ + "swagger": "2.0", + "info": { "version": "0.0.1", "title": "your title" }, + "paths": { + "/persons": { + "get": { + "operationId": "get_person", + "description": "Gets `Person` object.", + "responses": { "200": { "description": "empty schema" } } + } + } + }, + "definitions": { + "some_def": { "type": "object", "properties": { "some_def": { "type": "string" } }, "additionalProperties": false }, + + "test_add_props_01": { "type": "object", "properties": { "some_prop": { "type": "string" } } }, + "test_add_props_02": { "type": "object", "properties": { "some_prop": { "type": "string" } }, "additionalProperties": false }, + "test_add_props_03": { "type": "object", "properties": { "some_prop": { "type": "string" } }, "additionalProperties": true }, + "test_add_props_04": { "type": "object", "properties": { "some_prop": { "type": "string" } }, "additionalProperties": { "type": "string" } }, + "test_add_props_05": { "type": "object", "properties": { "some_prop": { "type": "string" } }, "additionalProperties": { "type": "object", "properties": { "nested_prop": { "type": "string" } } } }, + "test_add_props_06": { "type": "object", "properties": { "some_prop": { "type": "string" } }, "additionalProperties": { "$ref": "#/definitions/some_def" } }, + "test_add_props_07": { "type": "object" }, + "test_add_props_08": { "type": "object", "additionalProperties": false }, + "test_add_props_09": { "type": "object", "additionalProperties": true }, + "test_add_props_10": { "type": "object", "additionalProperties": { "type": "string" } }, + "test_add_props_11": { "type": "object", "additionalProperties": { "type": "object", "properties": { "nested_prop": { "type": "string" } } } }, + "test_add_props_12": { "type": "object", "additionalProperties": { "$ref": "#/definitions/some_def" } } + } +} diff --git a/__tests__/runner.js b/__tests__/runner.js index 52fd99bb..5ecdff73 100644 --- a/__tests__/runner.js +++ b/__tests__/runner.js @@ -13,6 +13,10 @@ var testCases = [ desc: "Should resolve references", fixture: "ref" }, + { + desc: "Should resolve \"additionalProperties\"", + fixture: "addProps" + }, { desc: "Real world: Uber", fixture: "uber" diff --git a/src/swagger/Swagger.ts b/src/swagger/Swagger.ts index e97e7b75..ffc5540a 100644 --- a/src/swagger/Swagger.ts +++ b/src/swagger/Swagger.ts @@ -21,6 +21,7 @@ export interface SwaggerType { readonly properties: { readonly [index: string]: SwaggerType; }; + readonly additionalProperties?: boolean | SwaggerType; } export interface SwaggerArray extends SwaggerType { diff --git a/src/test-helpers/testHelpers.ts b/src/test-helpers/testHelpers.ts index 2f435315..93f1cc23 100644 --- a/src/test-helpers/testHelpers.ts +++ b/src/test-helpers/testHelpers.ts @@ -37,7 +37,6 @@ export function makeEmptyTypeSpec(): TypeSpec { description: undefined, isEnum: false, isArray: false, - isDictionary: false, isObject: false, isRef: false, isNullable: false, @@ -45,6 +44,8 @@ export function makeEmptyTypeSpec(): TypeSpec { tsType: undefined, isAtomic: false, target: undefined, - properties: undefined + properties: undefined, + hasAdditionalProperties: false, + additionalPropertiesType: undefined }; } diff --git a/src/type-mappers/dictionary.ts b/src/type-mappers/dictionary.ts deleted file mode 100644 index bff984cd..00000000 --- a/src/type-mappers/dictionary.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { makeTypeSpecFromSwaggerType, TypeSpec } from "../typespec"; -import { convertType } from "../typescript"; -import { SwaggerDictionary, SwaggerType } from "../swagger/Swagger"; -import { Swagger } from "../swagger/Swagger"; - -export interface DictionaryTypeSpec extends TypeSpec { - readonly tsType: string; - readonly isAtomic: false; - readonly isDictionary: true; - readonly isArray: false; - readonly elementType: TypeSpec; -} - -export function makeDictionaryTypeSpec( - swaggerType: SwaggerDictionary, - swagger: Swagger -): DictionaryTypeSpec { - const elementTypeSpec = convertType( - swaggerType.additionalProperties, - swagger - ); - - return { - ...makeTypeSpecFromSwaggerType(swaggerType), - elementType: elementTypeSpec, - tsType: `{ [key: string]: ${elementTypeSpec.target || - elementTypeSpec.tsType || - "any"} }`, - isArray: false, - isAtomic: false, - isDictionary: true - }; -} - -export function isDictionary( - swaggerType: SwaggerType -): swaggerType is SwaggerDictionary { - return ( - swaggerType.type === "object" && - swaggerType.hasOwnProperty("additionalProperties") && - (swaggerType as any).additionalProperties !== false - ); -} diff --git a/src/type-mappers/object.ts b/src/type-mappers/object.ts index b1fdfa24..5e98c246 100644 --- a/src/type-mappers/object.ts +++ b/src/type-mappers/object.ts @@ -12,6 +12,7 @@ import { import { SwaggerType } from "../swagger/Swagger"; import { Swagger } from "../swagger/Swagger"; import { convertType } from "../typescript"; +import { makeAnyTypeSpec } from "./any"; export interface ObjectTypeSpec extends TypeSpec { readonly tsType: "object"; @@ -19,6 +20,35 @@ export interface ObjectTypeSpec extends TypeSpec { readonly isObject: true; readonly requiredPropertyNames: ReadonlyArray; readonly properties: ReadonlyArray; + readonly hasAdditionalProperties: boolean; + readonly additionalPropertiesType: TypeSpec | undefined; +} + +// see: https://support.reprezen.com/support/solutions/articles/6000162892-support-for-additionalproperties-in-swagger-2-0-schemas +export function extractAdditionalPropertiesType( + swaggerType: SwaggerType, + swagger: Swagger +): TypeSpec | undefined { + if (swaggerType.type !== "object") { + return undefined; + } + if (swaggerType.additionalProperties === false) { + return undefined; + } + if ( + swaggerType.additionalProperties === undefined || + swaggerType.additionalProperties === true + ) { + // is there an easier way to make an "any" type? + return makeAnyTypeSpec({ + type: "object", + required: [], + minItems: 0, + title: "any", + properties: {} + }); + } + return convertType(swaggerType.additionalProperties, swagger); } export function makeObjectTypeSpec( @@ -41,13 +71,17 @@ export function makeObjectTypeSpec( const uniqueProperties = uniqBy(reverse(allProperties), "name"); const properties = reverse(uniqueProperties); + const addPropsType = extractAdditionalPropertiesType(swaggerType, swagger); + return { ...makeTypeSpecFromSwaggerType(swaggerType), tsType: "object", isObject: true, isAtomic: false, properties, - requiredPropertyNames + requiredPropertyNames, + hasAdditionalProperties: addPropsType !== undefined, + additionalPropertiesType: addPropsType }; } diff --git a/src/typescript.test.ts b/src/typescript.test.ts index 1c1696c1..ac88869d 100644 --- a/src/typescript.test.ts +++ b/src/typescript.test.ts @@ -183,7 +183,11 @@ describe("convertType", () => { description: "The description of a array property", required: false, type: "array", - items: makeSwaggerType({ type: "object", required: false }) + items: makeSwaggerType({ + type: "object", + required: false, + additionalProperties: false + }) }); expect(convertType(swaggerType, swagger)).toEqual({ @@ -216,14 +220,16 @@ describe("convertType", () => { expect(convertType(swaggerType, swagger)).toEqual({ ...emptyTypeSpecWithDefaults, - tsType: "{ [key: string]: number }", + tsType: "object", isAtomic: false, - isArray: false, - isDictionary: true, - elementType: { + isObject: true, + properties: [], + requiredPropertyNames: [], + hasAdditionalProperties: true, + additionalPropertiesType: { ...emptyTypeSpecWithDefaults, - tsType: "number", - isAtomic: true + isAtomic: true, + tsType: "number" } }); }); @@ -239,16 +245,18 @@ describe("convertType", () => { expect(convertType(swaggerType, swagger)).toEqual({ ...emptyTypeSpecWithDefaults, description: "The description of a dictionary property", - isRequired: false, - isNullable: true, - isArray: false, - isDictionary: true, - tsType: "{ [key: string]: number }", + tsType: "object", isAtomic: false, - elementType: { + isObject: true, + isNullable: true, + isRequired: false, + properties: [], + requiredPropertyNames: [], + hasAdditionalProperties: true, + additionalPropertiesType: { ...emptyTypeSpecWithDefaults, - tsType: "number", - isAtomic: true + isAtomic: true, + tsType: "number" } }); }); @@ -292,7 +300,8 @@ describe("convertType", () => { describe("object", () => { it("correctly converts an object type", () => { swaggerType = makeSwaggerType({ - type: "object" + type: "object", + additionalProperties: false }); expect(convertType(swaggerType, swagger)).toEqual({ @@ -321,7 +330,13 @@ describe("convertType", () => { isAtomic: false, isObject: true, properties: [], - requiredPropertyNames: [] + requiredPropertyNames: [], + hasAdditionalProperties: true, + additionalPropertiesType: { + ...emptyTypeSpecWithDefaults, + isAtomic: true, + tsType: "any" + } }); }); @@ -330,7 +345,8 @@ describe("convertType", () => { type: "object", properties: { age: makeSwaggerType({ type: "number" }) - } + }, + additionalProperties: false }); expect(convertType(swaggerType, swagger)).toEqual({ @@ -362,19 +378,65 @@ describe("convertType", () => { tsType: "object", isAtomic: false, isObject: true, - isDictionary: false, properties: [], requiredPropertyNames: [] }); }); + it("correctly converts an object type with additionalProperties: true", () => { + swaggerType = makeSwaggerType({ + type: "object", + additionalProperties: true + }); + + expect(convertType(swaggerType, swagger)).toEqual({ + ...emptyTypeSpecWithDefaults, + tsType: "object", + isAtomic: false, + isObject: true, + properties: [], + requiredPropertyNames: [], + hasAdditionalProperties: true, + additionalPropertiesType: { + ...emptyTypeSpecWithDefaults, + isAtomic: true, + tsType: "any" + } + }); + }); + + it("correctly converts an object type with a missing additionalProperties field", () => { + // this needs to be treated exactly the same as with "additionalProperties = true" + // see: https://support.reprezen.com/support/solutions/articles/6000162892-support-for-additionalproperties-in-swagger-2-0-schemas + swaggerType = makeSwaggerType({ + type: "object", + additionalProperties: undefined + }); + + expect(convertType(swaggerType, swagger)).toEqual({ + ...emptyTypeSpecWithDefaults, + tsType: "object", + isAtomic: false, + isObject: true, + properties: [], + requiredPropertyNames: [], + hasAdditionalProperties: true, + additionalPropertiesType: { + ...emptyTypeSpecWithDefaults, + isAtomic: true, + tsType: "any" + } + }); + }); + it("handles required properties", () => { swaggerType = makeSwaggerType({ type: "object", properties: { age: makeSwaggerType({ type: "number" }) }, - required: ["age"] + required: ["age"], + additionalProperties: false }); expect(convertType(swaggerType, swagger)).toEqual({ @@ -405,10 +467,12 @@ describe("convertType", () => { type: "object", properties: { age: makeSwaggerType({ type: "number" }) - } + }, + additionalProperties: false }) ], - required: ["age"] + required: ["age"], + additionalProperties: false }); expect(convertType(swaggerType, swagger)).toEqual({ @@ -437,7 +501,8 @@ describe("convertType", () => { type: "object", properties: { age: makeSwaggerType({ type: "number" }) - } + }, + additionalProperties: false }) } }; @@ -450,7 +515,8 @@ describe("convertType", () => { type: "reference" }) ], - required: ["age"] + required: ["age"], + additionalProperties: false }); expect(convertType(swaggerType, swagger)).toEqual({ @@ -479,7 +545,8 @@ describe("convertType", () => { type: "object", properties: { age: makeSwaggerType({ type: "number" }) - } + }, + additionalProperties: false }) } }; @@ -492,7 +559,8 @@ describe("convertType", () => { type: "reference" }) ], - required: ["age"] + required: ["age"], + additionalProperties: false }); expect(convertType(swaggerType, swagger)).toEqual({ diff --git a/src/typescript.ts b/src/typescript.ts index a2b11d5e..68a48475 100644 --- a/src/typescript.ts +++ b/src/typescript.ts @@ -8,10 +8,6 @@ import { makeStringTypeSpec, isString } from "./type-mappers/string"; import { makeNumberTypeSpec, isNumber } from "./type-mappers/number"; import { makeBooleanTypeSpec, isBoolean } from "./type-mappers/boolean"; import { makeArrayTypeSpec, isArray } from "./type-mappers/array"; -import { - makeDictionaryTypeSpec, - isDictionary -} from "./type-mappers/dictionary"; import { makeAnyTypeSpec, isAnyTypeSpec } from "./type-mappers/any"; import { isSchema } from "./type-mappers/schema"; import { makeVoidTypeSpec, isVoidType } from "./type-mappers/void"; @@ -44,9 +40,6 @@ export function convertType( return makeBooleanTypeSpec(swaggerType); } else if (isArray(swaggerType)) { return makeArrayTypeSpec(swaggerType, swagger); - } else if (isDictionary(swaggerType)) { - // case where a it's a Dictionary - return makeDictionaryTypeSpec(swaggerType, swagger); } else if (isAnyTypeSpec(swaggerType)) { return makeAnyTypeSpec(swaggerType); } else if (isVoidType(swaggerType)) { diff --git a/src/typespec.ts b/src/typespec.ts index 716fcbc1..4d2ca0d0 100644 --- a/src/typespec.ts +++ b/src/typespec.ts @@ -10,12 +10,13 @@ export interface TypeSpec { readonly isObject: boolean; readonly isRef: boolean; readonly isAtomic: boolean; - readonly isDictionary: boolean; readonly isNullable: boolean; readonly isRequired: boolean; readonly tsType: TsType | string | undefined; readonly target: string | undefined; readonly properties: ReadonlyArray | undefined; + readonly hasAdditionalProperties: boolean; + readonly additionalPropertiesType: TypeSpec | undefined; } export function makeTypeSpecFromSwaggerType( @@ -26,7 +27,6 @@ export function makeTypeSpecFromSwaggerType( description: swaggerType.description, isEnum: false, isArray: false, - isDictionary: false, isObject: false, isRef: false, isNullable: !swaggerType.required, @@ -34,6 +34,8 @@ export function makeTypeSpecFromSwaggerType( tsType: undefined, isAtomic: false, target: undefined, - properties: undefined + properties: undefined, + hasAdditionalProperties: false, + additionalPropertiesType: undefined }; } diff --git a/templates/type.mustache b/templates/type.mustache index 2a2bf082..cb07b4f5 100644 --- a/templates/type.mustache +++ b/templates/type.mustache @@ -7,11 +7,10 @@ %><%#isAtomic%><%&tsType%><%/isAtomic%><%! %><%#isObject%>{ -<%#properties%> '<%name%>'<%^isRequired%>?<%/isRequired%>: <%>type%>;<%/properties%>}<%/isObject%><%! +<%#properties%> '<%name%>'<%^isRequired%>?<%/isRequired%>: <%>type%>;<%/properties%> }<%#hasAdditionalProperties%> & { + [key: string]: <%#additionalPropertiesType%><%>type%><%/additionalPropertiesType%>; }<%/hasAdditionalProperties%><%/isObject%><%! %><%#isArray%>Array<<%#elementType%><%>type%><%/elementType%>><%/isArray%><%! -%><%#isDictionary%>{[key: string]: <%#elementType%><%>type%><%/elementType%>}<%/isDictionary%><%! - %><%={{ }}=%> {{/tsType}}