From cd8cfc50ed3c68c43d0d97552ffc50671b098eab Mon Sep 17 00:00:00 2001 From: xhh Date: Mon, 16 Nov 2015 18:00:43 +0800 Subject: [PATCH 1/5] Add Clojure client codegen --- .../io/swagger/codegen/CodegenOperation.java | 8 +- .../io/swagger/codegen/DefaultCodegen.java | 3 + .../languages/ClojureClientCodegen.java | 157 ++++++++++++++++++ .../services/io.swagger.codegen.CodegenConfig | 1 + .../src/main/resources/clojure/api.mustache | 18 ++ .../src/main/resources/clojure/core.mustache | 151 +++++++++++++++++ .../main/resources/clojure/project.mustache | 8 + 7 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ClojureClientCodegen.java create mode 100644 modules/swagger-codegen/src/main/resources/clojure/api.mustache create mode 100644 modules/swagger-codegen/src/main/resources/clojure/core.mustache create mode 100644 modules/swagger-codegen/src/main/resources/clojure/project.mustache 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"]]) From ff5b1c86ba449278c3a0a819f0195290ea407b3a Mon Sep 17 00:00:00 2001 From: xhh Date: Mon, 16 Nov 2015 23:04:17 +0800 Subject: [PATCH 2/5] Set project URL and license from spec --- .../languages/ClojureClientCodegen.java | 25 ++++++++++++- .../src/main/resources/clojure/api.mustache | 37 ++++++++++--------- .../main/resources/clojure/project.mustache | 11 +++--- 3 files changed, 48 insertions(+), 25 deletions(-) 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 index 1c7b6ab3f2d..0277c2d9077 100644 --- 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 @@ -6,7 +6,9 @@ import io.swagger.codegen.CodegenOperation; import io.swagger.codegen.CodegenType; import io.swagger.codegen.DefaultCodegen; import io.swagger.codegen.SupportingFile; +import io.swagger.models.Contact; import io.swagger.models.Info; +import io.swagger.models.License; import io.swagger.models.Swagger; import org.apache.commons.lang.StringUtils; @@ -20,6 +22,9 @@ public class ClojureClientCodegen extends DefaultCodegen implements CodegenConfi 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 PROJECT_URL = "projectUrl"; + private static final String LICENSE_NAME = "licenseName"; + private static final String LICENSE_URL = "licenseUrl"; private static final String BASE_NAMESPACE = "baseNamespace"; protected String projectName = null; @@ -69,14 +74,30 @@ public class ClojureClientCodegen extends DefaultCodegen implements CodegenConfi // when projectName is not specified, generate it from info.title projectName = dashize(info.getTitle()); } - if (projectVersion == null && info.getVersion() != null) { + if (projectVersion == null) { // when projectVersion is not specified, use info.version projectVersion = info.getVersion(); } - if (projectDescription == null && info.getDescription() != null) { + if (projectDescription == null) { // when projectDescription is not specified, use info.description projectDescription = info.getDescription(); } + + if (info.getContact() != null) { + Contact contact = info.getContact(); + if (additionalProperties.get(PROJECT_URL) == null) { + additionalProperties.put(PROJECT_URL, contact.getUrl()); + } + } + if (info.getLicense() != null) { + License license = info.getLicense(); + if (additionalProperties.get(LICENSE_NAME) == null) { + additionalProperties.put(LICENSE_NAME, license.getName()); + } + if (additionalProperties.get(LICENSE_URL) == null) { + additionalProperties.put(LICENSE_URL, license.getUrl()); + } + } } // default values diff --git a/modules/swagger-codegen/src/main/resources/clojure/api.mustache b/modules/swagger-codegen/src/main/resources/clojure/api.mustache index 37107435e7b..474a1fc37cf 100644 --- a/modules/swagger-codegen/src/main/resources/clojure/api.mustache +++ b/modules/swagger-codegen/src/main/resources/clojure/api.mustache @@ -1,18 +1,19 @@ -(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 +{{=< >=}} +(ns . + (:require [.core :refer [call-api check-required-params]])) +<#operations><#operation> +(defn + "<&summary><#notes> + <¬es>"<#hasOptionalParams> + ([<#allParams><#required> ] (<#allParams><#required> nil)) + <#hasOptionalParams>([<#allParams><#required> <#hasOptionalParams>{:keys [<#allParams><^required> ]}]<#hasRequiredParams> + <#hasOptionalParams> (check-required-params<#allParams><#required> ) + <#hasOptionalParams> (call-api "" : + <#hasOptionalParams> {:path-params {<#pathParams>"" } + <#hasOptionalParams> :header-params {<#headerParams>"" } + <#hasOptionalParams> :query-params {<#queryParams>"" } + <#hasOptionalParams> :form-params {<#formParams>"" }<#bodyParam> + <#hasOptionalParams> :body-param + <#hasOptionalParams> :content-types [<#consumes>""<#hasMore> ] + <#hasOptionalParams> :accepts [<#produces>""<#hasMore> ]}))<#hasOptionalParams>) + \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/clojure/project.mustache b/modules/swagger-codegen/src/main/resources/clojure/project.mustache index ac01fb57152..75e870714a7 100644 --- a/modules/swagger-codegen/src/main/resources/clojure/project.mustache +++ b/modules/swagger-codegen/src/main/resources/clojure/project.mustache @@ -1,8 +1,9 @@ -(defproject {{{projectName}}} "{{{projectVersion}}}" - :description "{{{projectDescription}}}" - :url "http://example.com/FIXME" - :license {:name "Eclipse Public License" - :url "http://www.eclipse.org/legal/epl-v10.html"} +{{=< >=}} +(defproject <&projectName> "<&projectVersion>" + :description "<&projectDescription>"<#projectUrl> + :url "<&projectUrl>"<#licenseName> + :license {:name "<&licenseName>"<#licenseUrl> + :url "<&licenseUrl>"} :dependencies [[org.clojure/clojure "1.7.0"] [clj-http "2.0.0"] [cheshire "5.5.0"]]) From 31cb5b11677f726533b6ea9470fd0fc5de6a5302 Mon Sep 17 00:00:00 2001 From: xhh Date: Tue, 17 Nov 2015 00:36:52 +0800 Subject: [PATCH 3/5] Add serialize method to handle JSON serialization --- .../src/main/resources/clojure/api.mustache | 3 +-- .../src/main/resources/clojure/core.mustache | 18 +++++++++++++----- .../main/resources/clojure/project.mustache | 3 +-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/swagger-codegen/src/main/resources/clojure/api.mustache b/modules/swagger-codegen/src/main/resources/clojure/api.mustache index 474a1fc37cf..3d314531cdc 100644 --- a/modules/swagger-codegen/src/main/resources/clojure/api.mustache +++ b/modules/swagger-codegen/src/main/resources/clojure/api.mustache @@ -1,5 +1,4 @@ -{{=< >=}} -(ns . +{{=< >=}}(ns . (:require [.core :refer [call-api check-required-params]])) <#operations><#operation> (defn diff --git a/modules/swagger-codegen/src/main/resources/clojure/core.mustache b/modules/swagger-codegen/src/main/resources/clojure/core.mustache index b5b8dd06b9a..50d23a6a740 100644 --- a/modules/swagger-codegen/src/main/resources/clojure/core.mustache +++ b/modules/swagger-codegen/src/main/resources/clojure/core.mustache @@ -1,5 +1,5 @@ (ns {{{baseNamespace}}}.core - (:require [cheshire.core :refer [parse-string]] + (:require [cheshire.core :refer [generate-string parse-string]] [clojure.string :as str] [clj-http.client :as client]) (:import (com.fasterxml.jackson.core JsonParseException) @@ -115,6 +115,14 @@ first (or (first mimes)))) +(defn serialize + "Serialize the given data according to content-type. + Only JSON is supported for now." + [data content-type] + (if (json-mime? content-type) + (generate-string data {:date-format (:datetime-format *api-context*)}) + (throw (IllegalArgumentException. (str "Content type \"" content-type "\" is not support for serialization"))))) + (defn deserialize "Deserialize the given HTTP response according to the Content-Type header." [{:keys [body] {:keys [content-type]} :headers}] @@ -131,18 +139,18 @@ (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* + (let [{:keys [debug]} *api-context* url (make-url path path-params) - content-type (json-preferred-mime content-types) + content-type (or (json-preferred-mime content-types) + (and body-param :json)) 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) + body-param (assoc :body (serialize body-param content-type)) debug (assoc :debug true :debug-body true)) resp (client/request opts)] (when debug diff --git a/modules/swagger-codegen/src/main/resources/clojure/project.mustache b/modules/swagger-codegen/src/main/resources/clojure/project.mustache index 75e870714a7..c403c9c43c6 100644 --- a/modules/swagger-codegen/src/main/resources/clojure/project.mustache +++ b/modules/swagger-codegen/src/main/resources/clojure/project.mustache @@ -1,5 +1,4 @@ -{{=< >=}} -(defproject <&projectName> "<&projectVersion>" +{{=< >=}}(defproject <&projectName> "<&projectVersion>" :description "<&projectDescription>"<#projectUrl> :url "<&projectUrl>"<#licenseName> :license {:name "<&licenseName>"<#licenseUrl> From 9cf7cd9ef687703b0b1a37807a345a0ef9ea9da2 Mon Sep 17 00:00:00 2001 From: xhh Date: Tue, 17 Nov 2015 00:47:12 +0800 Subject: [PATCH 4/5] And bin/clojure-petstore.sh and build the petstore clojure client sample --- bin/all-petstore.sh | 1 + bin/clojure-petstore.sh | 31 ++++ samples/client/petstore/clojure/.gitignore | 11 ++ samples/client/petstore/clojure/project.clj | 7 + .../clojure/src/swagger_petstore/api/pet.clj | 107 ++++++++++++ .../src/swagger_petstore/api/store.clj | 52 ++++++ .../clojure/src/swagger_petstore/api/user.clj | 107 ++++++++++++ .../clojure/src/swagger_petstore/core.clj | 159 ++++++++++++++++++ .../test/swagger_petstore/api/pet_test.clj | 87 ++++++++++ .../test/swagger_petstore/api/store_test.clj | 27 +++ .../test/swagger_petstore/api/user_test.clj | 57 +++++++ .../test/swagger_petstore/core_test.clj | 125 ++++++++++++++ 12 files changed, 771 insertions(+) create mode 100755 bin/clojure-petstore.sh create mode 100644 samples/client/petstore/clojure/.gitignore create mode 100644 samples/client/petstore/clojure/project.clj create mode 100644 samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj create mode 100644 samples/client/petstore/clojure/src/swagger_petstore/api/store.clj create mode 100644 samples/client/petstore/clojure/src/swagger_petstore/api/user.clj create mode 100644 samples/client/petstore/clojure/src/swagger_petstore/core.clj create mode 100644 samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj create mode 100644 samples/client/petstore/clojure/test/swagger_petstore/api/store_test.clj create mode 100644 samples/client/petstore/clojure/test/swagger_petstore/api/user_test.clj create mode 100644 samples/client/petstore/clojure/test/swagger_petstore/core_test.clj diff --git a/bin/all-petstore.sh b/bin/all-petstore.sh index dc2b4116070..a439d015337 100755 --- a/bin/all-petstore.sh +++ b/bin/all-petstore.sh @@ -20,6 +20,7 @@ fi cd $APP_DIR ./bin/akka-scala-petstore.sh ./bin/android-java-petstore.sh +./bin/clojure-petstore.sh ./bin/csharp-petstore.sh ./bin/dynamic-html.sh ./bin/html-petstore.sh diff --git a/bin/clojure-petstore.sh b/bin/clojure-petstore.sh new file mode 100755 index 00000000000..1f53d9d386c --- /dev/null +++ b/bin/clojure-petstore.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +SCRIPT="$0" + +while [ -h "$SCRIPT" ] ; do + ls=`ls -ld "$SCRIPT"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=`dirname "$SCRIPT"`/"$link" + fi +done + +if [ ! -d "${APP_DIR}" ]; then + APP_DIR=`dirname "$SCRIPT"`/.. + APP_DIR=`cd "${APP_DIR}"; pwd` +fi + +executable="./modules/swagger-codegen-cli/target/swagger-codegen-cli.jar" + +if [ ! -f "$executable" ] +then + mvn clean package +fi + +# if you've executed sbt assembly previously it will use that instead. +export JAVA_OPTS="${JAVA_OPTS} -XX:MaxPermSize=256M -Xmx1024M -DloggerPath=conf/log4j.properties" +ags="$@ generate -i modules/swagger-codegen/src/test/resources/2_0/petstore.json -l clojure -o samples/client/petstore/clojure" + +java $JAVA_OPTS -jar $executable $ags diff --git a/samples/client/petstore/clojure/.gitignore b/samples/client/petstore/clojure/.gitignore new file mode 100644 index 00000000000..c53038ec0e3 --- /dev/null +++ b/samples/client/petstore/clojure/.gitignore @@ -0,0 +1,11 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +.hgignore +.hg/ diff --git a/samples/client/petstore/clojure/project.clj b/samples/client/petstore/clojure/project.clj new file mode 100644 index 00000000000..34dcb2d99a7 --- /dev/null +++ b/samples/client/petstore/clojure/project.clj @@ -0,0 +1,7 @@ +(defproject swagger-petstore "1.0.0" + :description "This is a sample server Petstore server. You can find out more about Swagger at http://swagger.io or on irc.freenode.net, #swagger. For this sample, you can use the api key \"special-key\" to test the authorization filters" + :license {:name "Apache 2.0" + :url "http://www.apache.org/licenses/LICENSE-2.0.html"} + :dependencies [[org.clojure/clojure "1.7.0"] + [clj-http "2.0.0"] + [cheshire "5.5.0"]]) diff --git a/samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj b/samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj new file mode 100644 index 00000000000..f73ef83893e --- /dev/null +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj @@ -0,0 +1,107 @@ +(ns swagger-petstore.api.pet + (:require [swagger-petstore.core :refer [call-api check-required-params]])) + +(defn update-pet + "Update an existing pet + " + ([] (update-pet nil)) + ([{:keys [body ]}] + (call-api "/pet" :put + {:path-params {} + :header-params {} + :query-params {} + :form-params {} + :body-param body + :content-types ["application/json" "application/xml"] + :accepts ["application/json" "application/xml"]}))) + +(defn add-pet + "Add a new pet to the store + " + ([] (add-pet nil)) + ([{:keys [body ]}] + (call-api "/pet" :post + {:path-params {} + :header-params {} + :query-params {} + :form-params {} + :body-param body + :content-types ["application/json" "application/xml"] + :accepts ["application/json" "application/xml"]}))) + +(defn find-pets-by-status + "Finds Pets by status + Multiple status values can be provided with comma seperated strings" + ([] (find-pets-by-status nil)) + ([{:keys [status ]}] + (call-api "/pet/findByStatus" :get + {:path-params {} + :header-params {} + :query-params {"status" status } + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn find-pets-by-tags + "Finds Pets by tags + Muliple tags can be provided with comma seperated strings. Use tag1, tag2, tag3 for testing." + ([] (find-pets-by-tags nil)) + ([{:keys [tags ]}] + (call-api "/pet/findByTags" :get + {:path-params {} + :header-params {} + :query-params {"tags" tags } + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn get-pet-by-id + "Find pet by ID + Returns a pet when ID < 10. ID > 10 or nonintegers will simulate API error conditions" + [pet-id ] + (call-api "/pet/{petId}" :get + {:path-params {"petId" pet-id } + :header-params {} + :query-params {} + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]})) + +(defn update-pet-with-form + "Updates a pet in the store with form data + " + ([pet-id ] (update-pet-with-form pet-id nil)) + ([pet-id {:keys [name status ]}] + (call-api "/pet/{petId}" :post + {:path-params {"petId" pet-id } + :header-params {} + :query-params {} + :form-params {"name" name "status" status } + :content-types ["application/x-www-form-urlencoded"] + :accepts ["application/json" "application/xml"]}))) + +(defn delete-pet + "Deletes a pet + " + ([pet-id ] (delete-pet pet-id nil)) + ([pet-id {:keys [api-key ]}] + (call-api "/pet/{petId}" :delete + {:path-params {"petId" pet-id } + :header-params {"api_key" api-key } + :query-params {} + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn upload-file + "uploads an image + " + ([pet-id ] (upload-file pet-id nil)) + ([pet-id {:keys [additional-metadata file ]}] + (call-api "/pet/{petId}/uploadImage" :post + {:path-params {"petId" pet-id } + :header-params {} + :query-params {} + :form-params {"additionalMetadata" additional-metadata "file" file } + :content-types ["multipart/form-data"] + :accepts ["application/json" "application/xml"]}))) diff --git a/samples/client/petstore/clojure/src/swagger_petstore/api/store.clj b/samples/client/petstore/clojure/src/swagger_petstore/api/store.clj new file mode 100644 index 00000000000..4d5ac3ca325 --- /dev/null +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/store.clj @@ -0,0 +1,52 @@ +(ns swagger-petstore.api.store + (:require [swagger-petstore.core :refer [call-api check-required-params]])) + +(defn get-inventory + "Returns pet inventories by status + Returns a map of status codes to quantities" + [] + (call-api "/store/inventory" :get + {:path-params {} + :header-params {} + :query-params {} + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]})) + +(defn place-order + "Place an order for a pet + " + ([] (place-order nil)) + ([{:keys [body ]}] + (call-api "/store/order" :post + {:path-params {} + :header-params {} + :query-params {} + :form-params {} + :body-param body + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn get-order-by-id + "Find purchase order by ID + For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions" + [order-id ] + (call-api "/store/order/{orderId}" :get + {:path-params {"orderId" order-id } + :header-params {} + :query-params {} + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]})) + +(defn delete-order + "Delete purchase order by ID + For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors" + [order-id ] + (call-api "/store/order/{orderId}" :delete + {:path-params {"orderId" order-id } + :header-params {} + :query-params {} + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]})) diff --git a/samples/client/petstore/clojure/src/swagger_petstore/api/user.clj b/samples/client/petstore/clojure/src/swagger_petstore/api/user.clj new file mode 100644 index 00000000000..9f34a1be537 --- /dev/null +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/user.clj @@ -0,0 +1,107 @@ +(ns swagger-petstore.api.user + (:require [swagger-petstore.core :refer [call-api check-required-params]])) + +(defn create-user + "Create user + This can only be done by the logged in user." + ([] (create-user nil)) + ([{:keys [body ]}] + (call-api "/user" :post + {:path-params {} + :header-params {} + :query-params {} + :form-params {} + :body-param body + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn create-users-with-array-input + "Creates list of users with given input array + " + ([] (create-users-with-array-input nil)) + ([{:keys [body ]}] + (call-api "/user/createWithArray" :post + {:path-params {} + :header-params {} + :query-params {} + :form-params {} + :body-param body + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn create-users-with-list-input + "Creates list of users with given input array + " + ([] (create-users-with-list-input nil)) + ([{:keys [body ]}] + (call-api "/user/createWithList" :post + {:path-params {} + :header-params {} + :query-params {} + :form-params {} + :body-param body + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn login-user + "Logs user into the system + " + ([] (login-user nil)) + ([{:keys [username password ]}] + (call-api "/user/login" :get + {:path-params {} + :header-params {} + :query-params {"username" username "password" password } + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn logout-user + "Logs out current logged in user session + " + [] + (call-api "/user/logout" :get + {:path-params {} + :header-params {} + :query-params {} + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]})) + +(defn get-user-by-name + "Get user by user name + " + [username ] + (call-api "/user/{username}" :get + {:path-params {"username" username } + :header-params {} + :query-params {} + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]})) + +(defn update-user + "Updated user + This can only be done by the logged in user." + ([username ] (update-user username nil)) + ([username {:keys [body ]}] + (call-api "/user/{username}" :put + {:path-params {"username" username } + :header-params {} + :query-params {} + :form-params {} + :body-param body + :content-types [] + :accepts ["application/json" "application/xml"]}))) + +(defn delete-user + "Delete user + This can only be done by the logged in user." + [username ] + (call-api "/user/{username}" :delete + {:path-params {"username" username } + :header-params {} + :query-params {} + :form-params {} + :content-types [] + :accepts ["application/json" "application/xml"]})) diff --git a/samples/client/petstore/clojure/src/swagger_petstore/core.clj b/samples/client/petstore/clojure/src/swagger_petstore/core.clj new file mode 100644 index 00000000000..85856674a19 --- /dev/null +++ b/samples/client/petstore/clojure/src/swagger_petstore/core.clj @@ -0,0 +1,159 @@ +(ns swagger-petstore.core + (:require [cheshire.core :refer [generate-string 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 serialize + "Serialize the given data according to content-type. + Only JSON is supported for now." + [data content-type] + (if (json-mime? content-type) + (generate-string data {:date-format (:datetime-format *api-context*)}) + (throw (IllegalArgumentException. (str "Content type \"" content-type "\" is not support for serialization"))))) + +(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]} *api-context* + url (make-url path path-params) + content-type (or (json-preferred-mime content-types) + (and body-param :json)) + accept (or (json-preferred-mime accepts) :json) + opts (cond-> {:url url :method method} + content-type (assoc :content-type content-type) + 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)) + body-param (assoc :body (serialize body-param content-type)) + debug (assoc :debug true :debug-body true)) + resp (client/request opts)] + (when debug + (println "Response:") + (println resp)) + (deserialize resp))) diff --git a/samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj b/samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj new file mode 100644 index 00000000000..8acd528c5a5 --- /dev/null +++ b/samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj @@ -0,0 +1,87 @@ +(ns swagger-petstore.api.pet-test + (:require [clojure.test :refer :all] + [clojure.java.io :as io] + [swagger-petstore.api.pet :refer :all])) + +(defn- make-random-pet + ([] (make-random-pet nil)) + ([{:keys [id] :as attrs :or {id (System/currentTimeMillis)}}] + (merge {:id id + :name (str "pet-" id) + :status "available" + :photoUrls ["http://foo.bar.com/1" "http://foo.bar.com/2"] + :category {:name "really-happy"}} + attrs))) + +(deftest test-create-and-get-pet + (let [{:keys [id] :as pet} (make-random-pet) + _ (add-pet {:body pet}) + fetched (get-pet-by-id id)] + (is (identity fetched)) + (is (= id (:id fetched))) + (is (identity (:category fetched))) + (is (= (get-in pet [:category :name]) (get-in fetched [:category :name]))) + (delete-pet id))) + +(deftest test-find-pets-by-status + (let [status "pending" + {:keys [id] :as pet} (make-random-pet {:status status}) + _ (add-pet {:body pet}) + pets (find-pets-by-status {:status [status]})] + (is (seq pets)) + (is (some #{id} (map :id pets))) + (delete-pet id))) + +(deftest test-find-pets-by-tags + (let [tag-name (str "tag-" (rand-int 1000)) + tag {:name tag-name} + {:keys [id] :as pet} (make-random-pet {:tags [tag]}) + _ (add-pet {:body pet}) + pets (find-pets-by-tags {:tags [tag-name]})] + (is (seq pets)) + (is (some #{id} (map :id pets))) + (delete-pet id))) + +(deftest test-update-pet-with-form + (let [{pet-id :id :as pet} (make-random-pet {:name "new name" :status "available"}) + _ (add-pet {:body pet}) + {:keys [id name status]} (get-pet-by-id pet-id)] + (is (= pet-id id)) + (is (= "new name" name)) + (is (= "available" status)) + ;; update "name" only + (update-pet-with-form pet-id {:name "updated name 1"}) + (let [{:keys [id name status]} (get-pet-by-id pet-id)] + (is (= pet-id id)) + (is (= "updated name 1" name)) + (is (= "available" status))) + ;; update "status" only + (update-pet-with-form pet-id {:status "pending"}) + (let [{:keys [id name status]} (get-pet-by-id pet-id)] + (is (= pet-id id)) + (is (= "updated name 1" name)) + (is (= "pending" status))) + ;; update both "name" and "status" + (update-pet-with-form pet-id {:name "updated name 2" :status "sold"}) + (let [{:keys [id name status]} (get-pet-by-id pet-id)] + (is (= pet-id id)) + (is (= "updated name 2" name)) + (is (= "sold" status))) + (delete-pet pet-id))) + +(deftest test-delete-pet + (let [{:keys [id] :as pet} (make-random-pet) + _ (add-pet {:body pet}) + fetched (get-pet-by-id id)] + (is (= id (:id fetched))) + (delete-pet id) + (is (thrown? RuntimeException (get-pet-by-id id))))) + +(comment + ;; TODO support file uploading + (deftest test-upload-file + (let [{:keys [id] :as pet} (make-random-pet) + _ (add-pet {:body pet}) + file (io/file (io/resource "hello.txt"))] + ;; no errors + (upload-file id {:file file :additionalMetadata "uploading file with clojure client"})))) diff --git a/samples/client/petstore/clojure/test/swagger_petstore/api/store_test.clj b/samples/client/petstore/clojure/test/swagger_petstore/api/store_test.clj new file mode 100644 index 00000000000..5a453e3693e --- /dev/null +++ b/samples/client/petstore/clojure/test/swagger_petstore/api/store_test.clj @@ -0,0 +1,27 @@ +(ns swagger-petstore.api.store-test + (:require [clojure.test :refer :all] + [swagger-petstore.api.store :refer :all]) + (:import (java.util Date))) + +(defn- make-random-order [] + {:id (+ 90000 (rand-int 10000)) + :petId 200 + :quantity 13 + :shipDate (Date.) + :status "placed" + :complete true}) + +(deftest test-get-inventory + (let [inventory (get-inventory)] + (is (pos? (count inventory))))) + +(deftest test-place-and-delete-order + (let [order (make-random-order) + order-id (:id order) + _ (place-order {:body order}) + fetched (get-order-by-id order-id)] + (doseq [attr [:id :petId :quantity]] + (is (= (attr order) (attr fetched)))) + (delete-order order-id) + (comment "it seems that delete-order does not really delete the order" + (is (thrown? RuntimeException (get-order-by-id order-id)))))) diff --git a/samples/client/petstore/clojure/test/swagger_petstore/api/user_test.clj b/samples/client/petstore/clojure/test/swagger_petstore/api/user_test.clj new file mode 100644 index 00000000000..aa05b23f19a --- /dev/null +++ b/samples/client/petstore/clojure/test/swagger_petstore/api/user_test.clj @@ -0,0 +1,57 @@ +(ns swagger-petstore.api.user-test + (:require [clojure.test :refer :all] + [swagger-petstore.api.user :refer :all])) + +(defn- make-random-user + ([] (make-random-user nil)) + ([{:keys [id] :as attrs :or {id (System/currentTimeMillis)}}] + (merge {:id id + :username (str "user-" id) + :password "my-password" + :userStatus 0} + attrs))) + +(deftest test-create-and-delete-user + (let [user (make-random-user) + username (:username user) + _ (create-user {:body user}) + fetched (get-user-by-name username)] + (doseq [attr [:id :username :password :userStatus]] + (is (= (attr user) (attr fetched)))) + (delete-user username) + (is (thrown? RuntimeException (get-user-by-name username))))) + +(deftest test-create-users-with-array-input + (let [id1 (System/currentTimeMillis) + id2 (inc id1) + user1 (make-random-user {:id id1}) + user2 (make-random-user {:id id2})] + (create-users-with-array-input {:body [user1 user2]}) + (let [fetched (get-user-by-name (:username user1))] + (is (= id1 (:id fetched)))) + (let [fetched (get-user-by-name (:username user2))] + (is (= id2 (:id fetched)))) + (delete-user (:username user1)) + (delete-user (:username user2)))) + +(deftest test-create-users-with-list-input + (let [id1 (System/currentTimeMillis) + id2 (inc id1) + user1 (make-random-user {:id id1}) + user2 (make-random-user {:id id2})] + (create-users-with-list-input {:body [user1 user2]}) + (let [fetched (get-user-by-name (:username user1))] + (is (= id1 (:id fetched)))) + (let [fetched (get-user-by-name (:username user2))] + (is (= id2 (:id fetched)))) + (delete-user (:username user1)) + (delete-user (:username user2)))) + +(deftest test-login-and-lougout-user + (let [{:keys [username password] :as user} (make-random-user) + _ (create-user {:body user}) + result (login-user {:username username :password password})] + (is (re-matches #"logged in user session:.+" result)) + ;; no error with logout-user + (logout-user) + (delete-user username))) diff --git a/samples/client/petstore/clojure/test/swagger_petstore/core_test.clj b/samples/client/petstore/clojure/test/swagger_petstore/core_test.clj new file mode 100644 index 00000000000..ac39e8a503f --- /dev/null +++ b/samples/client/petstore/clojure/test/swagger_petstore/core_test.clj @@ -0,0 +1,125 @@ +(ns swagger-petstore.core-test + (:require [clojure.test :refer :all] + [swagger-petstore.core :refer :all]) + (:import (java.text ParseException))) + +(deftest test-api-context + (testing "default" + (is (= {:base-url "http://petstore.swagger.io/v2" + :date-format "yyyy-MM-dd" + :datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" + :debug false} + default-api-context + *api-context* + (with-api-context {} + *api-context*)))) + (testing "customize via with-api-context" + (with-api-context {:base-url "http://localhost" :debug true} + (is (= {:base-url "http://localhost" + :date-format "yyyy-MM-dd" + :datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" + :debug true} + *api-context*)) + ;; nested with-api-context inherits values from the outer api context + (with-api-context {:datetime-format "yyyy-MM-dd HH:mm:ss"} + (is (= {:base-url "http://localhost" + :date-format "yyyy-MM-dd" + :datetime-format "yyyy-MM-dd HH:mm:ss" + :debug true} + *api-context*)))) + ;; back to default api context + (is (= {:base-url "http://petstore.swagger.io/v2" + :date-format "yyyy-MM-dd" + :datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" + :debug false} + *api-context*)))) + +(deftest test-check-required-params + (let [a nil b :not-nil] + (is (thrown? IllegalArgumentException (check-required-params a))) + (is (nil? (check-required-params b))))) + +(deftest test-parse-and-format-date + (testing "default date format" + (is (= "2015-11-07" (-> "2015-11-07T03:49:09.356+00:00" parse-datetime format-date))) + (is (= "2015-11-07" (-> "2015-11-07" parse-date format-date))) + (is (thrown? ParseException (parse-date "2015-11")))) + (testing "custom date format: without day" + (with-api-context {:date-format "yyyy-MM"} + (is (= "2015-11" (-> "2015-11-07T03:49:09.123Z" parse-datetime format-date))) + (is (= "2015-11" (-> "2015-11" parse-date format-date))) + (is (thrown? ParseException (parse-date "2015")))))) + +(deftest test-parse-and-format-datetime + (testing "default datetime format" + (are [s] + (is (= "2015-11-07T03:49:09.356Z" (-> s parse-datetime (format-datetime "UTC")))) + "2015-11-07T03:49:09.356+00:00" + "2015-11-07T05:49:09.356+02:00" + "2015-11-07T02:49:09.356-01:00" + "2015-11-07T03:49:09.356Z") + (is (thrown? ParseException (parse-datetime "2015-11-07 03:49:09")))) + (testing "custom datetime format: without milliseconds" + (with-api-context {:datetime-format "yyyy-MM-dd'T'HH:mm:ssXXX"} + (are [s] + (is (= "2015-11-07T13:49:09+10:00" (-> s parse-datetime (format-datetime "GMT+10")))) + "2015-11-07T03:49:09+00:00" + "2015-11-07T03:49:09Z" + "2015-11-07T00:49:09-03:00") + (is (thrown? ParseException (parse-datetime "2015-11-07T03:49:09.123Z")))))) + +(deftest test-param-to-str + (let [date (parse-datetime "2015-11-07T03:49:09.123Z")] + (are [param expected] + (is (= expected (param-to-str param))) + nil "" + "abc" "abc" + 123 "123" + 1.0 "1.0" + [12 "34"] "12,34" + date (format-datetime date)))) + +(deftest test-make-url + (are [path path-params url] + (is (= url (make-url path path-params))) + "/pet/{petId}" {"petId" 123} "http://petstore.swagger.io/v2/pet/123" + "/" nil "http://petstore.swagger.io/v2/" + "/pet" {"id" 1} "http://petstore.swagger.io/v2/pet" + "/pet/{id}" nil "http://petstore.swagger.io/v2/pet/{id}")) + +(deftest test-normalize-params + (is (= {:a "123" :b ["4" "5,6"]} + (normalize-params {:a 123 :b [4 [5 "6"]] :c nil})))) + +(deftest test-json-mime? + (are [mime expected] + (is (= expected (boolean (json-mime? mime)))) + :json true + "application/json" true + "application/json; charset=utf8" true + nil false + :xml false + "application/pdf" false)) + +(deftest test-json-preferred-mime + (are [mimes expected] + (is (= expected (json-preferred-mime mimes))) + ["application/xml" "application/json"] "application/json" + [:json] :json + [] nil + nil nil + ["application/xml"] "application/xml")) + +(deftest test-serialize + (is (= "{\"aa\":1,\"bb\":\"2\"}" (serialize {:aa 1 :bb "2"} :json))) + (is (= "{}" (serialize {} "application/json"))) + (is (= "[1,\"2\"]" (serialize [1 "2"] "application/json; charset=UTF8"))) + (is (thrown? IllegalArgumentException (serialize {} "application/xml")))) + +(deftest test-deserialize + (are [body content-type expected] + (is (= expected (deserialize {:body body :headers {:content-type content-type}}))) + "{\"aa\": 1, \"bb\": \"2\"}" "application/json" {:aa 1 :bb "2"} + "[1, \"2\"]" "application/json; charset=UTF8" [1 "2"] + "{invalid json}" "application/json" "{invalid json}" + "plain text" "text/plain" "plain text")) \ No newline at end of file From 473aab2d8e29138956d801b33a9bbc394ac18ca0 Mon Sep 17 00:00:00 2001 From: xhh Date: Wed, 18 Nov 2015 17:21:32 +0800 Subject: [PATCH 5/5] Support file uploading in Clojure client --- .../src/main/resources/clojure/api.mustache | 7 ++-- .../src/main/resources/clojure/core.mustache | 39 ++++++++++++++----- .../petstore/clojure/resources/hello.txt | 1 + .../clojure/src/swagger_petstore/api/pet.clj | 5 ++- .../src/swagger_petstore/api/store.clj | 3 +- .../clojure/src/swagger_petstore/api/user.clj | 3 +- .../clojure/src/swagger_petstore/core.clj | 39 ++++++++++++++----- .../test/swagger_petstore/api/pet_test.clj | 14 +++---- .../test/swagger_petstore/core_test.clj | 14 ++++++- 9 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 samples/client/petstore/clojure/resources/hello.txt diff --git a/modules/swagger-codegen/src/main/resources/clojure/api.mustache b/modules/swagger-codegen/src/main/resources/clojure/api.mustache index 3d314531cdc..a4425280f1b 100644 --- a/modules/swagger-codegen/src/main/resources/clojure/api.mustache +++ b/modules/swagger-codegen/src/main/resources/clojure/api.mustache @@ -1,11 +1,12 @@ {{=< >=}}(ns . - (:require [.core :refer [call-api check-required-params]])) + (:require [.core :refer [call-api check-required-params]]) + (:import (java.io File))) <#operations><#operation> (defn "<&summary><#notes> <¬es>"<#hasOptionalParams> - ([<#allParams><#required> ] (<#allParams><#required> nil)) - <#hasOptionalParams>([<#allParams><#required> <#hasOptionalParams>{:keys [<#allParams><^required> ]}]<#hasRequiredParams> + ([<#allParams><#required><#isFile>^File ] (<#allParams><#required> nil)) + <#hasOptionalParams>([<#allParams><#required><#isFile>^File <#hasOptionalParams>{:keys [<#allParams><^required><#isFile>^File ]}]<#hasRequiredParams> <#hasOptionalParams> (check-required-params<#allParams><#required> ) <#hasOptionalParams> (call-api "" : <#hasOptionalParams> {:path-params {<#pathParams>"" } diff --git a/modules/swagger-codegen/src/main/resources/clojure/core.mustache b/modules/swagger-codegen/src/main/resources/clojure/core.mustache index 50d23a6a740..32aa8a6bc0d 100644 --- a/modules/swagger-codegen/src/main/resources/clojure/core.mustache +++ b/modules/swagger-codegen/src/main/resources/clojure/core.mustache @@ -3,6 +3,7 @@ [clojure.string :as str] [clj-http.client :as client]) (:import (com.fasterxml.jackson.core JsonParseException) + (java.io File) (java.util Date TimeZone) (java.text SimpleDateFormat))) @@ -90,17 +91,24 @@ path-params)] (str (:base-url *api-context*) path))) +(defn normalize-param + "Normalize parameter value, handling three cases: + for sequential value, normalize each elements of it; + for File value, do nothing with it; + otherwise, call `param-to-string`." + [param] + (cond + (sequential? param) (map normalize-param param) + (instance? File param) param + :else (param-to-str param))) + (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)) + (->> params + (remove (comp nil? second)) + (map (fn [[k v]] [k (normalize-param v)])) + (into {}))) (defn json-mime? [mime] "Check if the given MIME is a standard JSON MIME or :json." @@ -136,6 +144,13 @@ ;; for non-JSON response, return the body string directly :else body)) +(defn form-params-to-multipart + "Convert the given form parameters map into a vector as clj-http's :multipart option." + [form-params] + (->> form-params + (map (fn [[k v]] (array-map :name k :content v))) + vec)) + (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]}] @@ -144,12 +159,16 @@ content-type (or (json-preferred-mime content-types) (and body-param :json)) accept (or (json-preferred-mime accepts) :json) + multipart? (= "multipart/form-data" content-type) opts (cond-> {:url url :method method} - content-type (assoc :content-type content-type) 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 content-type (not multipart?)) (assoc :content-type content-type) + multipart? (assoc :multipart (-> form-params + normalize-params + form-params-to-multipart)) + (and (not multipart?) (seq form-params)) (assoc :form-params (normalize-params form-params)) body-param (assoc :body (serialize body-param content-type)) debug (assoc :debug true :debug-body true)) resp (client/request opts)] diff --git a/samples/client/petstore/clojure/resources/hello.txt b/samples/client/petstore/clojure/resources/hello.txt new file mode 100644 index 00000000000..6769dd60bdf --- /dev/null +++ b/samples/client/petstore/clojure/resources/hello.txt @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj b/samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj index f73ef83893e..c331444c886 100644 --- a/samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj @@ -1,5 +1,6 @@ (ns swagger-petstore.api.pet - (:require [swagger-petstore.core :refer [call-api check-required-params]])) + (:require [swagger-petstore.core :refer [call-api check-required-params]]) + (:import (java.io File))) (defn update-pet "Update an existing pet @@ -97,7 +98,7 @@ "uploads an image " ([pet-id ] (upload-file pet-id nil)) - ([pet-id {:keys [additional-metadata file ]}] + ([pet-id {:keys [additional-metadata ^File file ]}] (call-api "/pet/{petId}/uploadImage" :post {:path-params {"petId" pet-id } :header-params {} diff --git a/samples/client/petstore/clojure/src/swagger_petstore/api/store.clj b/samples/client/petstore/clojure/src/swagger_petstore/api/store.clj index 4d5ac3ca325..f2a18cea2e8 100644 --- a/samples/client/petstore/clojure/src/swagger_petstore/api/store.clj +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/store.clj @@ -1,5 +1,6 @@ (ns swagger-petstore.api.store - (:require [swagger-petstore.core :refer [call-api check-required-params]])) + (:require [swagger-petstore.core :refer [call-api check-required-params]]) + (:import (java.io File))) (defn get-inventory "Returns pet inventories by status diff --git a/samples/client/petstore/clojure/src/swagger_petstore/api/user.clj b/samples/client/petstore/clojure/src/swagger_petstore/api/user.clj index 9f34a1be537..15d41515ebb 100644 --- a/samples/client/petstore/clojure/src/swagger_petstore/api/user.clj +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/user.clj @@ -1,5 +1,6 @@ (ns swagger-petstore.api.user - (:require [swagger-petstore.core :refer [call-api check-required-params]])) + (:require [swagger-petstore.core :refer [call-api check-required-params]]) + (:import (java.io File))) (defn create-user "Create user diff --git a/samples/client/petstore/clojure/src/swagger_petstore/core.clj b/samples/client/petstore/clojure/src/swagger_petstore/core.clj index 85856674a19..e7c55258e79 100644 --- a/samples/client/petstore/clojure/src/swagger_petstore/core.clj +++ b/samples/client/petstore/clojure/src/swagger_petstore/core.clj @@ -3,6 +3,7 @@ [clojure.string :as str] [clj-http.client :as client]) (:import (com.fasterxml.jackson.core JsonParseException) + (java.io File) (java.util Date TimeZone) (java.text SimpleDateFormat))) @@ -90,17 +91,24 @@ path-params)] (str (:base-url *api-context*) path))) +(defn normalize-param + "Normalize parameter value, handling three cases: + for sequential value, normalize each elements of it; + for File value, do nothing with it; + otherwise, call `param-to-string`." + [param] + (cond + (sequential? param) (map normalize-param param) + (instance? File param) param + :else (param-to-str param))) + (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)) + (->> params + (remove (comp nil? second)) + (map (fn [[k v]] [k (normalize-param v)])) + (into {}))) (defn json-mime? [mime] "Check if the given MIME is a standard JSON MIME or :json." @@ -136,6 +144,13 @@ ;; for non-JSON response, return the body string directly :else body)) +(defn form-params-to-multipart + "Convert the given form parameters map into a vector as clj-http's :multipart option." + [form-params] + (->> form-params + (map (fn [[k v]] (array-map :name k :content v))) + vec)) + (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]}] @@ -144,12 +159,16 @@ content-type (or (json-preferred-mime content-types) (and body-param :json)) accept (or (json-preferred-mime accepts) :json) + multipart? (= "multipart/form-data" content-type) opts (cond-> {:url url :method method} - content-type (assoc :content-type content-type) 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 content-type (not multipart?)) (assoc :content-type content-type) + multipart? (assoc :multipart (-> form-params + normalize-params + form-params-to-multipart)) + (and (not multipart?) (seq form-params)) (assoc :form-params (normalize-params form-params)) body-param (assoc :body (serialize body-param content-type)) debug (assoc :debug true :debug-body true)) resp (client/request opts)] diff --git a/samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj b/samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj index 8acd528c5a5..7327b44c45d 100644 --- a/samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj +++ b/samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj @@ -77,11 +77,9 @@ (delete-pet id) (is (thrown? RuntimeException (get-pet-by-id id))))) -(comment - ;; TODO support file uploading - (deftest test-upload-file - (let [{:keys [id] :as pet} (make-random-pet) - _ (add-pet {:body pet}) - file (io/file (io/resource "hello.txt"))] - ;; no errors - (upload-file id {:file file :additionalMetadata "uploading file with clojure client"})))) +(deftest test-upload-file + (let [{:keys [id] :as pet} (make-random-pet) + _ (add-pet {:body pet}) + file (io/file (io/resource "hello.txt"))] + ;; no errors with upload-file + (upload-file id {:file file :additional-metadata "uploading file with clojure client"}))) diff --git a/samples/client/petstore/clojure/test/swagger_petstore/core_test.clj b/samples/client/petstore/clojure/test/swagger_petstore/core_test.clj index ac39e8a503f..394824aa4a9 100644 --- a/samples/client/petstore/clojure/test/swagger_petstore/core_test.clj +++ b/samples/client/petstore/clojure/test/swagger_petstore/core_test.clj @@ -1,5 +1,6 @@ (ns swagger-petstore.core-test - (:require [clojure.test :refer :all] + (:require [clojure.java.io :as io] + [clojure.test :refer :all] [swagger-petstore.core :refer :all]) (:import (java.text ParseException))) @@ -87,8 +88,17 @@ "/pet" {"id" 1} "http://petstore.swagger.io/v2/pet" "/pet/{id}" nil "http://petstore.swagger.io/v2/pet/{id}")) +(deftest test-normalize-param + (let [file (-> "hello.txt" io/resource io/file)] + (are [param expected] + (is (= expected (normalize-param param))) + [12 "34"] ["12" "34"] + file file + "abc" "abc" + [[12 "34"] file "abc"] [["12" "34"] file "abc"]))) + (deftest test-normalize-params - (is (= {:a "123" :b ["4" "5,6"]} + (is (= {:a "123" :b ["4" ["5" "6"]]} (normalize-params {:a 123 :b [4 [5 "6"]] :c nil})))) (deftest test-json-mime?