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/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..0277c2d9077 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ClojureClientCodegen.java @@ -0,0 +1,178 @@ +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.Contact; +import io.swagger.models.Info; +import io.swagger.models.License; +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 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; + 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) { + // when projectVersion is not specified, use info.version + projectVersion = info.getVersion(); + } + 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 + 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 11283011a89..5c607488d9b 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 @@ -29,3 +29,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..a4425280f1b --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/clojure/api.mustache @@ -0,0 +1,19 @@ +{{=< >=}}(ns . + (:require [.core :refer [call-api check-required-params]]) + (:import (java.io File))) +<#operations><#operation> +(defn + "<&summary><#notes> + <¬es>"<#hasOptionalParams> + ([<#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>"" } + <#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/core.mustache b/modules/swagger-codegen/src/main/resources/clojure/core.mustache new file mode 100644 index 00000000000..32aa8a6bc0d --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/clojure/core.mustache @@ -0,0 +1,178 @@ +(ns {{{baseNamespace}}}.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.io File) + (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-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] + (->> 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." + (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 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]}] + (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) + multipart? (= "multipart/form-data" content-type) + opts (cond-> {:url url :method method} + accept (assoc :accept accept) + (seq query-params) (assoc :query-params (normalize-params query-params)) + (seq header-params) (assoc :header-params (normalize-params header-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)] + (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..c403c9c43c6 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/clojure/project.mustache @@ -0,0 +1,8 @@ +{{=< >=}}(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"]]) 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/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 new file mode 100644 index 00000000000..c331444c886 --- /dev/null +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/pet.clj @@ -0,0 +1,108 @@ +(ns swagger-petstore.api.pet + (:require [swagger-petstore.core :refer [call-api check-required-params]]) + (:import (java.io File))) + +(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 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..f2a18cea2e8 --- /dev/null +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/store.clj @@ -0,0 +1,53 @@ +(ns swagger-petstore.api.store + (:require [swagger-petstore.core :refer [call-api check-required-params]]) + (:import (java.io File))) + +(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..15d41515ebb --- /dev/null +++ b/samples/client/petstore/clojure/src/swagger_petstore/api/user.clj @@ -0,0 +1,108 @@ +(ns swagger-petstore.api.user + (:require [swagger-petstore.core :refer [call-api check-required-params]]) + (:import (java.io File))) + +(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..e7c55258e79 --- /dev/null +++ b/samples/client/petstore/clojure/src/swagger_petstore/core.clj @@ -0,0 +1,178 @@ +(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.io File) + (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-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] + (->> 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." + (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 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]}] + (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) + multipart? (= "multipart/form-data" content-type) + opts (cond-> {:url url :method method} + accept (assoc :accept accept) + (seq query-params) (assoc :query-params (normalize-params query-params)) + (seq header-params) (assoc :header-params (normalize-params header-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)] + (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..7327b44c45d --- /dev/null +++ b/samples/client/petstore/clojure/test/swagger_petstore/api/pet_test.clj @@ -0,0 +1,85 @@ +(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))))) + +(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/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..394824aa4a9 --- /dev/null +++ b/samples/client/petstore/clojure/test/swagger_petstore/core_test.clj @@ -0,0 +1,135 @@ +(ns swagger-petstore.core-test + (:require [clojure.java.io :as io] + [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-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"]]} + (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