Merge pull request #1585 from xhh/clojure-client

Add a Clojure client codegen
This commit is contained in:
wing328 2015-11-19 10:58:40 +08:00
commit ff1dd034a8
20 changed files with 1193 additions and 4 deletions

View File

@ -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

31
bin/clojure-petstore.sh Executable file
View File

@ -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

View File

@ -10,10 +10,10 @@ import java.util.Set;
public class CodegenOperation {
public final List<CodegenProperty> responseHeaders = new ArrayList<CodegenProperty>();
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<Map<String, String>> consumes, produces;

View File

@ -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) {

View File

@ -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<String, Object> postProcessOperations(Map<String, Object> operations) {
Map<String, Object> objs = (Map<String, Object>) operations.get("operations");
List<CodegenOperation> ops = (List<CodegenOperation>) 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("[_ ]", "-");
}
}

View File

@ -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

View File

@ -0,0 +1,19 @@
{{=< >=}}(ns <package>.<classname>
(:require [<projectName>.core :refer [call-api check-required-params]])
(:import (java.io File)))
<#operations><#operation>
(defn <nickname>
"<&summary><#notes>
<&notes></notes>"<#hasOptionalParams>
([<#allParams><#required><#isFile>^File </isFile><paramName> </required></allParams>] (<nickname><#allParams><#required> <paramName></required></allParams> nil))</hasOptionalParams>
<#hasOptionalParams>(</hasOptionalParams>[<#allParams><#required><#isFile>^File </isFile><paramName> </required></allParams><#hasOptionalParams>{:keys [<#allParams><^required><#isFile>^File </isFile><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>

View File

@ -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)))

View File

@ -0,0 +1,8 @@
{{=< >=}}(defproject <&projectName> "<&projectVersion>"
:description "<&projectDescription>"<#projectUrl>
:url "<&projectUrl>"</projectUrl><#licenseName>
:license {:name "<&licenseName>"<#licenseUrl>
:url "<&licenseUrl>"</licenseUrl>}</licenseName>
:dependencies [[org.clojure/clojure "1.7.0"]
[clj-http "2.0.0"]
[cheshire "5.5.0"]])

View File

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

View File

@ -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 <a href=\"http://swagger.io\">http://swagger.io</a> 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"]])

View File

@ -0,0 +1 @@
Hello world!

View File

@ -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"]})))

View File

@ -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"]}))

View File

@ -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"]}))

View File

@ -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)))

View File

@ -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"})))

View File

@ -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))))))

View File

@ -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)))

View File

@ -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"))