Add Clojure client codegen

This commit is contained in:
xhh 2015-11-16 18:00:43 +08:00
parent 96f71e420a
commit cd8cfc50ed
7 changed files with 342 additions and 4 deletions

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

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

View File

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

View File

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

View File

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