diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenOperation.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenOperation.java index 7183c4d3456..c5b9231341e 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenOperation.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/CodegenOperation.java @@ -10,10 +10,10 @@ import java.util.Set; public class CodegenOperation { public final List responseHeaders = new ArrayList(); - public Boolean hasAuthMethods, hasConsumes, hasProduces, hasParams, returnTypeIsPrimitive, - returnSimpleType, subresourceOperation, isMapContainer, isListContainer, - hasMore = Boolean.TRUE, isMultipart, isResponseBinary = Boolean.FALSE, - hasReference = Boolean.FALSE; + public Boolean hasAuthMethods, hasConsumes, hasProduces, hasParams, hasOptionalParams, + returnTypeIsPrimitive, returnSimpleType, subresourceOperation, isMapContainer, + isListContainer, isMultipart, hasMore = Boolean.TRUE, + isResponseBinary = Boolean.FALSE, hasReference = Boolean.FALSE; public String path, operationId, returnType, httpMethod, returnBaseType, returnContainer, summary, notes, baseName, defaultResponse; public List> consumes, produces; diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultCodegen.java index 3c8758fc15b..99ceca6796c 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/DefaultCodegen.java @@ -1299,6 +1299,9 @@ public class DefaultCodegen { p.isFormParam = new Boolean(true); formParams.add(p.copy()); } + if (p.required == null || !p.required) { + op.hasOptionalParams = true; + } } } for (String i : imports) { diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ClojureClientCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ClojureClientCodegen.java new file mode 100644 index 00000000000..1c7b6ab3f2d --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ClojureClientCodegen.java @@ -0,0 +1,157 @@ +package io.swagger.codegen.languages; + +import io.swagger.codegen.CodegenConfig; +import io.swagger.codegen.CodegenConstants; +import io.swagger.codegen.CodegenOperation; +import io.swagger.codegen.CodegenType; +import io.swagger.codegen.DefaultCodegen; +import io.swagger.codegen.SupportingFile; +import io.swagger.models.Info; +import io.swagger.models.Swagger; +import org.apache.commons.lang.StringUtils; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.List; + +public class ClojureClientCodegen extends DefaultCodegen implements CodegenConfig { + private static final String PROJECT_NAME = "projectName"; + private static final String PROJECT_DESCRIPTION = "projectDescription"; + private static final String PROJECT_VERSION = "projectVersion"; + private static final String BASE_NAMESPACE = "baseNamespace"; + + protected String projectName = null; + protected String projectDescription = null; + protected String projectVersion = null; + protected String sourceFolder = "src"; + + public ClojureClientCodegen() { + super(); + outputFolder = "generated-code" + File.separator + "clojure"; + apiTemplateFiles.put("api.mustache", ".clj"); + embeddedTemplateDir = templateDir = "clojure"; + } + + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String getName() { + return "clojure"; + } + + @Override + public String getHelp() { + return "Generates a Clojure client library."; + } + + @Override + public void preprocessSwagger(Swagger swagger) { + super.preprocessSwagger(swagger); + + if (additionalProperties.containsKey(PROJECT_NAME)) { + projectName = ((String) additionalProperties.get(PROJECT_NAME)); + } + if (additionalProperties.containsKey(PROJECT_DESCRIPTION)) { + projectDescription = ((String) additionalProperties.get(PROJECT_DESCRIPTION)); + } + if (additionalProperties.containsKey(PROJECT_VERSION)) { + projectVersion = ((String) additionalProperties.get(PROJECT_VERSION)); + } + + if (swagger.getInfo() != null) { + Info info = swagger.getInfo(); + if (projectName == null && info.getTitle() != null) { + // when projectName is not specified, generate it from info.title + projectName = dashize(info.getTitle()); + } + if (projectVersion == null && info.getVersion() != null) { + // when projectVersion is not specified, use info.version + projectVersion = info.getVersion(); + } + if (projectDescription == null && info.getDescription() != null) { + // when projectDescription is not specified, use info.description + projectDescription = info.getDescription(); + } + } + + // default values + if (projectName == null) { + projectName = "swagger-clj-client"; + } + if (projectVersion == null) { + projectVersion = "1.0.0"; + } + if (projectDescription == null) { + projectDescription = "Client library of " + projectName; + } + + final String baseNamespace = dashize(projectName); + apiPackage = baseNamespace + ".api"; + + additionalProperties.put(PROJECT_NAME, projectName); + additionalProperties.put(PROJECT_DESCRIPTION, escapeText(projectDescription)); + additionalProperties.put(PROJECT_VERSION, projectVersion); + additionalProperties.put(BASE_NAMESPACE, baseNamespace); + additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage); + + final String baseNamespaceFolder = sourceFolder + File.separator + namespaceToFolder(baseNamespace); + supportingFiles.add(new SupportingFile("project.mustache", "", "project.clj")); + supportingFiles.add(new SupportingFile("core.mustache", baseNamespaceFolder, "core.clj")); + } + + @Override + public String apiFileFolder() { + return outputFolder + File.separator + sourceFolder + File.separator + namespaceToFolder(apiPackage); + } + + @Override + public String toOperationId(String operationId) { + // throw exception if method name is empty + if (StringUtils.isEmpty(operationId)) { + throw new RuntimeException("Empty method/operation name (operationId) not allowed"); + } + + return dashize(sanitizeName(operationId)); + } + + @Override + public String toApiName(String name) { + return dashize(name); + } + + @Override + public String toParamName(String name) { + return toVarName(name); + } + + @Override + public String toVarName(String name) { + name = name.replaceAll("[^a-zA-Z0-9_-]+", ""); + name = dashize(name); + return name; + } + + @Override + public Map postProcessOperations(Map operations) { + Map objs = (Map) operations.get("operations"); + List ops = (List) objs.get("operation"); + for (CodegenOperation op : ops) { + // Convert httpMethod to lower case, e.g. "get", "post" + op.httpMethod = op.httpMethod.toLowerCase(); + } + return operations; + } + + protected String namespaceToFolder(String ns) { + return ns.replace(".", File.separator).replace("-", "_"); + } + + protected String dashize(String s) { + return underscore(s).replaceAll("[_ ]", "-"); + } +} 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 4cf785abbd8..c663ba54a67 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 @@ -28,3 +28,4 @@ io.swagger.codegen.languages.TypeScriptAngularClientCodegen io.swagger.codegen.languages.TypeScriptNodeClientCodegen io.swagger.codegen.languages.AkkaScalaClientCodegen io.swagger.codegen.languages.CsharpDotNet2ClientCodegen +io.swagger.codegen.languages.ClojureClientCodegen diff --git a/modules/swagger-codegen/src/main/resources/clojure/api.mustache b/modules/swagger-codegen/src/main/resources/clojure/api.mustache new file mode 100644 index 00000000000..37107435e7b --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/clojure/api.mustache @@ -0,0 +1,18 @@ +(ns {{{package}}}.{{{classname}}} + (:require [{{{projectName}}}.core :refer [call-api check-required-params]])) +{{#operations}}{{#operation}} +(defn {{{nickname}}} + "{{{summary}}}{{#notes}} + {{{notes}}}{{/notes}}"{{#hasOptionalParams}} + ([{{#allParams}}{{#required}}{{{paramName}}} {{/required}}{{/allParams}}] ({{{nickname}}}{{#allParams}} {{#required}}{{{paramName}}}{{/required}}{{/allParams}} nil)){{/hasOptionalParams}} + {{#hasOptionalParams}}({{/hasOptionalParams}}[{{#allParams}}{{#required}}{{{paramName}}} {{/required}}{{/allParams}}{{#hasOptionalParams}} {:keys [{{#allParams}}{{^required}}{{{paramName}}} {{/required}}{{/allParams}}]}{{/hasOptionalParams}}]{{#hasRequiredParams}} + {{#hasOptionalParams}} {{/hasOptionalParams}}(check-required-params{{#allParams}}{{#required}} {{{paramName}}}{{/required}}{{/allParams}}){{/hasRequiredParams}} + {{#hasOptionalParams}} {{/hasOptionalParams}}(call-api "{{{path}}}" :{{{httpMethod}}} + {{#hasOptionalParams}} {{/hasOptionalParams}} {:path-params { {{#pathParams}}"{{{baseName}}}" {{{paramName}}} {{/pathParams}} } + {{#hasOptionalParams}} {{/hasOptionalParams}} :header-params { {{#headerParams}}"{{{baseName}}}" {{{paramName}}} {{/headerParams}} } + {{#hasOptionalParams}} {{/hasOptionalParams}} :query-params { {{#queryParams}}"{{{baseName}}}" {{{paramName}}} {{/queryParams}} } + {{#hasOptionalParams}} {{/hasOptionalParams}} :form-params { {{#formParams}}"{{{baseName}}}" {{{paramName}}} {{/formParams}} }{{#bodyParam}} + {{#hasOptionalParams}} {{/hasOptionalParams}} :body-param {{{paramName}}}{{/bodyParam}} + {{#hasOptionalParams}} {{/hasOptionalParams}} :content-types [{{#consumes}}"{{mediaType}}"{{#hasMore}} {{/hasMore}}{{/consumes}}] + {{#hasOptionalParams}} {{/hasOptionalParams}} :accepts [{{#produces}}"{{mediaType}}"{{#hasMore}} {{/hasMore}}{{/produces}}]})){{#hasOptionalParams}}){{/hasOptionalParams}} +{{/operation}}{{/operations}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/clojure/core.mustache b/modules/swagger-codegen/src/main/resources/clojure/core.mustache new file mode 100644 index 00000000000..b5b8dd06b9a --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/clojure/core.mustache @@ -0,0 +1,151 @@ +(ns {{{baseNamespace}}}.core + (:require [cheshire.core :refer [parse-string]] + [clojure.string :as str] + [clj-http.client :as client]) + (:import (com.fasterxml.jackson.core JsonParseException) + (java.util Date TimeZone) + (java.text SimpleDateFormat))) + +(def default-api-context + "Default API context." + {:base-url "http://petstore.swagger.io/v2" + :date-format "yyyy-MM-dd" + :datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" + :debug false}) + +(def ^:dynamic *api-context* + "Dynamic API context to be applied in API calls." + default-api-context) + +(defmacro with-api-context + "A helper macro to wrap *api-context* with default values." + [context & body] + `(binding [*api-context* (merge *api-context* ~context)] + ~@body)) + +(defmacro check-required-params + "Throw exception if the given parameter value is nil." + [& params] + (->> params + (map (fn [p] + `(if (nil? ~p) + (throw (IllegalArgumentException. ~(str "The parameter \"" p "\" is required")))))) + (list* 'do))) + +(defn- make-date-format + ([format-str] (make-date-format format-str nil)) + ([format-str time-zone] + (let [date-format (SimpleDateFormat. format-str)] + (when time-zone + (.setTimeZone date-format (TimeZone/getTimeZone time-zone))) + date-format))) + +(defn format-date + "Format the given Date object with the :date-format defined in *api-options*. + NOTE: The UTC time zone is used." + [^Date date] + (let [{:keys [date-format]} *api-context*] + (-> (make-date-format date-format "UTC") + (.format date)))) + +(defn parse-date + "Parse the given string to a Date object with the :date-format defined in *api-options*. + NOTE: The UTC time zone is used." + [^String s] + (let [{:keys [date-format]} *api-context*] + (-> (make-date-format date-format "UTC") + (.parse s)))) + +(defn format-datetime + "Format the given Date object with the :datetime-format defined in *api-options*. + NOTE: The system's default time zone is used when not provided." + ([^Date date] (format-datetime date nil)) + ([^Date date ^String time-zone] + (let [{:keys [datetime-format]} *api-context*] + (-> (make-date-format datetime-format time-zone) + (.format date))))) + +(defn parse-datetime + "Parse the given string to a Date object with the :datetime-format defined in *api-options*. + NOTE: The system's default time zone is used when not provided." + ([^String s] (parse-datetime s nil)) + ([^String s ^String time-zone] + (let [{:keys [datetime-format]} *api-context*] + (-> (make-date-format datetime-format time-zone) + (.parse s))))) + +(defn param-to-str [param] + "Format the given parameter value to string." + (cond + (instance? Date param) (format-datetime param) + (sequential? param) (str/join "," param) + :else (str param))) + +(defn make-url + "Make full URL by adding base URL and filling path parameters." + [path path-params] + (let [path (reduce (fn [p [k v]] + (str/replace p (re-pattern (str "\\{" k "\\}")) (param-to-str v))) + path + path-params)] + (str (:base-url *api-context*) path))) + +(defn normalize-params + "Normalize parameters values: remove nils, format to string with `param-to-str`." + [params] + (reduce (fn [result [k v]] + (if (nil? v) + result + (assoc result k (if (sequential? v) + (map param-to-str v) + (param-to-str v))))) + {} + params)) + +(defn json-mime? [mime] + "Check if the given MIME is a standard JSON MIME or :json." + (if mime + (or (= :json mime) + (re-matches #"application/json(;.*)?" (name mime))))) + +(defn json-preferred-mime [mimes] + "Choose a MIME from the given MIMEs with JSON preferred, + i.e. return JSON if included, otherwise return the first one." + (-> (filter json-mime? mimes) + first + (or (first mimes)))) + +(defn deserialize + "Deserialize the given HTTP response according to the Content-Type header." + [{:keys [body] {:keys [content-type]} :headers}] + (cond + (json-mime? content-type) + (try + (parse-string body true) + (catch JsonParseException e + ;; return the body string directly on JSON parsing error + body)) + ;; for non-JSON response, return the body string directly + :else body)) + +(defn call-api + "Call an API by making HTTP request and return its response." + [path method {:keys [path-params query-params header-params form-params body-param content-types accepts]}] + (let [{:keys [debug datetime-format]} *api-context* + url (make-url path path-params) + content-type (json-preferred-mime content-types) + accept (or (json-preferred-mime accepts) :json) + opts (cond-> {:url url :method method} + content-type (assoc :content-type content-type) + (json-mime? content-type) (assoc :json-opts {:date-format datetime-format}) + accept (assoc :accept accept) + (seq query-params) (assoc :query-params (normalize-params query-params)) + (seq header-params) (assoc :header-params (normalize-params header-params)) + (seq form-params) (assoc :form-params (normalize-params form-params)) + (and (empty? form-params) body-param) (assoc :form-params body-param) + debug (assoc :debug true :debug-body true)) + resp (client/request opts)] + (when debug + (println "Response:") + (println resp)) + (deserialize resp))) diff --git a/modules/swagger-codegen/src/main/resources/clojure/project.mustache b/modules/swagger-codegen/src/main/resources/clojure/project.mustache new file mode 100644 index 00000000000..ac01fb57152 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/clojure/project.mustache @@ -0,0 +1,8 @@ +(defproject {{{projectName}}} "{{{projectVersion}}}" + :description "{{{projectDescription}}}" + :url "http://example.com/FIXME" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + :dependencies [[org.clojure/clojure "1.7.0"] + [clj-http "2.0.0"] + [cheshire "5.5.0"]])