From b95027194001818cb6ab4a2f9eee1c9d49dbb130 Mon Sep 17 00:00:00 2001 From: Andrew Z Allen Date: Wed, 27 Jan 2016 19:08:09 -0700 Subject: [PATCH] Add a Javascript (Closure) Angular generator. --- ...JavascriptClosureAngularClientCodegen.java | 206 ++++++++++++++++++ .../Javascript-Closure-Angular/api.mustache | 143 ++++++++++++ .../Javascript-Closure-Angular/model.mustache | 40 ++++ .../services/io.swagger.codegen.CodegenConfig | 1 + ...iptClosureAnularClientOptionsProvider.java | 30 +++ 5 files changed, 420 insertions(+) create mode 100644 modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/JavascriptClosureAngularClientCodegen.java create mode 100644 modules/swagger-codegen/src/main/resources/Javascript-Closure-Angular/api.mustache create mode 100644 modules/swagger-codegen/src/main/resources/Javascript-Closure-Angular/model.mustache create mode 100644 modules/swagger-codegen/src/test/java/io/swagger/codegen/options/JavascriptClosureAnularClientOptionsProvider.java diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/JavascriptClosureAngularClientCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/JavascriptClosureAngularClientCodegen.java new file mode 100644 index 00000000000..e74dff5f861 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/JavascriptClosureAngularClientCodegen.java @@ -0,0 +1,206 @@ +package io.swagger.codegen.languages; + +import io.swagger.codegen.CodegenModel; +import io.swagger.codegen.*; +import io.swagger.models.properties.*; + +import java.util.TreeSet; +import java.util.*; +import java.io.File; + +public class JavascriptClosureAngularClientCodegen extends DefaultCodegen implements CodegenConfig { + public JavascriptClosureAngularClientCodegen() { + super(); + + supportsInheritance = false; + reservedWords = new HashSet(Arrays.asList("abstract", + "continue", "for", "new", "switch", "assert", "default", "if", + "package", "synchronized", "do", "goto", "private", + "this", "break", "double", "implements", "protected", "throw", + "byte", "else", "import", "public", "throws", "case", "enum", + "instanceof", "return", "transient", "catch", "extends", "int", + "short", "try", "char", "final", "interface", "static", "void", + "class", "finally", "const", "super", "while")); + + languageSpecificPrimitives = new HashSet(Arrays.asList( + "string", + "boolean", + "number", + "Object", + "Blob", + "Date")); + instantiationTypes.put("array", "Array"); + + typeMapping = new HashMap(); + typeMapping.put("Array", "Array"); + typeMapping.put("array", "Array"); + typeMapping.put("List", "Array"); + typeMapping.put("boolean", "boolean"); + typeMapping.put("string", "string"); + typeMapping.put("int", "number"); + typeMapping.put("float", "number"); + typeMapping.put("number", "number"); + typeMapping.put("long", "number"); + typeMapping.put("short", "number"); + typeMapping.put("char", "string"); + typeMapping.put("double", "number"); + typeMapping.put("object", "Object"); + typeMapping.put("Object", "Object"); + typeMapping.put("File", "Blob"); + typeMapping.put("file", "Blob"); + typeMapping.put("integer", "number"); + typeMapping.put("Map", "Object"); + typeMapping.put("map", "Object"); + typeMapping.put("DateTime", "Date"); + + outputFolder = "generated-code/javascript-closure-angular"; + modelTemplateFiles.put("model.mustache", ".js"); + apiTemplateFiles.put("api.mustache", ".js"); + embeddedTemplateDir = templateDir = "Javascript-Closure-Angular"; + apiPackage = "API.Client"; + modelPackage = "API.Client"; + } + + @Override + public String getName() { + return "javascript-closure-angular"; + } + + @Override + public String getHelp() { + return "Generates a Javascript AngularJS client library annotated with Google Closure Compiler annotations" + + "(https://developers.google.com/closure/compiler/docs/js-for-compiler?hl=en)"; + } + + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String escapeReservedWord(String name) { + return "_" + name; + } + + @Override + public String apiFileFolder() { + return outputFolder + "/" + apiPackage().replace('.', File.separatorChar); + } + + public String modelFileFolder() { + return outputFolder + "/" + modelPackage().replace('.', File.separatorChar); + } + + @Override + public String toVarName(String name) { + // replace - with _ e.g. created-at => created_at + name = name.replaceAll("-", "_"); + + // if it's all uppper case, do nothing + if (name.matches("^[A-Z_]*$")) + return name; + + // camelize the variable name + // pet_id => PetId + name = camelize(name, true); + + // for reserved word or word starting with number, append _ + if (reservedWords.contains(name) || name.matches("^\\d.*")) + name = escapeReservedWord(name); + + return name; + } + + @Override + public String toParamName(String name) { + // should be the same as variable name + return toVarName(name); + } + + @Override + public String toModelName(String name) { + // model name cannot use reserved keyword, e.g. return + if (reservedWords.contains(name)) + throw new RuntimeException(name + + " (reserved word) cannot be used as a model name"); + + // camelize the model name + // phone_number => PhoneNumber + return camelize(name); + } + + @Override + public String toModelFilename(String name) { + // should be the same as the model name + return toModelName(name); + } + + @Override + public String getTypeDeclaration(Property p) { + if (p instanceof ArrayProperty) { + ArrayProperty ap = (ArrayProperty) p; + Property inner = ap.getItems(); + return getSwaggerType(p) + ""; + } else if (p instanceof MapProperty) { + MapProperty mp = (MapProperty) p; + Property inner = mp.getAdditionalProperties(); + return "Object"; + } else if (p instanceof FileProperty) { + return "Object"; + } + String type = super.getTypeDeclaration(p); + if (type.equals("boolean") || + type.equals("Date") || + type.equals("number") || + type.equals("string")) { + return type; + } + return apiPackage + "." + type; + } + + @Override + public String getSwaggerType(Property p) { + String swaggerType = super.getSwaggerType(p); + String type = null; + if (typeMapping.containsKey(swaggerType)) { + type = typeMapping.get(swaggerType); + if (languageSpecificPrimitives.contains(type)) + return type; + } else + type = swaggerType; + return type; + } + + @Override + public Map postProcessModels(Map objs) { + + List models = (List) objs.get("models"); + for (Object _mo : models) { + Map mo = (Map) _mo; + CodegenModel cm = (CodegenModel) mo.get("model"); + cm.imports = new TreeSet(cm.imports); + for (CodegenProperty var : cm.vars) { + // handle default value for enum, e.g. available => StatusEnum.available + if (var.isEnum && var.defaultValue != null && !"null".equals(var.defaultValue)) { + var.defaultValue = var.datatypeWithEnum + "." + var.defaultValue; + } + } + } + return objs; + } + + @Override + public Map postProcessOperations(Map objs) { + if (objs.get("imports") instanceof List) { + List> imports = (ArrayList>)objs.get("imports"); + Collections.sort(imports, new Comparator>() { + public int compare(Map o1, Map o2) { + return o1.get("import").compareTo(o2.get("import")); + } + }); + objs.put("imports", imports); + } + return objs; + } + +} diff --git a/modules/swagger-codegen/src/main/resources/Javascript-Closure-Angular/api.mustache b/modules/swagger-codegen/src/main/resources/Javascript-Closure-Angular/api.mustache new file mode 100644 index 00000000000..e6934799f52 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/Javascript-Closure-Angular/api.mustache @@ -0,0 +1,143 @@ +/** + * @fileoverview AUTOMATICALLY GENERATED service for {{package}}.{{classname}}. + * Do not edit this file by hand or your changes will be lost next time it is + * generated.{{#appDescription}} + * + * {{ appDescription }}{{/appDescription}}{{#version}} + * Version: {{version}}{{/version}}{{#appContact}} + * Contact: {{appContact}}{{/appContact}} + * Generated at: {{generatedDate}} + * Generated by: {{generatorClass}} + */{{#licenseInfo}} +/** + * @license {{licenseInfo}}{{#licenseUrl}} + * {{licenseUrl}}{{/licenseUrl}} + */ +{{/licenseInfo}} + +goog.provide('{{package}}.{{classname}}'); + +{{#imports}} +goog.require('{{import}}'); +{{/imports}} +{{#operations}} + +/** +{{#description}} + * {{&description}} +{{/description}} + * @constructor + * @param {!angular.$http} $http + * @param {!angular.$injector} $injector + * @struct + */ +{{package}}.{{classname}} = function($http, $injector) { + /** @private {!string} */ + this.basePath_ = $injector.has('{{classname}}BasePath') ? + /** @type {!string} */ ($injector.get('{{classname}}BasePath')) : + '{{basePath}}'; + + /** @private {!Object} */ + this.defaultHeaders_ = $injector.has('{{classname}}DefaultHeaders') ? + /** @type {!Object} */ ( + $injector.get('{{classname}}DefaultHeaders')) : + {}; + + /** @private {!angular.$http} */ + this.http_ = $http; +} +{{package}}.{{classname}}.$inject = ['$http', '$injector']; +{{#operation}} + +/** + * {{summary}} + * {{notes}}{{#allParams}} + * @param {!{{{dataType}}}{{^required}}={{/required}}} {{^required}}opt_{{/required}}{{paramName}} {{description}}{{/allParams}} + * @param {!angular.$http.Config=} opt_extraHttpRequestParams Extra HTTP parameters to send. + * @return {!angular.$q.Promise{{#returnType}}{{/returnType}}} + */ +{{package}}.{{classname}}.prototype.{{nickname}} = function({{#allParams}}{{^required}}opt_{{/required}}{{paramName}}, {{/allParams}}opt_extraHttpRequestParams) { + /** @const {!string} */ + var path = this.basePath_ + '{{path}}'{{#pathParams}} + .replace('{' + '{{baseName}}' + '}', String({{^required}}opt_{{/required}}{{paramName}})){{/pathParams}}; +{{#required}} + + // verify required parameter '{{paramName}}' is set + if (!{{paramName}}) { + throw new Error('Missing required parameter {{paramName}} when calling {{nickname}}'); + } +{{/required}} + + /** @type {!Object} */ + var queryParameters = {}; +{{#queryParams}} + if ({{^required}}opt_{{/required}}{{paramName}} !== undefined) { + queryParameters['{{baseName}}'] = String({{^required}}opt_{{/required}}{{paramName}}); + } +{{/queryParams}} + + /** @type {!Object} */ + var headerParams = angular.copy(this.defaultHeaders_); +{{#headerParams}} + if ({{^required}}opt_{{/required}}{{paramName}} !== undefined) { + headerParams['{{baseName}}'] = {{^required}}opt_{{/required}}{{paramName}}; + } +{{/headerParams}} +{{#hasFormParams}} + + /** @type {!FormData} */ + var formParams = new FormData(); +{{/hasFormParams}} +{{#formParams}} + if ({{^required}}opt_{{/required}}{{paramName}} !== undefined) { + var {{paramName}}_ = /** @type {?} */ ({{^required}}opt_{{/required}}{{paramName}}); + if ({{paramName}}_ instanceof Blob) { + formParams.append('{{baseName}}', {{paramName}}_); + } else if (typeof {{paramName}}_ === 'string') { + formParams.append('{{baseName}}', {{paramName}}_); + } else { + throw new Error('Forms parameter {{^required}}opt_{{/required}}{{paramName}} is required to be a string or a Blob (https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob)'); + } + } +{{/formParams}} +{{#allParams}} +{{/allParams}} + + /** @type {!angular.$http.Config} */ + var httpRequestConfig = /** @type {!angular.$http.Config} */ ({ + url: path, + json: {{#hasFormParams}}false{{/hasFormParams}}{{^hasFormParams}}true{{/hasFormParams}},{{#bodyParam}} + data: {{paramName}},{{/bodyParam}}{{#hasFormParams}} + data: formParams,{{/hasFormParams}} + params: queryParameters, + headers: headerParams + }); + + if (opt_extraHttpRequestParams) { + // If an opt_extraHttpRequestParams object is passed in, override values + // set the generated config with the passed in values. + httpRequestConfig = angular.merge(httpRequestConfig, opt_extraHttpRequestParams); + } + + // This whole block is to work around a limitation in closure compiler. It + // would be better to call the $http service directly as a function, but that + // isn't permitted since it has methods attached to it. Manually confirmed to + // compile down to just a single method even with only SIMPLE optimization on. + // https://github.com/google/closure-compiler/blob/90769b826df65eabfb0211517b0d6d85c0c1c60b/contrib/externs/angular-1.4.js#L1393 + switch ('{{httpMethod}}') { + case 'GET': + return this.http_.get(path, httpRequestConfig); + case 'HEAD': + return this.http_.head(path, httpRequestConfig); + case 'POST': + return this.http_.post(path, {}, httpRequestConfig); + case 'PUT': + return this.http_.put(path, {}, httpRequestConfig); + case 'DELETE': + return this.http_.delete(path, httpRequestConfig); + case 'PATCH': + return this.http_.patch(path, {}, httpRequestConfig); + } +} +{{/operation}} +{{/operations}} diff --git a/modules/swagger-codegen/src/main/resources/Javascript-Closure-Angular/model.mustache b/modules/swagger-codegen/src/main/resources/Javascript-Closure-Angular/model.mustache new file mode 100644 index 00000000000..81ec7591c87 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/Javascript-Closure-Angular/model.mustache @@ -0,0 +1,40 @@ +{{#models}} +{{#model}} +goog.provide('{{package}}.{{name}}'); +{{/model}} +{{/models}} + +{{#models}} +{{#model}} +/** +{{#description}} + * {{{description}}} +{{/description}} + * @record + */ +{{package}}.{{classname}} = function() {} +{{#vars}} + +/** +{{#description}} + * {{{description}}} +{{/description}} +{{! Explicitly force types to be non-nullable using !. This is redundant but valid }} + * @type {!{{{datatype}}}} + * @export + */ +{{package}}.{{classname}}.prototype.{{name}}; +{{/vars}} + +{{#hasEnums}} +{{#vars}} +{{#isEnum}} +/** @enum {string} */ +{{package}}.{{classname}}.{{datatypeWithEnum}} = { {{#allowableValues}}{{#values}} + {{.}}: '{{.}}',{{/values}}{{/allowableValues}} +} +{{/isEnum}} +{{/vars}} +{{/hasEnums}} +{{/model}} +{{/models}} diff --git a/modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig b/modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig index 18015a9328b..d023052aa55 100644 --- a/modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig +++ b/modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig @@ -10,6 +10,7 @@ io.swagger.codegen.languages.JavaJerseyServerCodegen io.swagger.codegen.languages.JavaCXFServerCodegen io.swagger.codegen.languages.JavaInflectorServerCodegen io.swagger.codegen.languages.JavascriptClientCodegen +io.swagger.codegen.languages.JavascriptClosureAngularClientCodegen io.swagger.codegen.languages.JMeterCodegen io.swagger.codegen.languages.NodeJSServerCodegen io.swagger.codegen.languages.ObjcClientCodegen diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/options/JavascriptClosureAnularClientOptionsProvider.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/options/JavascriptClosureAnularClientOptionsProvider.java new file mode 100644 index 00000000000..3bc5a3df22d --- /dev/null +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/options/JavascriptClosureAnularClientOptionsProvider.java @@ -0,0 +1,30 @@ +package io.swagger.codegen.options; + +import io.swagger.codegen.CodegenConstants; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public class JavascriptClosureAnularClientOptionsProvider implements OptionsProvider { + public static final String SORT_PARAMS_VALUE = "false"; + public static final String ENSURE_UNIQUE_PARAMS_VALUE = "true"; + + @Override + public String getLanguage() { + return "javascript-closure-angular"; + } + + @Override + public Map createOptions() { + ImmutableMap.Builder builder = new ImmutableMap.Builder(); + return builder.put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, SORT_PARAMS_VALUE) + .put(CodegenConstants.ENSURE_UNIQUE_PARAMS, ENSURE_UNIQUE_PARAMS_VALUE) + .build(); + } + + @Override + public boolean isServer() { + return false; + } +}