diff --git a/README.md b/README.md index bef96642..af85abd3 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,12 @@ In addition to the common options listed below, `getCustomCode()` *requires* a ` lint: type: boolean description: whether or not to run jslint on the generated code + language: + type: string + description: currently only 'typescript' is supported, but could potentially be 'coffeescript', 'es2015'... + framework: + type: string + description: currently only 'angular' is supported, but could potentially be 'react', 'polymer'... esnext: type: boolean description: passed through to jslint diff --git a/lib/codegen.js b/lib/codegen.js index 65b356d1..9acb4448 100644 --- a/lib/codegen.js +++ b/lib/codegen.js @@ -41,6 +41,7 @@ var getViewForSwagger2 = function(opts, type){ className: opts.className, imports: opts.imports, domain: (swagger.schemes && swagger.schemes.length > 0 && swagger.host && swagger.basePath) ? swagger.schemes[0] + '://' + swagger.host + swagger.basePath.replace(/\/+$/g,'') : '', + basePath: swagger.basePath, methods: [], definitions: [] }; @@ -57,7 +58,7 @@ var getViewForSwagger2 = function(opts, type){ } }); _.forEach(api, function(op, m){ - if(authorizedMethods.indexOf(m.toUpperCase()) === -1) { + if(authorizedMethods.indexOf(m.toUpperCase()) === -1 || m.toUpperCase() == 'OPTIONS') { return; } var method = { @@ -74,7 +75,7 @@ var getViewForSwagger2 = function(opts, type){ headers: [] }; - if(op.produces) { + if (op.produces) { var headers = []; headers.value = []; @@ -85,17 +86,40 @@ var getViewForSwagger2 = function(opts, type){ } var consumes = op.consumes || swagger.consumes; - if(consumes) { + if (consumes) { method.headers.push({name: 'Content-Type', value: '\'' + consumes + '\'' }); } var params = []; - if(_.isArray(op.parameters)) { + if (_.isArray(op.parameters)) { params = op.parameters; } + + method.hasBody = false; + method.hasForm = false; + method.tsType = 'void'; + method.hasVoidReturn = true; + + if (op.responses) { + _.some(['200', '201'], function(code) { + if (op.responses[code]) { + method.tsType = ts.convertType(op.responses[code]); + if (method.tsType.isRef) { + method.tsType = method.tsType.target.charAt(0).toUpperCase() + method.tsType.target.substring(1); + } else { + method.tsType = method.tsType.tsType; + } + if (method.tsType != 'any') { + method.hasVoidReturn = false; + } + return true; + } + }); + } + params = params.concat(globalParams); _.forEach(params, function(parameter) { - //Ignore parameters which contain the x-exclude-from-bindings extension + // Ignore parameters which contain the x-exclude-from-bindings extension if(parameter['x-exclude-from-bindings'] === true) { return; } @@ -114,8 +138,10 @@ var getViewForSwagger2 = function(opts, type){ parameter.isSingleton = true; parameter.singleton = parameter.enum[0]; } + parameter.paramType = parameter.in; if(parameter.in === 'body'){ parameter.isBodyParameter = true; + method.hasBody = true; } else if(parameter.in === 'path'){ parameter.isPathParameter = true; } else if(parameter.in === 'query'){ @@ -128,6 +154,7 @@ var getViewForSwagger2 = function(opts, type){ parameter.isHeaderParameter = true; } else if(parameter.in === 'formData'){ parameter.isFormParameter = true; + method.hasForm = true; } parameter.tsType = ts.convertType(parameter); parameter.cardinality = parameter.required ? '' : '?'; @@ -137,11 +164,10 @@ var getViewForSwagger2 = function(opts, type){ }); }); - _.forEach(swagger.definitions, function(definition, name){ - data.definitions.push({ - name: name, - tsType: ts.convertType(definition) - }); + _.forEach(swagger.definitions, function(definition, name) { + var type = ts.convertType(definition); + type.name = name.charAt(0).toUpperCase() + name.substring(1); + data.definitions.push(type); }); return data; @@ -159,6 +185,8 @@ var getViewForSwagger1 = function(opts, type){ }; swagger.apis.forEach(function(api){ api.operations.forEach(function(op){ + if (op.method == 'OPTIONS') return; + var method = { path: api.path, className: opts.className, @@ -210,23 +238,65 @@ var getViewForSwagger1 = function(opts, type){ return data; }; +var fileExists = function(filePath) { + try { + return fs.statSync(filePath).isFile(); + } catch (err) { + return false; + } +}; + +/** + * @param path eg: __dirname + '/../templates/' + * @param language eg: 'typescript', 'coffeescript' + * @param framework eg: 'angular', 'angular2', 'react', 'polymer' + * @param suffix eg: 'class.mustache' + */ +var locateTemplate = function(path, language, framework, suffix) { + if (language && framework && fileExists(path + language + '-' + framework + '-' + suffix)) { + return path + language + '-' + framework + '-' + suffix; + } + if (language && fileExists(path + language + '-' + suffix)) { + return path + language + '-' + suffix; + } + if (framework && fileExists(path + framework + '-' + suffix)) { + return path + framework + '-' + suffix; + } + return path + suffix; +}; + +var readTemplate = function(path, language, framework, suffix) { + return fs.readFileSync(locateTemplate(path, language, framework, suffix), 'utf-8'); +}; + +/** + * @param {{ template?: {}, framework?: string, language?: string }} opts + * @param type - 'typescript', 'angular', 'node' + */ +var selectTemplates = function(opts, type) { + if (!_.isObject(opts.template)) { + opts.template = {}; + } + var templates = __dirname + '/../templates/'; + var language = opts.language || (type === 'typescript' ? type : undefined); + var framework = opts.framework || (type !== 'typescript' ? type : undefined); + + opts.template.class = opts.template.class || readTemplate(templates, language, framework, 'class.mustache'); + opts.template.method = opts.template.method || readTemplate(templates, language, framework, 'method.mustache'); + if(type === 'typescript') { + opts.template.type = readTemplate(templates, language, framework, 'type.mustache'); + } +}; + var getCode = function(opts, type) { // For Swagger Specification version 2.0 value of field 'swagger' must be a string '2.0' var data = opts.swagger.swagger === '2.0' ? getViewForSwagger2(opts, type) : getViewForSwagger1(opts, type); if (type === 'custom') { if (!_.isObject(opts.template) || !_.isString(opts.template.class) || !_.isString(opts.template.method)) { - throw new Error('Unprovided custom template. Please use the following template: template: { class: "...", method: "...", request: "..." }'); + throw new Error('Unprovided custom template. Please use the following template: template: { class: "...", method: "..." }'); } } else { - if (!_.isObject(opts.template)) { - opts.template = {}; - } - var templates = __dirname + '/../templates/'; - opts.template.class = opts.template.class || fs.readFileSync(templates + type + '-class.mustache', 'utf-8'); - opts.template.method = opts.template.method || fs.readFileSync(templates + (type === 'typescript' ? 'typescript-' : '') + 'method.mustache', 'utf-8'); - if(type === 'typescript') { - opts.template.type = opts.template.type || fs.readFileSync(templates + 'type.mustache', 'utf-8'); - } + selectTemplates(opts, type); } if (opts.mustache) { @@ -247,7 +317,7 @@ var getCode = function(opts, type) { lintOptions.esnext = true; } - if(type === 'typescript') { + if (type === 'typescript') { opts.lint = false; } diff --git a/lib/typescript.js b/lib/typescript.js index b33070c4..ae7a6897 100644 --- a/lib/typescript.js +++ b/lib/typescript.js @@ -14,14 +14,39 @@ var _ = require('lodash'); function convertType(swaggerType) { var typespec = {}; + if (swaggerType.hasOwnProperty('allOf')) { + typespec.isIntersection = true; + typespec.items = swaggerType.allOf.map(function(item) { + // join with '&' and avoid 'RangeError: Maximum call stack size exceeded' + return Object.assign(convertType(item), {hasPrefix: true, isIntersection: false}); + }); + typespec.items[0].hasPrefix = false; + typespec.tsType = 'intersection'; - if (swaggerType.hasOwnProperty('schema')) { + } else if (swaggerType.hasOwnProperty('schema')) { return convertType(swaggerType.schema); } else if (_.isString(swaggerType.$ref)) { typespec.tsType = 'ref'; typespec.target = swaggerType.$ref.substring(swaggerType.$ref.lastIndexOf('/') + 1); + } else if (swaggerType.type === 'array') { + if (swaggerType.in === 'query' && swaggerType.collectionFormat !== 'multi') { + // arrays in query parameters are merged by csv, ssv, tsv or pipes + typespec.tsType = 'string'; + if (swaggerType.hasOwnProperty('enum')) { + // doesn't affect the compiler, but useful for documentation + typespec.tsType += ' | ' + swaggerType.enum.map(function (str) { + return typeof str == 'string' ? '\'' + str + '\'' : JSON.stringify(str); + }).join(' | '); + typespec.isAtomic = true; + } + } else { + typespec.tsType = 'array'; + } + typespec.elementType = convertType(swaggerType.items); } else if (swaggerType.hasOwnProperty('enum')) { - typespec.tsType = swaggerType.enum.map(function(str) { return JSON.stringify(str); }).join(' | '); + typespec.tsType = swaggerType.enum.map(function(str) { + return typeof str == 'string' ? '\'' + str + '\'' : JSON.stringify(str); + }).join(' | '); typespec.isAtomic = true; } else if (swaggerType.type === 'string') { typespec.tsType = 'string'; @@ -29,20 +54,18 @@ function convertType(swaggerType) { typespec.tsType = 'number'; } else if (swaggerType.type === 'boolean') { typespec.tsType = 'boolean'; - } else if (swaggerType.type === 'array') { - typespec.tsType = 'array'; - typespec.elementType = convertType(swaggerType.items); } else if (swaggerType.type === 'object') { typespec.tsType = 'object'; typespec.properties = []; - _.forEach(swaggerType.properties, function (propertyType, propertyName) { var property = convertType(propertyType); property.name = propertyName; + property.isOptional = !(swaggerType.required && _.includes(swaggerType.required, propertyName)); typespec.properties.push(property); }); } else { // type unknown or unsupported... just map to 'any'... + // TODO: probably void typespec.tsType = 'any'; } @@ -51,9 +74,10 @@ function convertType(swaggerType) { typespec.isObject = typespec.tsType === 'object'; typespec.isArray = typespec.tsType === 'array'; typespec.isAtomic = typespec.isAtomic || _.includes(['string', 'number', 'boolean', 'any'], typespec.tsType); + typespec.isOptional = !swaggerType.required; + typespec.description = swaggerType.description; return typespec; - } module.exports.convertType = convertType; diff --git a/package.json b/package.json index 8f4c0852..5e16dcdf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "swagger-js-codegen", "main": "./lib/codegen.js", - "version": "1.7.12", + "version": "1.7.15-c", "description": "A Swagger codegen for JavaScript", "scripts": { "test": "grunt", diff --git a/templates/type.mustache b/templates/type.mustache index d8f2518d..bd9b87fa 100644 --- a/templates/type.mustache +++ b/templates/type.mustache @@ -1,11 +1,12 @@ {{#tsType}} {{! must use different delimiters to avoid ambiguities when delimiters directly follow a literal brace {. }} {{=<% %>=}} -<%#isRef%><%target%><%/isRef%><%! +<%#isIntersection%><%#items%><%#hasPrefix%>&<%/hasPrefix%><%>type%><%/items%><%/isIntersection%><%! +%><%#isRef%><%target%><%/isRef%><%! %><%#isAtomic%><%&tsType%><%/isAtomic%><%! -%><%#isObject%>{<%#properties%> -'<%name%>': <%>type%><%/properties%> +%><%#isObject%>{<%#properties%><%#description%>/** <%description%> */<%/description%> +<%name%><%#isOptional%>?<%/isOptional%>: <%>type%>;<%/properties%> }<%/isObject%><%! -%><%#isArray%>Array<<%#elementType%><%>type%><%/elementType%>>|<%#elementType%><%>type%><%/elementType%><%/isArray%> -<%={{ }}=%> -{{/tsType}} \ No newline at end of file +%><%#isArray%>Array<<%#elementType%><%>type%><%/elementType%>><%/isArray%><%! +%><%#isBoolean%>boolean<%/isBoolean%><%! +%><%={{ }}=%>{{/tsType}} diff --git a/templates/typescript-angular-class.mustache b/templates/typescript-angular-class.mustache new file mode 100644 index 00000000..23891bde --- /dev/null +++ b/templates/typescript-angular-class.mustache @@ -0,0 +1,45 @@ +/* tslint:disable */ +import * as angular from 'angular'; + +export module {{moduleName}} { + +{{#definitions}} + {{#description}}/** {{description}} */{{/description}} + export type {{&name}} = {{#tsType}}{{> type}}{{/tsType}}; + +{{/definitions}} + +/** +* {{&description}} +* @class {{&className}} +* @param {ng.IHttpService} $http +* @param {ng.IQService} $q +* @param {string} domain - The project domain. +* provide using .constant('domain', '//example.com') or .factory('domain', function(){return '//example.com'}) +*/ +export class {{&className}} { + static $inject = ['$http', '$q', 'domain']; + + constructor(private $http: ng.IHttpService, private $q: ng.IQService, private domain: string) {} + +{{#methods}} + {{> method}} + +{{/methods}} + + private static transformRequest(obj: any): string { + var str = []; + for(var p in obj) { + var val = obj[p]; + if(angular.isArray(val)) { + val.forEach(function(val){ + str.push(encodeURIComponent(p) + '=' + encodeURIComponent(val)); + }); + } else { + str.push(encodeURIComponent(p) + '=' + encodeURIComponent(val)); + } + } + return str.join('&'); + } +} +} diff --git a/templates/typescript-angular-method.mustache b/templates/typescript-angular-method.mustache new file mode 100644 index 00000000..6fd01f68 --- /dev/null +++ b/templates/typescript-angular-method.mustache @@ -0,0 +1,151 @@ +/** +* {{&summary}} +* @method +{{#externalDocs}} +* @see {@link {{&url}}|{{#description}}{{&description}}{{/description}}{{^description}}External docs{{/description}}} +{{/externalDocs}} +* @name {{&className}}#{{&methodName}} +{{#parameters}} + {{^isSingleton}} * @param {{=<% %>=}}{<%&type%>}<%={{ }}=%> {{&camelCaseName}} - {{&description}}{{/isSingleton}} +{{/parameters}} +*/ +{{&methodName}}(parameters: { +{{#parameters}}{{^isSingleton}}{{&camelCaseName}}{{&cardinality}}: {{> type}}, +{{/isSingleton}}{{/parameters}} + $queryParameters?: {}, + }, + opts: { + $timeout?: number; + $refresh?: boolean; + $cache?: { + get:(key: string) => string|Object + put:(key: string, value: string|Object, options?: {}) => void; + }; + $cacheItemOpts?: {}; + } = {}): ng.IPromise<{{&tsType}}|any> { + let domain = this.domain; + let path = '{{&basePath}}{{&path}}'; + {{#hasBody}} + let body; + {{/hasBody}} + let queryParameters = {}; + let headers = {}; + {{#hasForm}} + let form = {}; + {{/hasForm}} + let deferred = this.$q.defer(); + +{{#headers}} + headers['{{&name}}'] = [{{&value}}]; +{{/headers}} + +{{#parameters}} + {{#isQueryParameter}} + {{#isSingleton}} + queryParameters['{{&name}}'] = '{{&singleton}}'; + {{/isSingleton}} + {{^isSingleton}} + {{#isPatternType}} + Object.keys(parameters).forEach(function(parameterName) { + if(new RegExp('{{&pattern}}').test(parameterName)){ + queryParameters[parameterName] = parameters[parameterName]; + } + }); + {{/isPatternType}} + {{^isPatternType}} + if(parameters['{{&camelCaseName}}'] !== undefined){ + queryParameters['{{&name}}'] = parameters['{{&camelCaseName}}']; + } + {{/isPatternType}} + {{/isSingleton}} + {{/isQueryParameter}} + + {{#isPathParameter}} + path = path.replace('{{=<% %>=}}{<%&name%>}<%={{ }}=%>', parameters['{{&camelCaseName}}']); + {{/isPathParameter}} + + {{#isHeaderParameter}} + {{#isSingleton}} + headers['{{&name}}'] = '{{&singleton}}'; + {{/isSingleton}} + {{^isSingleton}} + if(parameters['{{&camelCaseName}}'] !== undefined){ + headers['{{&name}}'] = parameters['{{&camelCaseName}}']; + } + {{/isSingleton}} + {{/isHeaderParameter}} + + {{#isBodyParameter}} + if(parameters['{{&camelCaseName}}'] !== undefined){ + body = parameters['{{&camelCaseName}}']; + } + {{/isBodyParameter}} + + {{#isFormParameter}} + {{#isSingleton}} + form['{{&name}}'] = '{{&singleton}}'; + {{/isSingleton}} + {{^isSingleton}} + if(parameters['{{&camelCaseName}}'] !== undefined){ + form['{{&name}}'] = parameters['{{&camelCaseName}}']; + } + {{/isSingleton}} + {{/isFormParameter}} +{{/parameters}} + +if(parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName){ + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); +} + +var url = domain + path; +{{#isGET}} +var cached = opts.$cache && opts.$cache.get(url); +if(cached !== undefined && opts.$refresh !== true) { + deferred.resolve(cached); + return deferred.promise; +} +{{/isGET}} +var options = { + timeout: opts.$timeout, + method: '{{method}}', + url: url, + params: queryParameters, +{{#hasBody}} + data: body, +{{/hasBody}} +{{#hasForm}} + data: form, +{{/hasForm}} + headers: headers +}; +{{! I don't think this is actually needed #hasBody} } +if(typeof(body) === 'object' && !(body.constructor.name === 'Buffer')) { + options.headers['Content-Type'] = 'application/json'; +{ {/hasBody}} +{{#hasForm}} +options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; +options.transformRequest = {{&className}}.transformRequest; +{{/hasForm}} + +this.$http(options).then(function(response: ng.IHttpPromiseCallbackArg<{{&tsType}}|any>){ + if(response.status >= 400) { + deferred.reject(response); + } else { + {{#hasVoidReturn}} + deferred.resolve(); + {{/hasVoidReturn}} + {{^hasVoidReturn}} + deferred.resolve(response.data); + if(opts.$cache !== undefined) { + opts.$cache.put(url, response.data, opts.$cacheItemOpts ? opts.$cacheItemOpts : {}); + } + {{/hasVoidReturn}} + } +}, deferred.reject); + +return deferred.promise; + +} diff --git a/templates/typescript-class.mustache b/templates/typescript-class.mustache index d4f81ddf..bdf1988a 100644 --- a/templates/typescript-class.mustache +++ b/templates/typescript-class.mustache @@ -1,7 +1,6 @@ {{#imports}} /// {{/imports}} - import * as request from "superagent"; import {SuperAgentStatic} from "superagent"; @@ -10,13 +9,20 @@ type CallbackHandler = (err: any, res?: request.Response) => void; type {{&name}} = {{#tsType}}{{> type}}{{/tsType}}; {{/definitions}} +module {{moduleName}} { + +{{#definitions}} +{{#description}}/** {{description}} */{{/description}} +interface {{name}} {{> type}} + +{{/definitions}} + /** * {{&description}} * @class {{&className}} - * @param {(string)} [domainOrOptions] - The project domain. + * @param {string} domain - The project domain. */ export default class {{&className}} { - private domain: string = "{{&domain}}"; private errorHandlers: CallbackHandler[] = []; diff --git a/templates/typescript-method.mustache b/templates/typescript-method.mustache index 45bf93d6..47ceb2b7 100644 --- a/templates/typescript-method.mustache +++ b/templates/typescript-method.mustache @@ -6,7 +6,7 @@ $domain?: string }): string { let queryParameters: any = {}; const domain = parameters.$domain ? parameters.$domain : this.domain; - let path = '{{&path}}'; + let path = '{{&basePath}}{{&path}}'; {{#parameters}} {{#isQueryParameter}} {{#isSingleton}} @@ -71,18 +71,22 @@ $domain?: string const domain = parameters.$domain ? parameters.$domain : this.domain; const errorHandlers = this.errorHandlers; const request = this.request; - let path = '{{&path}}'; + const path = '{{&path}}'; + {{#hasBody}} let body: any; + {{/hasBody}} let queryParameters: any = {}; let headers: any = {}; + {{#hasForm}} let form: any = {}; + {{/hasForm}} + return new Promise(function(resolve, reject) { {{#headers}} headers['{{&name}}'] = {{&value}}; {{/headers}} {{#parameters}} - {{#isQueryParameter}} {{#isSingleton}} queryParameters['{{&name}}'] = '{{&singleton}}'; @@ -134,22 +138,14 @@ $domain?: string } {{/isSingleton}} {{/isFormParameter}} - - {{#required}} - if(parameters['{{&camelCaseName}}'] === undefined){ - reject(new Error('Missing required {{¶mType}} parameter: {{&camelCaseName}}')); - return; - } - {{/required}} - {{/parameters}} -if(parameters.$queryParameters) { - Object.keys(parameters.$queryParameters).forEach(function(parameterName){ - var parameter = parameters.$queryParameters[parameterName]; - queryParameters[parameterName] = parameter; - }); -} + if(parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName){ + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } {{^isBodyParameter}} {{#isPOST}} @@ -161,4 +157,4 @@ if(parameters.$queryParameters) { request('{{method}}', domain + path, body, headers, queryParameters, form, reject, resolve, errorHandlers); }); -}; +} diff --git a/tests/utilities.js b/tests/utilities.js new file mode 100644 index 00000000..23b57c09 --- /dev/null +++ b/tests/utilities.js @@ -0,0 +1,65 @@ +'use strict'; + +var assert = require('assert'); +var rewire = require('rewire'); +var vows = require('vows'); + +var CodeGen = rewire('../lib/codegen.js'); + +vows.describe('Test Utilities').addBatch({ + 'selectTemplates': { + topic: function(){ + var locateTemplate = CodeGen.__get__('locateTemplate'); + CodeGen.__set__('readTemplate', function(path, language, framework, suffix) { + return locateTemplate(path, language, framework, suffix); + }); + return CodeGen.__get__('selectTemplates'); + }, + 'should be backward-compatible': { + topic: function(selectTemplates) { + return selectTemplates; //_.curry(selectTemplates)({}); + }, + 'with angular': function(selectTemplates) { + var opts = {}; + var dirname = __dirname.replace(/tests$/, 'lib') + '/../templates/'; + selectTemplates(opts, 'angular'); + assert.equal(opts.template.class, dirname + 'angular-class.mustache'); + assert.equal(opts.template.method, dirname + 'method.mustache'); + assert.equal(opts.template.type, undefined); + }, + 'with node': function(selectTemplates) { + var opts = {}; + var dirname = __dirname.replace(/tests$/, 'lib') + '/../templates/'; + selectTemplates(opts, 'node'); + assert.equal(opts.template.class, dirname + 'node-class.mustache'); + assert.equal(opts.template.method, dirname + 'method.mustache'); + assert.equal(opts.template.type, undefined); + }, + 'with typescript': function(selectTemplates) { + var opts = {}; + var dirname = __dirname.replace(/tests$/, 'lib') + '/../templates/'; + selectTemplates(opts, 'typescript'); + assert.equal(opts.template.class, dirname + 'typescript-class.mustache'); + assert.equal(opts.template.method, dirname + 'typescript-method.mustache'); + assert.equal(opts.template.type, dirname + 'type.mustache'); + } + } + }, + 'locateTemplate': { + topic: function(){ + return CodeGen.__get__('locateTemplate'); + }, + 'should find templates for language and framework': function(locateTemplate) { + assert.equal(locateTemplate(__dirname + '/../templates/', 'typescript', 'angular', 'class.mustache'), + __dirname + '/../templates/typescript-angular-class.mustache'); + }, + 'should find templates for language': function(locateTemplate) { + assert.equal(locateTemplate(__dirname + '/../templates/', 'typescript', undefined, 'class.mustache'), + __dirname + '/../templates/typescript-class.mustache'); + }, + 'should find templates for framework': function(locateTemplate) { + assert.equal(locateTemplate(__dirname + '/../templates/', undefined, 'angular', 'class.mustache'), + __dirname + '/../templates/angular-class.mustache'); + } + } +}).export(module);