mtennoe / swagger-typescript-codegen

A Swagger Codegenerator tailored for typescript.
Apache License 2.0
140 stars 52 forks source link

Types with additionalProperties are not generated correctly #91

Closed devpie closed 4 years ago

devpie commented 4 years ago

Hi, i just updated to version 2.0.0 and now my types with additionalProperties do not get generated correctly. Here is an example based on your tests for additionalProperties. Within the tests of swagger-typescript-codegen it seems to work, but in my project and my test project it doesn't. What am i doing wrong?

swagger.json:

{
  "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"
      }
    }
  }
}

output.ts:

// 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;
};

export type test_add_props_02 = {
    'some_prop' ? : string;
};

export type test_add_props_03 = ;

export type test_add_props_04 = ;

export type test_add_props_05 = ;

export type test_add_props_06 = ;

export type test_add_props_07 = {};

export type test_add_props_08 = {};

export type test_add_props_09 = ;

export type test_add_props_10 = ;

export type test_add_props_11 = ;

export type test_add_props_12 = ;

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 TestApi
 * @param {(string)} [domainOrOptions] - The project domain.
 */
export class TestApi {

    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 TestApi#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 TestApi;
mtennoe commented 4 years ago

Hi!

There was a recent PR that improved the way additionalProperties work, see #86. Are you using a custom mustache template, or the default ones?

@Kosta-Github - Were you able to compile a migration guide for this?

devpie commented 4 years ago

Hi @mtennoe,

yes i saw this PR. No we don't use custom mustache templates. We use the default ones. As mentioned i created a test project for testing this issue in order to eliminate any side effects. Here are my package.json and my generate.js which used the swagger.json above and produced the output.ts above.

generate.js:

const fs = require('fs');
const path = require('path');
const CodeGen = require('swagger-typescript-codegen').CodeGen;

function generateSwaggerClient(swaggerFile, outputFile, moduleName) {
  const swaggerJson = JSON.parse(fs.readFileSync(swaggerFile, 'UTF-8'));

  const tsSourceCode = CodeGen.getTypescriptCode({
    moduleName: moduleName,
    className: moduleName + 'Api',
    swagger: swaggerJson,
    beautify: true,
  });

  const outputDir = path.dirname(outputFile);
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, {recursive: true});
  }

  fs.writeFileSync(outputFile, tsSourceCode, 'UTF-8');
}

generateSwaggerClient('./api/test.json', './src-gen/rest-client/test.ts', 'Test');

package.json

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "generate": "node generate.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "swagger-typescript-codegen": "^2.0.0"
  }
}
mtennoe commented 4 years ago

Hm, interesting. When it's failing for you, are you using the same swagger.json as in the PR, i.e. this guy?

devpie commented 4 years ago

Yes, exactly :)

andy-viv commented 4 years ago

@devpie it looks like this works if you use 1.11.0. @mtennoe did anything else change between 1.11.0 and 2.0.0?

mtennoe commented 4 years ago

2.0.0 was just a republish of 1.11.0, so it shouldnt have, but something might have gone wrong in the process. I will take a look at that on monday

mtennoe commented 4 years ago

Seems like the issue was introduced in #89. To get back to a good state quickly I reverted that change and published it as 2.0.1, which seems to work in my own test environment. Does this solve it for you as well?

@bweggersen - Can you take a look to see what in the change caused the issue, and try to reintroduce it?

devpie commented 4 years ago

Yes it works again. You are the best. Thank you very much. :smile:

mtennoe commented 4 years ago

Great to hear! Closing the issue :)