mirror of
https://github.com/OpenAPITools/openapi-generator.git
synced 2025-12-11 01:12:43 +00:00
add haskell-http-client-generator (#6429)
This commit is contained in:
@@ -0,0 +1,861 @@
|
||||
package io.swagger.codegen.languages;
|
||||
|
||||
import io.swagger.codegen.*;
|
||||
import io.swagger.models.Model;
|
||||
import io.swagger.models.ModelImpl;
|
||||
import io.swagger.models.Operation;
|
||||
import io.swagger.models.Swagger;
|
||||
import io.swagger.models.parameters.Parameter;
|
||||
import io.swagger.models.properties.ArrayProperty;
|
||||
import io.swagger.models.properties.MapProperty;
|
||||
import io.swagger.models.properties.Property;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import io.swagger.codegen.CliOption;
|
||||
import io.swagger.codegen.CodegenConstants;
|
||||
import io.swagger.codegen.CodegenModel;
|
||||
import io.swagger.codegen.CodegenOperation;
|
||||
import io.swagger.codegen.CodegenProperty;
|
||||
import io.swagger.codegen.SupportingFile;
|
||||
import io.swagger.util.Json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.text.WordUtils;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class HaskellHttpClientCodegen extends DefaultCodegen implements CodegenConfig {
|
||||
|
||||
// source folder where to write the files
|
||||
protected String sourceFolder = "src";
|
||||
protected String apiVersion = "0.0.1";
|
||||
|
||||
protected String artifactId = "swagger-haskell-http-client";
|
||||
protected String artifactVersion = "1.0.0";
|
||||
|
||||
protected String defaultDateTimeFormat = "%Y-%m-%dT%H:%M:%S%Q%z";
|
||||
protected String defaultDateFormat = "%Y-%m-%d";
|
||||
|
||||
// CLI
|
||||
public static final String ALLOW_FROMJSON_NULLS = "allowFromJsonNulls";
|
||||
public static final String ALLOW_TOJSON_NULLS = "allowToJsonNulls";
|
||||
public static final String DATETIME_FORMAT = "dateTimeFormat";
|
||||
public static final String DATE_FORMAT = "dateFormat";
|
||||
public static final String GENERATE_FORM_URLENCODED_INSTANCES = "generateFormUrlEncodedInstances";
|
||||
public static final String GENERATE_LENSES = "generateLenses";
|
||||
public static final String GENERATE_MODEL_CONSTRUCTORS = "generateModelConstructors";
|
||||
public static final String MODEL_DERIVING = "modelDeriving";
|
||||
|
||||
// protected String MODEL_IMPORTS = "modelImports";
|
||||
// protected String MODEL_EXTENSIONS = "modelExtensions";
|
||||
|
||||
private static final Pattern LEADING_UNDERSCORE = Pattern.compile("^_+");
|
||||
|
||||
static final String MEDIA_TYPE = "mediaType";
|
||||
static final String MEDIA_DATA_TYPE = "x-mediaDataType";
|
||||
|
||||
|
||||
protected Map<String, CodegenParameter> uniqueOptionalParamsByName = new HashMap<String, CodegenParameter>();
|
||||
protected Map<String, CodegenModel> modelNames = new HashMap<String, CodegenModel>();
|
||||
protected Map<String, Map<String,String>> allMimeTypes = new HashMap<String, Map<String,String>>();
|
||||
protected Map<String, String> knownMimeDataTypes = new HashMap<String, String>();
|
||||
protected Map<String, Set<String>> modelMimeTypes = new HashMap<String, Set<String>>();
|
||||
protected String lastTag = "";
|
||||
protected ArrayList<Map<String,String>> unknownMimeTypes = new ArrayList<Map<String,String>>();
|
||||
|
||||
public CodegenType getTag() {
|
||||
return CodegenType.CLIENT;
|
||||
}
|
||||
public String getName() {
|
||||
return "haskell-http-client";
|
||||
}
|
||||
public String getHelp() {
|
||||
return "Generates a Haskell http-client library.";
|
||||
}
|
||||
|
||||
|
||||
public HaskellHttpClientCodegen() {
|
||||
super();
|
||||
|
||||
// override the mapping to keep the original mapping in Haskell
|
||||
specialCharReplacements.put("-", "Dash");
|
||||
specialCharReplacements.put(">", "GreaterThan");
|
||||
specialCharReplacements.put("<", "LessThan");
|
||||
|
||||
// backslash and double quote need double the escapement for both Java and Haskell
|
||||
specialCharReplacements.remove("\\");
|
||||
specialCharReplacements.remove("\"");
|
||||
specialCharReplacements.put("\\\\", "Back_Slash");
|
||||
specialCharReplacements.put("\\\"", "Double_Quote");
|
||||
|
||||
// set the output folder here
|
||||
outputFolder = "generated-code/haskell-http-client";
|
||||
|
||||
embeddedTemplateDir = templateDir = "haskell-http-client";
|
||||
apiPackage = "API";
|
||||
modelPackage = "Model";
|
||||
|
||||
// Haskell keywords and reserved function names, taken mostly from https://wiki.haskell.org/Keywords
|
||||
setReservedWordsLowerCase(
|
||||
Arrays.asList(
|
||||
// Keywords
|
||||
"as", "case", "of",
|
||||
"class", "data", "family",
|
||||
"default", "deriving",
|
||||
"do", "forall", "foreign", "hiding",
|
||||
"if", "then", "else",
|
||||
"import", "infix", "infixl", "infixr",
|
||||
"instance", "let", "in",
|
||||
"mdo", "module", "newtype",
|
||||
"proc", "qualified", "rec",
|
||||
"type", "where"
|
||||
)
|
||||
);
|
||||
|
||||
additionalProperties.put("apiVersion", apiVersion);
|
||||
additionalProperties.put("artifactId", artifactId);
|
||||
additionalProperties.put("artifactVersion", artifactVersion);
|
||||
|
||||
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
|
||||
supportingFiles.add(new SupportingFile("stack.mustache", "", "stack.yaml"));
|
||||
supportingFiles.add(new SupportingFile("Setup.mustache", "", "Setup.hs"));
|
||||
supportingFiles.add(new SupportingFile(".gitignore", "", ".gitignore"));
|
||||
supportingFiles.add(new SupportingFile(".travis.yml", "", ".travis.yml"));
|
||||
supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh"));
|
||||
|
||||
supportingFiles.add(new SupportingFile("tests/ApproxEq.mustache", "tests", "ApproxEq.hs"));
|
||||
supportingFiles.add(new SupportingFile("tests/Instances.mustache", "tests", "Instances.hs"));
|
||||
supportingFiles.add(new SupportingFile("tests/PropMime.mustache", "tests", "PropMime.hs"));
|
||||
supportingFiles.add(new SupportingFile("tests/Test.mustache", "tests", "Test.hs"));
|
||||
|
||||
languageSpecificPrimitives = new HashSet<String>(
|
||||
Arrays.asList(
|
||||
"Bool",
|
||||
"String",
|
||||
"Int",
|
||||
"Integer",
|
||||
"Float",
|
||||
"Char",
|
||||
"Double",
|
||||
"List",
|
||||
"FilePath"
|
||||
)
|
||||
);
|
||||
|
||||
typeMapping.clear();
|
||||
// typeMapping.put("array", "List");
|
||||
typeMapping.put("set", "Set");
|
||||
typeMapping.put("boolean", "Bool");
|
||||
typeMapping.put("string", "Text");
|
||||
typeMapping.put("int", "Int");
|
||||
typeMapping.put("long", "Integer");
|
||||
typeMapping.put("short", "Int");
|
||||
typeMapping.put("char", "Char");
|
||||
typeMapping.put("float", "Float");
|
||||
typeMapping.put("double", "Double");
|
||||
typeMapping.put("Date", "Day");
|
||||
typeMapping.put("DateTime", "UTCTime");
|
||||
typeMapping.put("file", "FilePath");
|
||||
typeMapping.put("number", "Double");
|
||||
typeMapping.put("integer", "Int");
|
||||
typeMapping.put("any", "Value");
|
||||
typeMapping.put("UUID", "Text");
|
||||
typeMapping.put("binary", "ByteString");
|
||||
typeMapping.put("ByteArray", "ByteString");
|
||||
|
||||
knownMimeDataTypes.put("application/json", "MimeJSON");
|
||||
knownMimeDataTypes.put("application/xml", "MimeXML");
|
||||
knownMimeDataTypes.put("application/x-www-form-urlencoded", "MimeFormUrlEncoded");
|
||||
knownMimeDataTypes.put("application/octet-stream", "MimeOctetStream");
|
||||
knownMimeDataTypes.put("multipart/form-data", "MimeMultipartFormData");
|
||||
knownMimeDataTypes.put("text/plain", "MimePlainText");
|
||||
|
||||
importMapping.clear();
|
||||
importMapping.put("Map", "qualified Data.Map as Map");
|
||||
|
||||
cliOptions.add(new CliOption(CodegenConstants.MODEL_PACKAGE, CodegenConstants.MODEL_PACKAGE_DESC));
|
||||
cliOptions.add(new CliOption(CodegenConstants.API_PACKAGE, CodegenConstants.API_PACKAGE_DESC));
|
||||
|
||||
cliOptions.add(new CliOption(ALLOW_FROMJSON_NULLS, "allow JSON Null during model decoding from JSON").defaultValue(Boolean.TRUE.toString()));
|
||||
cliOptions.add(new CliOption(ALLOW_TOJSON_NULLS, "allow emitting JSON Null during model encoding to JSON").defaultValue(Boolean.FALSE.toString()));
|
||||
cliOptions.add(new CliOption(GENERATE_LENSES, "Generate Lens optics for Models").defaultValue(Boolean.TRUE.toString()));
|
||||
cliOptions.add(new CliOption(GENERATE_MODEL_CONSTRUCTORS, "Generate smart constructors (only supply required fields) for models").defaultValue(Boolean.TRUE.toString()));
|
||||
cliOptions.add(new CliOption(GENERATE_FORM_URLENCODED_INSTANCES, "Generate FromForm/ToForm instances for models that are used by operations that produce or consume application/x-www-form-urlencoded").defaultValue(Boolean.TRUE.toString()));
|
||||
|
||||
cliOptions.add(new CliOption(MODEL_DERIVING, "Additional classes to include in the deriving() clause of Models"));
|
||||
|
||||
cliOptions.add(new CliOption(DATETIME_FORMAT, "format string used to parse/render a datetime").defaultValue(defaultDateTimeFormat));
|
||||
cliOptions.add(new CliOption(DATE_FORMAT, "format string used to parse/render a date").defaultValue(defaultDateFormat));
|
||||
|
||||
cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP, "hides the timestamp when files were generated").defaultValue(Boolean.TRUE.toString()));
|
||||
|
||||
// cliOptions.add(new CliOption(MODEL_IMPORTS, "Additional imports in the Models file"));
|
||||
// cliOptions.add(new CliOption(MODEL_EXTENSIONS, "Additional extensions in the Models file"));
|
||||
}
|
||||
|
||||
public void setAllowFromJsonNulls(Boolean value) {
|
||||
additionalProperties.put(ALLOW_FROMJSON_NULLS, value);
|
||||
}
|
||||
|
||||
public void setAllowToJsonNulls(Boolean value) {
|
||||
additionalProperties.put(ALLOW_TOJSON_NULLS, value);
|
||||
}
|
||||
|
||||
public void setGenerateModelConstructors(Boolean value) {
|
||||
additionalProperties.put(GENERATE_MODEL_CONSTRUCTORS, value);
|
||||
}
|
||||
|
||||
public void setGenerateFormUrlEncodedInstances(Boolean value) {
|
||||
additionalProperties.put(GENERATE_FORM_URLENCODED_INSTANCES, value);
|
||||
}
|
||||
|
||||
public void setGenerateLenses(Boolean value) {
|
||||
additionalProperties.put(GENERATE_LENSES, value);
|
||||
}
|
||||
|
||||
public void setModelDeriving(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
additionalProperties.remove(MODEL_DERIVING);
|
||||
} else {
|
||||
additionalProperties.put(MODEL_DERIVING, StringUtils.join(value.split(" "), ","));
|
||||
}
|
||||
}
|
||||
|
||||
public void setDateTimeFormat(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
additionalProperties.remove(DATETIME_FORMAT);
|
||||
} else {
|
||||
additionalProperties.put(DATETIME_FORMAT, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setDateFormat(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
additionalProperties.remove(DATE_FORMAT);
|
||||
} else {
|
||||
additionalProperties.put(DATE_FORMAT, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processOpts() {
|
||||
super.processOpts();
|
||||
// default HIDE_GENERATION_TIMESTAMP to true
|
||||
if (additionalProperties.containsKey(CodegenConstants.HIDE_GENERATION_TIMESTAMP)) {
|
||||
convertPropertyToBooleanAndWriteBack(CodegenConstants.HIDE_GENERATION_TIMESTAMP);
|
||||
} else {
|
||||
additionalProperties.put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, true);
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(ALLOW_FROMJSON_NULLS)) {
|
||||
setAllowFromJsonNulls(convertPropertyToBoolean(ALLOW_FROMJSON_NULLS));
|
||||
} else {
|
||||
setAllowFromJsonNulls(true);
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(ALLOW_TOJSON_NULLS)) {
|
||||
setAllowToJsonNulls(convertPropertyToBoolean(ALLOW_TOJSON_NULLS));
|
||||
} else {
|
||||
setAllowToJsonNulls(false);
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(GENERATE_MODEL_CONSTRUCTORS)) {
|
||||
setGenerateModelConstructors(convertPropertyToBoolean(GENERATE_MODEL_CONSTRUCTORS));
|
||||
} else {
|
||||
setGenerateModelConstructors(true);
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(GENERATE_FORM_URLENCODED_INSTANCES)) {
|
||||
setGenerateFormUrlEncodedInstances(convertPropertyToBoolean(GENERATE_FORM_URLENCODED_INSTANCES));
|
||||
} else {
|
||||
setGenerateFormUrlEncodedInstances(true);
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(GENERATE_LENSES)) {
|
||||
setGenerateLenses(convertPropertyToBoolean(GENERATE_LENSES));
|
||||
} else {
|
||||
setGenerateLenses(true);
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(MODEL_DERIVING)) {
|
||||
setModelDeriving(additionalProperties.get(MODEL_DERIVING).toString());
|
||||
} else {
|
||||
setModelDeriving("");
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(DATETIME_FORMAT)) {
|
||||
setDateTimeFormat(additionalProperties.get(DATETIME_FORMAT).toString());
|
||||
} else {
|
||||
setDateTimeFormat(null);
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(DATE_FORMAT)) {
|
||||
setDateFormat(additionalProperties.get(DATE_FORMAT).toString());
|
||||
} else {
|
||||
setDateFormat(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public String apiFileFolder() {
|
||||
// String apiName = (String)additionalProperties.get("title");
|
||||
// return outputFolder + File.separator + "lib/" + apiName;
|
||||
// }
|
||||
// @Override
|
||||
// public String modelFileFolder() {
|
||||
// String apiName = (String)additionalProperties.get("title");
|
||||
// return outputFolder + File.separator + "lib/" + apiName;
|
||||
// }
|
||||
|
||||
@Override
|
||||
public void preprocessSwagger(Swagger swagger) {
|
||||
// From the title, compute a reasonable name for the package and the API
|
||||
String title = swagger.getInfo().getTitle();
|
||||
|
||||
// Drop any API suffix
|
||||
if (title == null) {
|
||||
title = "Swagger";
|
||||
} else {
|
||||
title = title.trim();
|
||||
if (title.toUpperCase().endsWith("API")) {
|
||||
title = title.substring(0, title.length() - 3);
|
||||
}
|
||||
}
|
||||
|
||||
String[] words = title.split(" ");
|
||||
|
||||
// The package name is made by appending the lowercased words of the title interspersed with dashes
|
||||
List<String> wordsLower = new ArrayList<String>();
|
||||
for (String word : words) {
|
||||
wordsLower.add(word.toLowerCase());
|
||||
}
|
||||
String cabalName = StringUtils.join(wordsLower, "-");
|
||||
String pathsName = StringUtils.join(wordsLower, "_");
|
||||
|
||||
// The API name is made by appending the capitalized words of the title
|
||||
List<String> wordsCaps = new ArrayList<String>();
|
||||
for (String word : words) {
|
||||
wordsCaps.add(firstLetterToUpper(word));
|
||||
}
|
||||
String apiName = StringUtils.join(wordsCaps, "");
|
||||
|
||||
// Set the filenames to write for the API
|
||||
|
||||
// root
|
||||
supportingFiles.add(new SupportingFile("haskell-http-client.cabal.mustache", "", cabalName + ".cabal"));
|
||||
|
||||
supportingFiles.add(new SupportingFile("haskell-http-client.cabal.mustache", "", cabalName + ".cabal"));
|
||||
supportingFiles.add(new SupportingFile("package.mustache", "", "package.yaml"));
|
||||
|
||||
// lib
|
||||
supportingFiles.add(new SupportingFile("TopLevel.mustache", "lib/", apiName + ".hs"));
|
||||
supportingFiles.add(new SupportingFile("Client.mustache", "lib/" + apiName, "Client.hs"));
|
||||
|
||||
supportingFiles.add(new SupportingFile("API.mustache", "lib/" + apiName, "API.hs"));
|
||||
supportingFiles.add(new SupportingFile("Model.mustache", "lib/" + apiName, "Model.hs"));
|
||||
supportingFiles.add(new SupportingFile("MimeTypes.mustache", "lib/" + apiName, "MimeTypes.hs"));
|
||||
|
||||
// modelTemplateFiles.put("API.mustache", ".hs");
|
||||
// apiTemplateFiles.put("Model.mustache", ".hs");
|
||||
|
||||
// lens
|
||||
if ((boolean)additionalProperties.get(GENERATE_LENSES)) {
|
||||
supportingFiles.add(new SupportingFile("Lens.mustache", "lib/" + apiName, "Lens.hs"));
|
||||
}
|
||||
|
||||
additionalProperties.put("title", apiName);
|
||||
additionalProperties.put("titleLower", firstLetterToLower(apiName));
|
||||
additionalProperties.put("package", cabalName);
|
||||
additionalProperties.put("pathsName", pathsName);
|
||||
additionalProperties.put("requestType", apiName + "Request");
|
||||
additionalProperties.put("configType", apiName + "Config");
|
||||
additionalProperties.put("swaggerVersion", swagger.getSwagger());
|
||||
|
||||
// prepend '
|
||||
// List<Map<String, Object>> replacements = new ArrayList<>();
|
||||
// Object[] replacementChars = specialCharReplacements.keySet().toArray();
|
||||
// for (int i = 0; i < replacementChars.length; i++) {
|
||||
// String c = (String) replacementChars[i];
|
||||
// Map<String, Object> o = new HashMap<>();
|
||||
// o.put("char", c);
|
||||
// o.put("replacement", "'" + specialCharReplacements.get(c));
|
||||
// o.put("hasMore", i != replacementChars.length - 1);
|
||||
// replacements.add(o);
|
||||
// }
|
||||
// additionalProperties.put("specialCharReplacements", replacements);
|
||||
|
||||
//copy input swagger to output folder
|
||||
try {
|
||||
String swaggerJson = Json.pretty(swagger);
|
||||
FileUtils.writeStringToFile(new File(outputFolder + File.separator + "swagger.json"), swaggerJson);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e.getMessage(), e.getCause());
|
||||
}
|
||||
|
||||
super.preprocessSwagger(swagger);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getTypeDeclaration(Property p) {
|
||||
if (p instanceof ArrayProperty) {
|
||||
ArrayProperty ap = (ArrayProperty) p;
|
||||
Property inner = ap.getItems();
|
||||
return "[" + getTypeDeclaration(inner) + "]";
|
||||
} else if (p instanceof MapProperty) {
|
||||
MapProperty mp = (MapProperty) p;
|
||||
Property inner = mp.getAdditionalProperties();
|
||||
return "Map.Map String " + getTypeDeclaration(inner);
|
||||
}
|
||||
return fixModelChars(super.getTypeDeclaration(p));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSwaggerType(Property p) {
|
||||
String swaggerType = super.getSwaggerType(p);
|
||||
String type = null;
|
||||
if (typeMapping.containsKey(swaggerType)) {
|
||||
type = typeMapping.get(swaggerType);
|
||||
if (languageSpecificPrimitives.contains(type))
|
||||
return toModelName(type);
|
||||
} else if (swaggerType == "object") {
|
||||
type = "Value";
|
||||
} else if (typeMapping.containsValue(swaggerType)) {
|
||||
type = swaggerType + "_";
|
||||
} else {
|
||||
type = swaggerType;
|
||||
}
|
||||
return toModelName(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toInstantiationType(Property p) {
|
||||
if (p instanceof MapProperty) {
|
||||
MapProperty ap = (MapProperty) p;
|
||||
Property additionalProperties2 = ap.getAdditionalProperties();
|
||||
String type = additionalProperties2.getType();
|
||||
if (null == type) {
|
||||
LOGGER.error("No Type defined for Additional Property " + additionalProperties2 + "\n" //
|
||||
+ "\tIn Property: " + p);
|
||||
}
|
||||
String inner = getSwaggerType(additionalProperties2);
|
||||
return "(Map.Map Text " + inner + ")";
|
||||
} else if (p instanceof ArrayProperty) {
|
||||
ArrayProperty ap = (ArrayProperty) p;
|
||||
String inner = getSwaggerType(ap.getItems());
|
||||
// Return only the inner type; the wrapping with QueryList is done
|
||||
// somewhere else, where we have access to the collection format.
|
||||
return inner;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodegenOperation fromOperation(String resourcePath, String httpMethod, Operation operation, Map<String, Model> definitions, Swagger swagger) {
|
||||
CodegenOperation op = super.fromOperation(resourcePath, httpMethod, operation, definitions, swagger);
|
||||
|
||||
op.vendorExtensions.put("x-baseOperationId", op.operationId);
|
||||
op.vendorExtensions.put("x-haddockPath", String.format("%s %s", op.httpMethod, op.path.replace("/", "\\/")));
|
||||
op.operationId = toHsVarName(op.operationId);
|
||||
op.vendorExtensions.put("x-operationType", toHsTypeName(op.operationId));
|
||||
op.vendorExtensions.put("x-hasBodyOrFormParam", op.getHasBodyParam() || op.getHasFormParams());
|
||||
|
||||
for (CodegenParameter param : op.allParams) {
|
||||
param.vendorExtensions.put("x-operationType", capitalize(op.operationId));
|
||||
param.vendorExtensions.put("x-isBodyOrFormParam", param.isBodyParam || param.isFormParam);
|
||||
if (!StringUtils.isBlank(param.collectionFormat)) {
|
||||
param.vendorExtensions.put("x-collectionFormat", mapCollectionFormat(param.collectionFormat));
|
||||
}
|
||||
if (!param.required) {
|
||||
op.vendorExtensions.put("x-hasOptionalParams", true);
|
||||
|
||||
String paramNameType = capitalize(param.paramName);
|
||||
|
||||
if (uniqueOptionalParamsByName.containsKey(paramNameType)) {
|
||||
CodegenParameter lastParam = this.uniqueOptionalParamsByName.get(paramNameType);
|
||||
if (lastParam.dataType != null && lastParam.dataType.equals(param.dataType)) {
|
||||
param.vendorExtensions.put("x-duplicate", true);
|
||||
} else {
|
||||
paramNameType = paramNameType + param.dataType;
|
||||
while (modelNames.containsKey(paramNameType)) {
|
||||
paramNameType = generateNextName(paramNameType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (modelNames.containsKey(paramNameType)) {
|
||||
paramNameType = generateNextName(paramNameType);
|
||||
}
|
||||
uniqueOptionalParamsByName.put(paramNameType, param);
|
||||
}
|
||||
|
||||
param.vendorExtensions.put("x-paramNameType", paramNameType);
|
||||
op.vendorExtensions.put("x-hasBodyOrFormParam", op.getHasBodyParam() || op.getHasFormParams());
|
||||
}
|
||||
}
|
||||
if (op.getHasPathParams()) {
|
||||
String remainingPath = op.path;
|
||||
for (CodegenParameter param : op.pathParams) {
|
||||
param.paramName = toHsVarName(param.paramName);
|
||||
String[] pieces = remainingPath.split("\\{" + param.baseName + "\\}");
|
||||
if (pieces.length == 0)
|
||||
throw new RuntimeException("paramName {" + param.baseName + "} not in path " + op.path);
|
||||
if (pieces.length > 2)
|
||||
throw new RuntimeException("paramName {" + param.baseName + "} found multiple times in path " + op.path);
|
||||
if (pieces.length == 2) {
|
||||
param.vendorExtensions.put("x-pathPrefix", pieces[0]);
|
||||
remainingPath = pieces[1];
|
||||
} else {
|
||||
if (remainingPath.startsWith("{" + param.baseName + "}")) {
|
||||
remainingPath = pieces[0];
|
||||
} else {
|
||||
param.vendorExtensions.put("x-pathPrefix", pieces[0]);
|
||||
remainingPath = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
op.vendorExtensions.put("x-hasPathParams", true);
|
||||
if (remainingPath.length() > 0) {
|
||||
op.vendorExtensions.put("x-pathSuffix", remainingPath);
|
||||
}
|
||||
} else {
|
||||
op.vendorExtensions.put("x-hasPathParams", false);
|
||||
op.vendorExtensions.put("x-pathSuffix", op.path);
|
||||
}
|
||||
for (CodegenParameter param : op.queryParams) {
|
||||
}
|
||||
for (CodegenParameter param : op.headerParams) {
|
||||
}
|
||||
for (CodegenParameter param : op.bodyParams) {
|
||||
}
|
||||
for (CodegenParameter param : op.formParams) {
|
||||
}
|
||||
|
||||
if (op.hasConsumes) {
|
||||
for (Map<String, String> m : op.consumes) {
|
||||
processMediaType(op,m);
|
||||
}
|
||||
if (isMultipart(op.consumes)) {
|
||||
op.isMultipart = Boolean.TRUE;
|
||||
}
|
||||
}
|
||||
if (op.hasProduces) {
|
||||
for (Map<String, String> m : op.produces) {
|
||||
processMediaType(op,m);
|
||||
}
|
||||
}
|
||||
|
||||
String returnType = op.returnType;
|
||||
if (returnType == null || returnType.equals("null")) {
|
||||
if(op.hasProduces) {
|
||||
returnType = "res";
|
||||
op.vendorExtensions.put("x-hasUnknownReturn", true);
|
||||
} else {
|
||||
returnType = "NoContent";
|
||||
}
|
||||
}
|
||||
if (returnType.indexOf(" ") >= 0) {
|
||||
returnType = "(" + returnType + ")";
|
||||
}
|
||||
op.vendorExtensions.put("x-returnType", returnType);
|
||||
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> postProcessOperations(Map<String, Object> objs) {
|
||||
Map<String, Object> ret = super.postProcessOperations(objs);
|
||||
|
||||
HashMap<String, Object> pathOps = (HashMap<String, Object>)ret.get("operations");
|
||||
ArrayList<CodegenOperation> ops = (ArrayList<CodegenOperation>)pathOps.get("operation");
|
||||
if(ops.size() > 0) {
|
||||
ops.get(0).vendorExtensions.put("x-hasNewTag", true);
|
||||
}
|
||||
|
||||
additionalProperties.put("x-hasUnknownMimeTypes", !unknownMimeTypes.isEmpty());
|
||||
additionalProperties.put("x-unknownMimeTypes", unknownMimeTypes);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> objs, List<Object> allModels) {
|
||||
for (Object o : allModels) {
|
||||
HashMap<String, Object> h = (HashMap<String, Object>) o;
|
||||
CodegenModel m = (CodegenModel) h.get("model");
|
||||
if (modelMimeTypes.containsKey(m.classname)) {
|
||||
Set<String> mimeTypes = modelMimeTypes.get(m.classname);
|
||||
m.vendorExtensions.put("x-mimeTypes", mimeTypes);
|
||||
if ((boolean)additionalProperties.get(GENERATE_FORM_URLENCODED_INSTANCES) && mimeTypes.contains("MimeFormUrlEncoded")) {
|
||||
Boolean hasMimeFormUrlEncoded = true;
|
||||
for (CodegenProperty v : m.vars) {
|
||||
if (!(v.isPrimitiveType || v.isString || v.isDate || v.isDateTime)) {
|
||||
hasMimeFormUrlEncoded = false;
|
||||
}
|
||||
}
|
||||
if (hasMimeFormUrlEncoded) {
|
||||
m.vendorExtensions.put("x-hasMimeFormUrlEncoded", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodegenModel fromModel(String name, Model mod, Map<String, Model> allDefinitions) {
|
||||
CodegenModel model = super.fromModel(name, mod, allDefinitions);
|
||||
|
||||
// Clean up the class name to remove invalid characters
|
||||
model.classname = fixModelChars(model.classname);
|
||||
if (typeMapping.containsValue(model.classname)) {
|
||||
model.classname += "_";
|
||||
}
|
||||
while (uniqueOptionalParamsByName.containsKey(model.classname)) {
|
||||
model.classname = generateNextName(model.classname);
|
||||
}
|
||||
|
||||
// From the model name, compute the prefix for the fields.
|
||||
String prefix = camelize(model.classname, true);
|
||||
for (CodegenProperty prop : model.vars) {
|
||||
prop.name = toVarName(prefix + camelize(fixOperatorChars(prop.name)));
|
||||
}
|
||||
|
||||
//String dataOrNewtype = "data";
|
||||
// check if it's a ModelImpl before casting
|
||||
if (!(mod instanceof ModelImpl)) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// Create newtypes for things with non-object types
|
||||
// String modelType = ((ModelImpl) mod).getType();
|
||||
// if(modelType != "object" && typeMapping.containsKey(modelType)) {
|
||||
// String newtype = typeMapping.get(modelType);
|
||||
// model.vendorExtensions.put("x-customNewtype", newtype);
|
||||
// }
|
||||
|
||||
modelNames.put(model.classname, model);
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodegenParameter fromParameter(Parameter param, Set<String> imports) {
|
||||
CodegenParameter p = super.fromParameter(param, imports);
|
||||
p.paramName = toHsVarName(p.baseName);
|
||||
p.dataType = fixModelChars(p.dataType);
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String escapeReservedWord(String name) {
|
||||
if (this.reservedWordsMappings().containsKey(name)) {
|
||||
return this.reservedWordsMappings().get(name);
|
||||
}
|
||||
return "_" + name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toModelFilename(String name) {
|
||||
// should be the same as the model name
|
||||
return toModelName(name);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String escapeQuotationMark(String input) {
|
||||
// remove " to avoid code injection
|
||||
return input.replace("\"", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String escapeUnsafeCharacters(String input) {
|
||||
return input.replace("{-", "{_-").replace("-}", "-_}");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDataTypeFile(String dataType) {
|
||||
return dataType != null && dataType.equals("FilePath");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDataTypeBinary(final String dataType) {
|
||||
return dataType != null && dataType.equals("B.ByteString");
|
||||
}
|
||||
|
||||
private void processMediaType(CodegenOperation op, Map<String, String> m) {
|
||||
String mediaType = m.get(MEDIA_TYPE);
|
||||
|
||||
if(StringUtils.isBlank(mediaType)) return;
|
||||
|
||||
String[] mediaTypeParts = mediaType.split("/",2);
|
||||
if(mediaTypeParts.length > 1) {
|
||||
m.put("x-mediaMainType", mediaTypeParts[0]);
|
||||
m.put("x-mediaSubType", mediaTypeParts[1]);
|
||||
} else {
|
||||
m.put("x-mediaMainType", mediaTypeParts[0]);
|
||||
m.put("x-mediaSubType", "");
|
||||
}
|
||||
|
||||
String mimeType = getMimeDataType(mediaType);
|
||||
m.put(MEDIA_DATA_TYPE, mimeType);
|
||||
|
||||
allMimeTypes.put(mediaType, m);
|
||||
if(!knownMimeDataTypes.containsKey(mediaType) && !unknownMimeTypes.contains(m)) {
|
||||
unknownMimeTypes.add(m);
|
||||
}
|
||||
for (CodegenParameter param : op.allParams) {
|
||||
if (param.isBodyParam || param.isFormParam && (!param.isPrimitiveType && !param.isListContainer && !param.isMapContainer)) {
|
||||
Set<String> mimeTypes = modelMimeTypes.containsKey(param.dataType) ? modelMimeTypes.get(param.dataType) : new HashSet();
|
||||
mimeTypes.add(mimeType);
|
||||
modelMimeTypes.put(param.dataType, mimeTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String firstLetterToUpper(String word) {
|
||||
if (word.length() == 0) {
|
||||
return word;
|
||||
} else if (word.length() == 1) {
|
||||
return word.substring(0, 1).toUpperCase();
|
||||
} else {
|
||||
return word.substring(0, 1).toUpperCase() + word.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
public String firstLetterToLower(String word) {
|
||||
if (word.length() == 0) {
|
||||
return word;
|
||||
} else if (word.length() == 1) {
|
||||
return word.substring(0, 1).toLowerCase();
|
||||
} else {
|
||||
return word.substring(0, 1).toLowerCase() + word.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
private String mapCollectionFormat(String collectionFormat) {
|
||||
switch (collectionFormat) {
|
||||
case "csv":
|
||||
return "CommaSeparated";
|
||||
case "tsv":
|
||||
return "TabSeparated";
|
||||
case "ssv":
|
||||
return "SpaceSeparated";
|
||||
case "pipes":
|
||||
return "PipeSeparated";
|
||||
case "multi":
|
||||
return "MultiParamArray";
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private String getMimeDataType(String mimeType) {
|
||||
if (StringUtils.isBlank(mimeType)) {
|
||||
return "MimeNoContent";
|
||||
}
|
||||
if (knownMimeDataTypes.containsKey(mimeType)) {
|
||||
return knownMimeDataTypes.get(mimeType);
|
||||
}
|
||||
String shortenedName = mimeType.replaceFirst("application/","");
|
||||
return "Mime" + toHsTypeName(shortenedName);
|
||||
}
|
||||
|
||||
private String toHsVarName(String paramName) {
|
||||
return toVarName(camelize(fixOperatorChars(fixModelChars(paramName)), true));
|
||||
}
|
||||
|
||||
private String toHsTypeName(String paramName) {
|
||||
return toHsTypeName(paramName, "");
|
||||
}
|
||||
|
||||
private String toHsTypeName(String paramName, String modelCharReplacement) {
|
||||
return camelize(fixOperatorChars(fixModelChars(paramName, modelCharReplacement)), false);
|
||||
}
|
||||
|
||||
private String fixOperatorChars(String string) {
|
||||
if(string == null) return null;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String name = string;
|
||||
//Check if it is a reserved word, in which case the underscore is added when property name is generated.
|
||||
if (string.startsWith("_")) {
|
||||
if (reservedWords.contains(string.substring(1, string.length()))) {
|
||||
name = string.substring(1, string.length());
|
||||
} else if (reservedWordsMappings.containsValue(string)) {
|
||||
name = LEADING_UNDERSCORE.matcher(string).replaceFirst("");
|
||||
}
|
||||
}
|
||||
// prepend '
|
||||
for (char c : name.toCharArray()) {
|
||||
String cString = String.valueOf(c);
|
||||
if (specialCharReplacements.containsKey(cString)) {
|
||||
// sb.append("'");
|
||||
sb.append(specialCharReplacements.get(cString));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// Remove characters from a string that do not belong in a model classname
|
||||
private String fixModelChars(String string, String replacement) {
|
||||
if(string == null) return null;
|
||||
return string.replace(".", replacement).replace("-", replacement);
|
||||
}
|
||||
|
||||
private String fixModelChars(String string) {
|
||||
return fixModelChars(string, "");
|
||||
}
|
||||
|
||||
private String capitalize(String word) {
|
||||
if(word == null) return null;
|
||||
if (word.length() > 0) {
|
||||
word = word.substring(0, 1).toUpperCase() + word.substring(1);
|
||||
}
|
||||
|
||||
return word;
|
||||
}
|
||||
private static String generateNextName(String name) {
|
||||
Pattern pattern = Pattern.compile("\\d+\\z");
|
||||
Matcher matcher = pattern.matcher(name);
|
||||
if (matcher.find()) {
|
||||
String numStr = matcher.group();
|
||||
int num = Integer.parseInt(numStr) + 1;
|
||||
return name.substring(0, name.length() - numStr.length()) + num;
|
||||
} else {
|
||||
return name + "2";
|
||||
}
|
||||
}
|
||||
private static boolean isMultipart(List<Map<String, String>> consumes) {
|
||||
for(Map<String, String> consume : consumes) {
|
||||
if (consume != null) {
|
||||
if ("multipart/form-data".equals(consume.get(MEDIA_TYPE))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// private boolean isModelledType(CodegenParameter param) {
|
||||
// return isModelledType(param.baseType == null ? param.dataType : param.baseType);
|
||||
// }
|
||||
//
|
||||
// private boolean isModelledType(String typeName) {
|
||||
// return !languageSpecificPrimitives.contains(typeName) && !typeMapping.values().contains(typeName);
|
||||
// }
|
||||
}
|
||||
@@ -20,6 +20,7 @@ io.swagger.codegen.languages.FlaskConnexionCodegen
|
||||
io.swagger.codegen.languages.GoClientCodegen
|
||||
io.swagger.codegen.languages.GoServerCodegen
|
||||
io.swagger.codegen.languages.GroovyClientCodegen
|
||||
io.swagger.codegen.languages.HaskellHttpClientCodegen
|
||||
io.swagger.codegen.languages.HaskellServantCodegen
|
||||
io.swagger.codegen.languages.JMeterCodegen
|
||||
io.swagger.codegen.languages.JavaCXFClientCodegen
|
||||
|
||||
8
modules/swagger-codegen/src/main/resources/haskell-http-client/.gitignore
vendored
Normal file
8
modules/swagger-codegen/src/main/resources/haskell-http-client/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.stack-work
|
||||
src/highlight.js
|
||||
src/style.css
|
||||
dist
|
||||
dist-newstyle
|
||||
cabal.project.local
|
||||
.cabal-sandbox
|
||||
cabal.sandbox.config
|
||||
@@ -0,0 +1,16 @@
|
||||
sudo: false
|
||||
language: c
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libgmp-dev
|
||||
before_install:
|
||||
- mkdir -p ~/.local/bin
|
||||
- export PATH=$HOME/.local/bin:$PATH
|
||||
- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
|
||||
script:
|
||||
- stack --install-ghc --no-haddock-deps haddock
|
||||
- stack test
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.stack
|
||||
@@ -0,0 +1,298 @@
|
||||
{-|
|
||||
Module : {{title}}.API
|
||||
-}
|
||||
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unused-binds -fno-warn-unused-imports #-}
|
||||
|
||||
module {{title}}.API where
|
||||
|
||||
|
||||
import {{title}}.Model as M
|
||||
import {{title}}.MimeTypes
|
||||
|
||||
import qualified Data.Aeson as A
|
||||
import Data.Aeson (Value)
|
||||
|
||||
import qualified Data.Time as TI
|
||||
import Data.Time (UTCTime)
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import Data.ByteString.Lazy (ByteString)
|
||||
import qualified Data.ByteString.Builder as BB
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import qualified Data.ByteString.Lazy.Char8 as BCL
|
||||
|
||||
import qualified Network.HTTP.Client.MultipartFormData as NH
|
||||
import qualified Network.HTTP.Media as ME
|
||||
import qualified Network.HTTP.Types as NH
|
||||
|
||||
import qualified Web.HttpApiData as WH
|
||||
import qualified Web.FormUrlEncoded as WH
|
||||
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Data as P (Typeable)
|
||||
import qualified Data.Foldable as P
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Maybe as P
|
||||
import qualified Data.Proxy as P (Proxy(..))
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Data.Text.Lazy as TL
|
||||
import qualified Data.Text.Lazy.Encoding as TL
|
||||
import qualified GHC.Base as P (Alternative)
|
||||
import qualified Control.Arrow as P (left)
|
||||
|
||||
import Data.Monoid ((<>))
|
||||
import Data.Function ((&))
|
||||
import Data.Set (Set)
|
||||
import Data.Text (Text)
|
||||
import GHC.Base ((<|>))
|
||||
|
||||
import Prelude ((==),(/=),($), (.),(<$>),(<*>),(>>=),Maybe(..),Bool(..),Char,Double,FilePath,Float,Int,Integer,String,fmap,undefined,mempty,maybe,pure,Monad,Applicative,Functor)
|
||||
import qualified Prelude as P
|
||||
|
||||
-- * Operations
|
||||
{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}{{#vendorExtensions.x-hasNewTag}}
|
||||
|
||||
-- ** {{baseName}}{{/vendorExtensions.x-hasNewTag}}
|
||||
|
||||
-- *** {{operationId}}
|
||||
|
||||
-- | @{{{vendorExtensions.x-haddockPath}}}@
|
||||
-- {{#summary}}
|
||||
-- {{{.}}}
|
||||
-- {{/summary}}{{#notes}}
|
||||
-- {{{.}}}
|
||||
-- {{/notes}}{{#hasAuthMethods}}
|
||||
-- AuthMethod: {{#authMethods}}{{{name}}}{{#hasMore}}, {{/hasMore}}{{/authMethods}}
|
||||
-- {{/hasAuthMethods}}{{#vendorExtensions.x-hasUnknownReturn}}
|
||||
-- Note: Has 'Produces' instances, but no response schema
|
||||
-- {{/vendorExtensions.x-hasUnknownReturn}}
|
||||
{{operationId}}
|
||||
:: {{#vendorExtensions.x-hasBodyOrFormParam}}(Consumes {{{vendorExtensions.x-operationType}}} contentType{{#allParams}}{{#isBodyParam}}{{#required}}, MimeRender contentType {{dataType}}{{/required}}{{/isBodyParam}}{{/allParams}})
|
||||
=> contentType -- ^ request content-type ('MimeType')
|
||||
-> {{/vendorExtensions.x-hasBodyOrFormParam}}{{#allParams}}{{#required}}{{dataType}} -- ^ "{{{paramName}}}"{{#description}} - {{/description}} {{{description}}}
|
||||
-> {{/required}}{{/allParams}}{{requestType}} {{{vendorExtensions.x-operationType}}} {{#vendorExtensions.x-hasBodyOrFormParam}}contentType{{/vendorExtensions.x-hasBodyOrFormParam}}{{^vendorExtensions.x-hasBodyOrFormParam}}MimeNoContent{{/vendorExtensions.x-hasBodyOrFormParam}} {{vendorExtensions.x-returnType}}
|
||||
{{operationId}} {{#vendorExtensions.x-hasBodyOrFormParam}}_ {{/vendorExtensions.x-hasBodyOrFormParam}}{{#allParams}}{{#required}}{{{paramName}}} {{/required}}{{/allParams}}=
|
||||
_mkRequest "{{httpMethod}}" [{{#pathParams}}{{#vendorExtensions.x-pathPrefix}}"{{.}}",{{/vendorExtensions.x-pathPrefix}}toPath {{{paramName}}}{{#hasMore}},{{/hasMore}}{{/pathParams}}{{#vendorExtensions.x-pathSuffix}}{{#vendorExtensions.x-hasPathParams}},{{/vendorExtensions.x-hasPathParams}}"{{.}}"{{/vendorExtensions.x-pathSuffix}}]{{#allParams}}{{#required}}
|
||||
{{#isHeaderParam}}`setHeader` {{>_headerColl}} ("{{{baseName}}}", {{{paramName}}}){{/isHeaderParam}}{{#isQueryParam}}`_setQuery` {{>_queryColl}} ("{{{baseName}}}", Just {{{paramName}}}){{/isQueryParam}}{{#isFormParam}}{{#isFile}}`_addMultiFormPart` NH.partFileSource "{{{baseName}}}" {{{paramName}}}{{/isFile}}{{^isFile}}{{#isMultipart}}`_addMultiFormPart` NH.partLBS "{{{baseName}}}" (mimeRender' MimeMultipartFormData {{{paramName}}}){{/isMultipart}}{{^isMultipart}}`_addForm` {{>_formColl}} ("{{{baseName}}}", {{{paramName}}}){{/isMultipart}}{{/isFile}}{{/isFormParam}}{{#isBodyParam}}`setBodyParam` {{{paramName}}}{{/isBodyParam}}{{/required}}{{/allParams}}{{#isDeprecated}}
|
||||
|
||||
{-# DEPRECATED {{operationId}} "" #-}{{/isDeprecated}}
|
||||
|
||||
data {{{vendorExtensions.x-operationType}}} {{#allParams}}{{#isBodyParam}}{{#description}}
|
||||
|
||||
-- | /Body Param/ "{{{baseName}}}" - {{{description}}}{{/description}}
|
||||
instance HasBodyParam {{{vendorExtensions.x-operationType}}} {{{dataType}}}{{/isBodyParam}}{{/allParams}} {{#vendorExtensions.x-hasOptionalParams}}{{#allParams}}{{^isBodyParam}}{{^required}}{{#description}}
|
||||
|
||||
-- | /Optional Param/ "{{{baseName}}}" - {{{description}}}{{/description}}
|
||||
instance HasOptionalParam {{{vendorExtensions.x-operationType}}} {{{vendorExtensions.x-paramNameType}}} where
|
||||
applyOptionalParam req ({{{vendorExtensions.x-paramNameType}}} xs) =
|
||||
{{#isHeaderParam}}req `setHeader` {{>_headerColl}} ("{{{baseName}}}", xs){{/isHeaderParam}}{{#isQueryParam}}req `_setQuery` {{>_queryColl}} ("{{{baseName}}}", Just xs){{/isQueryParam}}{{#isFormParam}}{{#isFile}}req `_addMultiFormPart` NH.partFileSource "{{{baseName}}}" xs{{/isFile}}{{^isFile}}{{#isMultipart}}req `_addMultiFormPart` NH.partLBS "{{{baseName}}}" (mimeRender' MimeMultipartFormData xs){{/isMultipart}}{{^isMultipart}}req `_addForm` {{>_formColl}} ("{{{baseName}}}", xs){{/isMultipart}}{{/isFile}}{{/isFormParam}}{{/required}}{{/isBodyParam}}{{/allParams}}{{/vendorExtensions.x-hasOptionalParams}}{{#hasConsumes}}
|
||||
|
||||
{{#consumes}}-- | @{{{mediaType}}}@
|
||||
instance Consumes {{{vendorExtensions.x-operationType}}} {{{x-mediaDataType}}}
|
||||
{{/consumes}}{{/hasConsumes}}{{#hasProduces}}
|
||||
{{#produces}}-- | @{{{mediaType}}}@
|
||||
instance Produces {{{vendorExtensions.x-operationType}}} {{{x-mediaDataType}}}
|
||||
{{/produces}}{{/hasProduces}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
|
||||
|
||||
|
||||
-- * HasBodyParam
|
||||
|
||||
-- | Designates the body parameter of a request
|
||||
class HasBodyParam req param where
|
||||
setBodyParam :: forall contentType res. (Consumes req contentType, MimeRender contentType param) => {{requestType}} req contentType res -> param -> {{requestType}} req contentType res
|
||||
setBodyParam req xs =
|
||||
req `_setBodyLBS` mimeRender (P.Proxy :: P.Proxy contentType) xs & _setContentTypeHeader
|
||||
|
||||
-- * HasOptionalParam
|
||||
|
||||
-- | Designates the optional parameters of a request
|
||||
class HasOptionalParam req param where
|
||||
{-# MINIMAL applyOptionalParam | (-&-) #-}
|
||||
|
||||
-- | Apply an optional parameter to a request
|
||||
applyOptionalParam :: {{requestType}} req contentType res -> param -> {{requestType}} req contentType res
|
||||
applyOptionalParam = (-&-)
|
||||
{-# INLINE applyOptionalParam #-}
|
||||
|
||||
-- | infix operator \/ alias for 'addOptionalParam'
|
||||
(-&-) :: {{requestType}} req contentType res -> param -> {{requestType}} req contentType res
|
||||
(-&-) = applyOptionalParam
|
||||
{-# INLINE (-&-) #-}
|
||||
|
||||
infixl 2 -&-
|
||||
|
||||
-- * Optional Request Parameter Types
|
||||
|
||||
{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}{{#vendorExtensions.x-hasOptionalParams}}{{#allParams}}{{^required}}{{^vendorExtensions.x-duplicate}}
|
||||
newtype {{{vendorExtensions.x-paramNameType}}} = {{{vendorExtensions.x-paramNameType}}} { un{{{vendorExtensions.x-paramNameType}}} :: {{{dataType}}} } deriving (P.Eq, P.Show)
|
||||
{{/vendorExtensions.x-duplicate}}{{/required}}{{/allParams}}{{/vendorExtensions.x-hasOptionalParams}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
|
||||
|
||||
-- * {{requestType}}
|
||||
|
||||
-- | Represents a request. The "req" type variable is the request type. The "res" type variable is the response type.
|
||||
data {{requestType}} req contentType res = {{requestType}}
|
||||
{ rMethod :: NH.Method -- ^ Method of {{requestType}}
|
||||
, urlPath :: [BCL.ByteString] -- ^ Endpoint of {{requestType}}
|
||||
, params :: Params -- ^ params of {{requestType}}
|
||||
}
|
||||
deriving (P.Show)
|
||||
|
||||
-- | Request Params
|
||||
data Params = Params
|
||||
{ paramsQuery :: NH.Query
|
||||
, paramsHeaders :: NH.RequestHeaders
|
||||
, paramsBody :: ParamBody
|
||||
}
|
||||
deriving (P.Show)
|
||||
|
||||
-- | Request Body
|
||||
data ParamBody
|
||||
= ParamBodyNone
|
||||
| ParamBodyB B.ByteString
|
||||
| ParamBodyBL BL.ByteString
|
||||
| ParamBodyFormUrlEncoded WH.Form
|
||||
| ParamBodyMultipartFormData [NH.Part]
|
||||
deriving (P.Show)
|
||||
|
||||
-- ** {{requestType}} Utils
|
||||
|
||||
_mkRequest :: NH.Method -- ^ Method
|
||||
-> [BCL.ByteString] -- ^ Endpoint
|
||||
-> {{requestType}} req contentType res -- ^ req: Request Type, res: Response Type
|
||||
_mkRequest m u = {{requestType}} m u _mkParams
|
||||
|
||||
_mkParams :: Params
|
||||
_mkParams = Params [] [] ParamBodyNone
|
||||
|
||||
setHeader :: {{requestType}} req contentType res -> [NH.Header] -> {{requestType}} req contentType res
|
||||
setHeader req header =
|
||||
let _params = params (req `removeHeader` P.fmap P.fst header)
|
||||
in req { params = _params { paramsHeaders = header P.++ paramsHeaders _params } }
|
||||
|
||||
removeHeader :: {{requestType}} req contentType res -> [NH.HeaderName] -> {{requestType}} req contentType res
|
||||
removeHeader req header =
|
||||
let _params = params req
|
||||
in req { params = _params { paramsHeaders = [h | h <- paramsHeaders _params, cifst h `P.notElem` P.fmap CI.mk header] } }
|
||||
where cifst = CI.mk . P.fst
|
||||
|
||||
|
||||
_setContentTypeHeader :: forall req contentType res. MimeType contentType => {{requestType}} req contentType res -> {{requestType}} req contentType res
|
||||
_setContentTypeHeader req =
|
||||
case mimeType (P.Proxy :: P.Proxy contentType) of
|
||||
Just m -> req `setHeader` [("content-type", BC.pack $ P.show m)]
|
||||
Nothing -> req `removeHeader` ["content-type"]
|
||||
|
||||
_setAcceptHeader :: forall req contentType res accept. MimeType accept => {{requestType}} req contentType res -> accept -> {{requestType}} req contentType res
|
||||
_setAcceptHeader req accept =
|
||||
case mimeType' accept of
|
||||
Just m -> req `setHeader` [("accept", BC.pack $ P.show m)]
|
||||
Nothing -> req `removeHeader` ["accept"]
|
||||
|
||||
_setQuery :: {{requestType}} req contentType res -> [NH.QueryItem] -> {{requestType}} req contentType res
|
||||
_setQuery req query =
|
||||
let _params = params req
|
||||
in req { params = _params { paramsQuery = query P.++ [q | q <- paramsQuery _params, cifst q `P.notElem` P.fmap cifst query] } }
|
||||
where cifst = CI.mk . P.fst
|
||||
|
||||
_addForm :: {{requestType}} req contentType res -> WH.Form -> {{requestType}} req contentType res
|
||||
_addForm req newform =
|
||||
let _params = params req
|
||||
form = case paramsBody _params of
|
||||
ParamBodyFormUrlEncoded _form -> _form
|
||||
_ -> mempty
|
||||
in req { params = _params { paramsBody = ParamBodyFormUrlEncoded (newform <> form) } }
|
||||
|
||||
_addMultiFormPart :: {{requestType}} req contentType res -> NH.Part -> {{requestType}} req contentType res
|
||||
_addMultiFormPart req newpart =
|
||||
let _params = params req
|
||||
parts = case paramsBody _params of
|
||||
ParamBodyMultipartFormData _parts -> _parts
|
||||
_ -> []
|
||||
in req { params = _params { paramsBody = ParamBodyMultipartFormData (newpart : parts) } }
|
||||
|
||||
_setBodyBS :: {{requestType}} req contentType res -> B.ByteString -> {{requestType}} req contentType res
|
||||
_setBodyBS req body =
|
||||
let _params = params req
|
||||
in req { params = _params { paramsBody = ParamBodyB body } }
|
||||
|
||||
_setBodyLBS :: {{requestType}} req contentType res -> BL.ByteString -> {{requestType}} req contentType res
|
||||
_setBodyLBS req body =
|
||||
let _params = params req
|
||||
in req { params = _params { paramsBody = ParamBodyBL body } }
|
||||
|
||||
|
||||
-- ** Params Utils
|
||||
|
||||
toPath
|
||||
:: WH.ToHttpApiData a
|
||||
=> a -> BCL.ByteString
|
||||
toPath = BB.toLazyByteString . WH.toEncodedUrlPiece
|
||||
|
||||
toHeader :: WH.ToHttpApiData a => (NH.HeaderName, a) -> [NH.Header]
|
||||
toHeader x = [fmap WH.toHeader x]
|
||||
|
||||
toForm :: WH.ToHttpApiData v => (BC.ByteString, v) -> WH.Form
|
||||
toForm (k,v) = WH.toForm [(BC.unpack k,v)]
|
||||
|
||||
toQuery :: WH.ToHttpApiData a => (BC.ByteString, Maybe a) -> [NH.QueryItem]
|
||||
toQuery x = [(fmap . fmap) toQueryParam x]
|
||||
where toQueryParam = T.encodeUtf8 . WH.toQueryParam
|
||||
|
||||
-- *** Swagger `CollectionFormat` Utils
|
||||
|
||||
-- | Determines the format of the array if type array is used.
|
||||
data CollectionFormat
|
||||
= CommaSeparated -- ^ CSV format for multiple parameters.
|
||||
| SpaceSeparated -- ^ Also called "SSV"
|
||||
| TabSeparated -- ^ Also called "TSV"
|
||||
| PipeSeparated -- ^ `value1|value2|value2`
|
||||
| MultiParamArray -- ^ Using multiple GET parameters, e.g. `foo=bar&foo=baz`. This is valid only for parameters in "query" ('NH.Query') or "formData" ('WH.Form')
|
||||
|
||||
toHeaderColl :: WH.ToHttpApiData a => CollectionFormat -> (NH.HeaderName, [a]) -> [NH.Header]
|
||||
toHeaderColl c xs = _toColl c toHeader xs
|
||||
|
||||
toFormColl :: WH.ToHttpApiData v => CollectionFormat -> (BC.ByteString, [v]) -> WH.Form
|
||||
toFormColl c xs = WH.toForm $ fmap unpack $ _toColl c toHeader $ pack xs
|
||||
where
|
||||
pack (k,v) = (CI.mk k, v)
|
||||
unpack (k,v) = (BC.unpack (CI.original k), BC.unpack v)
|
||||
|
||||
toQueryColl :: WH.ToHttpApiData a => CollectionFormat -> (BC.ByteString, Maybe [a]) -> NH.Query
|
||||
toQueryColl c xs = _toCollA c toQuery xs
|
||||
|
||||
_toColl :: P.Traversable f => CollectionFormat -> (f a -> [(b, BC.ByteString)]) -> f [a] -> [(b, BC.ByteString)]
|
||||
_toColl c encode xs = fmap (fmap P.fromJust) (_toCollA' c fencode BC.singleton (fmap Just xs))
|
||||
where fencode = fmap (fmap Just) . encode . fmap P.fromJust
|
||||
{-# INLINE fencode #-}
|
||||
|
||||
_toCollA :: (P.Traversable f, P.Traversable t, P.Alternative t) => CollectionFormat -> (f (t a) -> [(b, t BC.ByteString)]) -> f (t [a]) -> [(b, t BC.ByteString)]
|
||||
_toCollA c encode xs = _toCollA' c encode BC.singleton xs
|
||||
|
||||
_toCollA' :: (P.Monoid c, P.Traversable f, P.Traversable t, P.Alternative t) => CollectionFormat -> (f (t a) -> [(b, t c)]) -> (Char -> c) -> f (t [a]) -> [(b, t c)]
|
||||
_toCollA' c encode one xs = case c of
|
||||
CommaSeparated -> go (one ',')
|
||||
SpaceSeparated -> go (one ' ')
|
||||
TabSeparated -> go (one '\t')
|
||||
PipeSeparated -> go (one '|')
|
||||
MultiParamArray -> expandList
|
||||
where
|
||||
go sep =
|
||||
[P.foldl1 (\(sk, sv) (_, v) -> (sk, (combine sep <$> sv <*> v) <|> sv <|> v)) expandList]
|
||||
combine sep x y = x <> sep <> y
|
||||
expandList = (P.concatMap encode . (P.traverse . P.traverse) P.toList) xs
|
||||
{-# INLINE go #-}
|
||||
{-# INLINE expandList #-}
|
||||
{-# INLINE combine #-}
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
{-|
|
||||
Module : {{title}}.Client
|
||||
-}
|
||||
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
{-# LANGUAGE DeriveFoldable #-}
|
||||
{-# LANGUAGE DeriveTraversable #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unused-binds -fno-warn-unused-imports #-}
|
||||
|
||||
module {{title}}.Client where
|
||||
|
||||
import {{title}}.Model
|
||||
import {{title}}.API
|
||||
import {{title}}.MimeTypes
|
||||
|
||||
import qualified Control.Monad.IO.Class as P
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.Types as A
|
||||
import qualified Data.Proxy as P (Proxy(..))
|
||||
import Data.Function ((&))
|
||||
import Data.Monoid ((<>))
|
||||
import Data.Text (Text)
|
||||
import GHC.Exts (IsString(..))
|
||||
import Web.FormUrlEncoded as WH
|
||||
import Web.HttpApiData as WH
|
||||
import Control.Monad.Catch (MonadThrow)
|
||||
|
||||
import qualified Control.Monad.Logger as LG
|
||||
|
||||
import qualified Data.Time as TI
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Text.Printf as T
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import qualified Data.ByteString.Lazy.Char8 as BCL
|
||||
import qualified Data.ByteString.Builder as BB
|
||||
import qualified Network.HTTP.Client as NH
|
||||
import qualified Network.HTTP.Client.TLS as NH
|
||||
import qualified Network.HTTP.Client.MultipartFormData as NH
|
||||
import qualified Network.HTTP.Types.Method as NH
|
||||
import qualified Network.HTTP.Types as NH
|
||||
import qualified Network.HTTP.Types.URI as NH
|
||||
|
||||
import qualified Control.Exception.Safe as E
|
||||
-- * Config
|
||||
|
||||
-- |
|
||||
data {{configType}} = {{configType}}
|
||||
{ configHost :: BCL.ByteString -- ^ host supplied in the Request
|
||||
, configUserAgent :: Text -- ^ user-agent supplied in the Request
|
||||
, configExecLoggingT :: ExecLoggingT -- ^ Run a block using a MonadLogger instance
|
||||
, configLoggingFilter :: LG.LogSource -> LG.LogLevel -> Bool -- ^ Only log messages passing the given predicate function.
|
||||
}
|
||||
|
||||
-- | display the config
|
||||
instance Show {{configType}} where
|
||||
show c =
|
||||
T.printf
|
||||
"{ configHost = %v, configUserAgent = %v, ..}"
|
||||
(show (configHost c))
|
||||
(show (configUserAgent c))
|
||||
|
||||
-- | constructs a default {{configType}}
|
||||
--
|
||||
-- configHost:
|
||||
--
|
||||
-- @{{basePath}}@
|
||||
--
|
||||
-- configUserAgent:
|
||||
--
|
||||
-- @"{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}{{{artifactId}}}/{{{artifactVersion}}}{{/httpUserAgent}}"@
|
||||
--
|
||||
-- configExecLoggingT: 'runNullLoggingT'
|
||||
--
|
||||
-- configLoggingFilter: 'infoLevelFilter'
|
||||
newConfig :: {{configType}}
|
||||
newConfig =
|
||||
{{configType}}
|
||||
{ configHost = "{{basePath}}"
|
||||
, configUserAgent = "{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}{{{artifactId}}}/{{{artifactVersion}}}{{/httpUserAgent}}"
|
||||
, configExecLoggingT = runNullLoggingT
|
||||
, configLoggingFilter = infoLevelFilter
|
||||
}
|
||||
|
||||
-- | updates the config to use a MonadLogger instance which prints to stdout.
|
||||
withStdoutLogging :: {{configType}} -> {{configType}}
|
||||
withStdoutLogging p = p { configExecLoggingT = LG.runStdoutLoggingT}
|
||||
|
||||
-- | updates the config to use a MonadLogger instance which prints to stderr.
|
||||
withStderrLogging :: {{configType}} -> {{configType}}
|
||||
withStderrLogging p = p { configExecLoggingT = LG.runStderrLoggingT}
|
||||
|
||||
-- | updates the config to disable logging
|
||||
withNoLogging :: {{configType}} -> {{configType}}
|
||||
withNoLogging p = p { configExecLoggingT = runNullLoggingT}
|
||||
|
||||
-- * Dispatch
|
||||
|
||||
-- ** Lbs
|
||||
|
||||
-- | send a request returning the raw http response
|
||||
dispatchLbs
|
||||
:: (Produces req accept, MimeType contentType)
|
||||
=> NH.Manager -- ^ http-client Connection manager
|
||||
-> {{configType}} -- ^ config
|
||||
-> {{requestType}} req contentType res -- ^ request
|
||||
-> accept -- ^ "accept" 'MimeType'
|
||||
-> IO (NH.Response BCL.ByteString) -- ^ response
|
||||
dispatchLbs manager config request accept = do
|
||||
initReq <- _toInitRequest config request accept
|
||||
dispatchInitUnsafe manager config initReq
|
||||
|
||||
-- ** Mime
|
||||
|
||||
-- | pair of decoded http body and http response
|
||||
data MimeResult res =
|
||||
MimeResult { mimeResult :: Either MimeError res -- ^ decoded http body
|
||||
, mimeResultResponse :: NH.Response BCL.ByteString -- ^ http response
|
||||
}
|
||||
deriving (Show, Functor, Foldable, Traversable)
|
||||
|
||||
-- | pair of unrender/parser error and http response
|
||||
data MimeError =
|
||||
MimeError {
|
||||
mimeError :: String -- ^ unrender/parser error
|
||||
, mimeErrorResponse :: NH.Response BCL.ByteString -- ^ http response
|
||||
} deriving (Eq, Show)
|
||||
|
||||
-- | send a request returning the 'MimeResult'
|
||||
dispatchMime
|
||||
:: (Produces req accept, MimeUnrender accept res, MimeType contentType)
|
||||
=> NH.Manager -- ^ http-client Connection manager
|
||||
-> {{configType}} -- ^ config
|
||||
-> {{requestType}} req contentType res -- ^ request
|
||||
-> accept -- ^ "accept" 'MimeType'
|
||||
-> IO (MimeResult res) -- ^ response
|
||||
dispatchMime manager config request accept = do
|
||||
httpResponse <- dispatchLbs manager config request accept
|
||||
parsedResult <-
|
||||
runExceptionLoggingT "Client" config $
|
||||
do case mimeUnrender' accept (NH.responseBody httpResponse) of
|
||||
Left s -> do
|
||||
logNST LG.LevelError "Client" (T.pack s)
|
||||
pure (Left (MimeError s httpResponse))
|
||||
Right r -> pure (Right r)
|
||||
return (MimeResult parsedResult httpResponse)
|
||||
|
||||
-- | like 'dispatchMime', but only returns the decoded http body
|
||||
dispatchMime'
|
||||
:: (Produces req accept, MimeUnrender accept res, MimeType contentType)
|
||||
=> NH.Manager -- ^ http-client Connection manager
|
||||
-> {{configType}} -- ^ config
|
||||
-> {{requestType}} req contentType res -- ^ request
|
||||
-> accept -- ^ "accept" 'MimeType'
|
||||
-> IO (Either MimeError res) -- ^ response
|
||||
dispatchMime' manager config request accept = do
|
||||
MimeResult parsedResult _ <- dispatchMime manager config request accept
|
||||
return parsedResult
|
||||
|
||||
-- ** Unsafe
|
||||
|
||||
-- | like 'dispatchReqLbs', but does not validate the operation is a 'Producer' of the "accept" 'MimeType'. (Useful if the server's response is undocumented)
|
||||
dispatchLbsUnsafe
|
||||
:: (MimeType accept, MimeType contentType)
|
||||
=> NH.Manager -- ^ http-client Connection manager
|
||||
-> {{configType}} -- ^ config
|
||||
-> {{requestType}} req contentType res -- ^ request
|
||||
-> accept -- ^ "accept" 'MimeType'
|
||||
-> IO (NH.Response BCL.ByteString) -- ^ response
|
||||
dispatchLbsUnsafe manager config request accept = do
|
||||
initReq <- _toInitRequest config request accept
|
||||
dispatchInitUnsafe manager config initReq
|
||||
|
||||
-- | dispatch an InitRequest
|
||||
dispatchInitUnsafe
|
||||
:: NH.Manager -- ^ http-client Connection manager
|
||||
-> {{configType}} -- ^ config
|
||||
-> InitRequest req contentType res accept -- ^ init request
|
||||
-> IO (NH.Response BCL.ByteString) -- ^ response
|
||||
dispatchInitUnsafe manager config (InitRequest req) = do
|
||||
runExceptionLoggingT logSrc config $
|
||||
do logNST LG.LevelInfo logSrc requestLogMsg
|
||||
logNST LG.LevelDebug logSrc requestDbgLogMsg
|
||||
res <- P.liftIO $ NH.httpLbs req manager
|
||||
logNST LG.LevelInfo logSrc (responseLogMsg res)
|
||||
logNST LG.LevelDebug logSrc ((T.pack . show) res)
|
||||
return res
|
||||
where
|
||||
logSrc = "Client"
|
||||
endpoint =
|
||||
T.pack $
|
||||
BC.unpack $
|
||||
NH.method req <> " " <> NH.host req <> NH.path req <> NH.queryString req
|
||||
requestLogMsg = "REQ:" <> endpoint
|
||||
requestDbgLogMsg =
|
||||
"Headers=" <> (T.pack . show) (NH.requestHeaders req) <> " Body=" <>
|
||||
(case NH.requestBody req of
|
||||
NH.RequestBodyLBS xs -> T.decodeUtf8 (BL.toStrict xs)
|
||||
_ -> "<RequestBody>")
|
||||
responseStatusCode = (T.pack . show) . NH.statusCode . NH.responseStatus
|
||||
responseLogMsg res =
|
||||
"RES:statusCode=" <> responseStatusCode res <> " (" <> endpoint <> ")"
|
||||
|
||||
-- * InitRequest
|
||||
|
||||
-- | wraps an http-client 'Request' with request/response type parameters
|
||||
newtype InitRequest req contentType res accept = InitRequest
|
||||
{ unInitRequest :: NH.Request
|
||||
} deriving (Show)
|
||||
|
||||
-- | Build an http-client 'Request' record from the supplied config and request
|
||||
_toInitRequest
|
||||
:: (MimeType accept, MimeType contentType)
|
||||
=> {{configType}} -- ^ config
|
||||
-> {{requestType}} req contentType res -- ^ request
|
||||
-> accept -- ^ "accept" 'MimeType'
|
||||
-> IO (InitRequest req contentType res accept) -- ^ initialized request
|
||||
_toInitRequest config req0 accept = do
|
||||
parsedReq <- NH.parseRequest $ BCL.unpack $ BCL.append (configHost config) (BCL.concat (urlPath req0))
|
||||
let req1 = _setAcceptHeader req0 accept & _setContentTypeHeader
|
||||
reqHeaders = ("User-Agent", WH.toHeader (configUserAgent config)) : paramsHeaders (params req1)
|
||||
reqQuery = NH.renderQuery True (paramsQuery (params req1))
|
||||
pReq = parsedReq { NH.method = (rMethod req1)
|
||||
, NH.requestHeaders = reqHeaders
|
||||
, NH.queryString = reqQuery
|
||||
}
|
||||
outReq <- case paramsBody (params req1) of
|
||||
ParamBodyNone -> pure (pReq { NH.requestBody = mempty })
|
||||
ParamBodyB bs -> pure (pReq { NH.requestBody = NH.RequestBodyBS bs })
|
||||
ParamBodyBL bl -> pure (pReq { NH.requestBody = NH.RequestBodyLBS bl })
|
||||
ParamBodyFormUrlEncoded form -> pure (pReq { NH.requestBody = NH.RequestBodyLBS (WH.urlEncodeForm form) })
|
||||
ParamBodyMultipartFormData parts -> NH.formDataBody parts pReq
|
||||
|
||||
pure (InitRequest outReq)
|
||||
|
||||
-- | modify the underlying Request
|
||||
modifyInitRequest :: InitRequest req contentType res accept -> (NH.Request -> NH.Request) -> InitRequest req contentType res accept
|
||||
modifyInitRequest (InitRequest req) f = InitRequest (f req)
|
||||
|
||||
-- | modify the underlying Request (monadic)
|
||||
modifyInitRequestM :: Monad m => InitRequest req contentType res accept -> (NH.Request -> m NH.Request) -> m (InitRequest req contentType res accept)
|
||||
modifyInitRequestM (InitRequest req) f = fmap InitRequest (f req)
|
||||
|
||||
-- * Logging
|
||||
|
||||
-- | A block using a MonadLogger instance
|
||||
type ExecLoggingT = forall m. P.MonadIO m =>
|
||||
forall a. LG.LoggingT m a -> m a
|
||||
|
||||
-- ** Null Logger
|
||||
|
||||
-- | a logger which disables logging
|
||||
nullLogger :: LG.Loc -> LG.LogSource -> LG.LogLevel -> LG.LogStr -> IO ()
|
||||
nullLogger _ _ _ _ = return ()
|
||||
|
||||
-- | run the monad transformer that disables logging
|
||||
runNullLoggingT :: LG.LoggingT m a -> m a
|
||||
runNullLoggingT = (`LG.runLoggingT` nullLogger)
|
||||
|
||||
-- ** Logging Filters
|
||||
|
||||
-- | a log filter that uses 'LevelError' as the minimum logging level
|
||||
errorLevelFilter :: LG.LogSource -> LG.LogLevel -> Bool
|
||||
errorLevelFilter = minLevelFilter LG.LevelError
|
||||
|
||||
-- | a log filter that uses 'LevelInfo' as the minimum logging level
|
||||
infoLevelFilter :: LG.LogSource -> LG.LogLevel -> Bool
|
||||
infoLevelFilter = minLevelFilter LG.LevelInfo
|
||||
|
||||
-- | a log filter that uses 'LevelDebug' as the minimum logging level
|
||||
debugLevelFilter :: LG.LogSource -> LG.LogLevel -> Bool
|
||||
debugLevelFilter = minLevelFilter LG.LevelDebug
|
||||
|
||||
minLevelFilter :: LG.LogLevel -> LG.LogSource -> LG.LogLevel -> Bool
|
||||
minLevelFilter l _ l' = l' >= l
|
||||
|
||||
-- ** Logging
|
||||
|
||||
-- | Log a message using the current time
|
||||
logNST :: (P.MonadIO m, LG.MonadLogger m) => LG.LogLevel -> Text -> Text -> m ()
|
||||
logNST level src msg = do
|
||||
now <- P.liftIO (formatTimeLog <$> TI.getCurrentTime)
|
||||
LG.logOtherNS sourceLog level (now <> " " <> msg)
|
||||
where
|
||||
sourceLog = "{{title}}/" <> src
|
||||
formatTimeLog =
|
||||
T.pack . TI.formatTime TI.defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Z"
|
||||
|
||||
-- | re-throws exceptions after logging them
|
||||
logExceptions
|
||||
:: (LG.MonadLogger m, E.MonadCatch m, P.MonadIO m)
|
||||
=> Text -> m a -> m a
|
||||
logExceptions src =
|
||||
E.handle
|
||||
(\(e :: E.SomeException) -> do
|
||||
logNST LG.LevelError src ((T.pack . show) e)
|
||||
E.throw e)
|
||||
|
||||
-- | Run a block using the configured MonadLogger instance
|
||||
runLoggingT :: {{configType}} -> ExecLoggingT
|
||||
runLoggingT config =
|
||||
configExecLoggingT config . LG.filterLogger (configLoggingFilter config)
|
||||
|
||||
-- | Run a block using the configured MonadLogger instance (logs exceptions)
|
||||
runExceptionLoggingT
|
||||
:: (E.MonadCatch m, P.MonadIO m)
|
||||
=> T.Text -> {{configType}} -> LG.LoggingT m a -> m a
|
||||
runExceptionLoggingT logSrc config = runLoggingT config . logExceptions logSrc
|
||||
@@ -0,0 +1,66 @@
|
||||
{-|
|
||||
Module : {{title}}.Lens
|
||||
-}
|
||||
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# OPTIONS_GHC -fno-warn-name-shadowing -fno-warn-unused-matches -fno-warn-unused-binds -fno-warn-unused-imports #-}
|
||||
|
||||
module {{title}}.Lens where
|
||||
|
||||
import Data.Text (Text)
|
||||
|
||||
import qualified Data.Aeson as A
|
||||
import Data.Aeson (Value)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.ByteString.Lazy (ByteString)
|
||||
import qualified Data.Data as P (Data, Typeable)
|
||||
import qualified Data.Map as Map
|
||||
|
||||
import qualified Data.Time as TI
|
||||
import Data.Time (UTCTime)
|
||||
|
||||
import Prelude (($), (.),(<$>),(<*>),(=<<),Maybe(..),Bool(..),Char,Double,FilePath,Float,Int,Integer,String,fmap,undefined,mempty,maybe,pure,Monad,Applicative,Functor)
|
||||
import qualified Prelude as P
|
||||
|
||||
import {{title}}.Model
|
||||
|
||||
-- * Type Aliases
|
||||
|
||||
type Traversal_' s a = Traversal_ s s a a
|
||||
type Traversal_ s t a b = forall (f :: * -> *). Applicative f => (a -> f b) -> s -> f t
|
||||
type Lens_' s a = Lens_ s s a a
|
||||
type Lens_ s t a b = forall (f :: * -> *). Functor f => (a -> f b) -> s -> f t
|
||||
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
|
||||
-- * {{classname}}
|
||||
|
||||
{{#vars}}
|
||||
{{#required}}
|
||||
-- | '{{name}}' Lens
|
||||
{{name}}L :: Lens_' {{classname}} {{datatype}}
|
||||
{{name}}L f {{classname}}{..} = (\{{name}} -> {{classname}} { {{name}}, ..} ) <$> f {{name}}
|
||||
{-# INLINE {{name}}L #-}
|
||||
{{/required}}
|
||||
{{^required}}
|
||||
-- | '{{name}}' Traversal
|
||||
{{name}}T :: Traversal_' {{classname}} {{datatype}}
|
||||
{{name}}T f s = _mtraversal {{name}} (\b -> s { {{name}} = Just b}) f s
|
||||
{-# INLINE {{name}}T #-}
|
||||
{{/required}}
|
||||
|
||||
{{/vars}}
|
||||
|
||||
{{/model}}
|
||||
{{/models}}
|
||||
|
||||
|
||||
-- * Helpers
|
||||
|
||||
_mtraversal :: Applicative f => (b -> Maybe t) -> (a -> b) -> (t -> f a) -> b -> f b
|
||||
_mtraversal x fsb f s = maybe (pure s) (\a -> fsb <$> f a) (x s)
|
||||
{-# INLINE _mtraversal #-}
|
||||
@@ -0,0 +1,204 @@
|
||||
|
||||
{-|
|
||||
Module : {{title}}.MimeTypes
|
||||
-}
|
||||
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unused-binds -fno-warn-unused-imports #-}
|
||||
|
||||
module {{title}}.MimeTypes where
|
||||
|
||||
|
||||
import qualified Data.Aeson as A
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.ByteString.Builder as BB
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import qualified Data.ByteString.Lazy.Char8 as BCL
|
||||
|
||||
import qualified Network.HTTP.Media as ME
|
||||
|
||||
import qualified Web.FormUrlEncoded as WH
|
||||
|
||||
import qualified Data.Data as P (Typeable)
|
||||
import qualified Data.Proxy as P (Proxy(..))
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Control.Arrow as P (left)
|
||||
|
||||
import Prelude (($), (.),(<$>),(<*>),Maybe(..),Bool(..),Char,Double,FilePath,Float,Int,Integer,String,fmap,undefined,mempty)
|
||||
import qualified Prelude as P
|
||||
|
||||
-- * Content Negotiation
|
||||
|
||||
-- | A type for responses without content-body.
|
||||
data NoContent = NoContent
|
||||
deriving (P.Show, P.Eq)
|
||||
|
||||
-- ** Mime Types
|
||||
|
||||
data MimeJSON = MimeJSON deriving (P.Typeable)
|
||||
data MimeXML = MimeXML deriving (P.Typeable)
|
||||
data MimePlainText = MimePlainText deriving (P.Typeable)
|
||||
data MimeFormUrlEncoded = MimeFormUrlEncoded deriving (P.Typeable)
|
||||
data MimeMultipartFormData = MimeMultipartFormData deriving (P.Typeable)
|
||||
data MimeOctetStream = MimeOctetStream deriving (P.Typeable)
|
||||
data MimeNoContent = MimeNoContent deriving (P.Typeable)
|
||||
|
||||
{{#x-unknownMimeTypes}}data {{{x-mediaDataType}}} = {{{x-mediaDataType}}} deriving (P.Typeable)
|
||||
{{/x-unknownMimeTypes}}
|
||||
|
||||
-- ** MimeType Class
|
||||
|
||||
class P.Typeable mtype => MimeType mtype where
|
||||
{-# MINIMAL mimeType | mimeTypes #-}
|
||||
|
||||
mimeTypes :: P.Proxy mtype -> [ME.MediaType]
|
||||
mimeTypes p =
|
||||
case mimeType p of
|
||||
Just x -> [x]
|
||||
Nothing -> []
|
||||
|
||||
mimeType :: P.Proxy mtype -> Maybe ME.MediaType
|
||||
mimeType p =
|
||||
case mimeTypes p of
|
||||
[] -> Nothing
|
||||
(x:_) -> Just x
|
||||
|
||||
mimeType' :: mtype -> Maybe ME.MediaType
|
||||
mimeType' _ = mimeType (P.Proxy :: P.Proxy mtype)
|
||||
mimeTypes' :: mtype -> [ME.MediaType]
|
||||
mimeTypes' _ = mimeTypes (P.Proxy :: P.Proxy mtype)
|
||||
|
||||
-- ** MimeType Instances
|
||||
|
||||
-- | @application/json@
|
||||
instance MimeType MimeJSON where
|
||||
mimeTypes _ =
|
||||
[ "application" ME.// "json" ME./: ("charset", "utf-8")
|
||||
, "application" ME.// "json"
|
||||
]
|
||||
|
||||
-- | @application/xml@
|
||||
instance MimeType MimeXML where
|
||||
mimeType _ = Just $ "application" ME.// "xml"
|
||||
|
||||
-- | @application/x-www-form-urlencoded@
|
||||
instance MimeType MimeFormUrlEncoded where
|
||||
mimeType _ = Just $ "application" ME.// "x-www-form-urlencoded"
|
||||
|
||||
-- | @multipart/form-data@
|
||||
instance MimeType MimeMultipartFormData where
|
||||
mimeType _ = Just $ "multipart" ME.// "form-data"
|
||||
|
||||
-- | @text/plain;charset=utf-8@
|
||||
instance MimeType MimePlainText where
|
||||
mimeType _ = Just $ "text" ME.// "plain" ME./: ("charset", "utf-8")
|
||||
instance MimeType MimeOctetStream where
|
||||
mimeType _ = Just $ "application" ME.// "octet-stream"
|
||||
instance MimeType MimeNoContent where
|
||||
mimeType _ = Nothing
|
||||
|
||||
{{#x-unknownMimeTypes}}
|
||||
-- | @{{{mediaType}}}@
|
||||
instance MimeType {{{x-mediaDataType}}} where
|
||||
mimeType _ = Just $ "{{{x-mediaMainType}}}" ME.// "{{{x-mediaSubType}}}"
|
||||
|
||||
{{/x-unknownMimeTypes}}
|
||||
|
||||
-- ** MimeRender Class
|
||||
|
||||
class MimeType mtype => MimeRender mtype x where
|
||||
mimeRender :: P.Proxy mtype -> x -> BL.ByteString
|
||||
mimeRender' :: mtype -> x -> BL.ByteString
|
||||
mimeRender' _ x = mimeRender (P.Proxy :: P.Proxy mtype) x
|
||||
|
||||
|
||||
-- ** MimeRender Instances
|
||||
|
||||
-- | `A.encode`
|
||||
instance A.ToJSON a => MimeRender MimeJSON a where mimeRender _ = A.encode
|
||||
-- | @WH.urlEncodeAsForm@
|
||||
instance WH.ToForm a => MimeRender MimeFormUrlEncoded a where mimeRender _ = WH.urlEncodeAsForm
|
||||
|
||||
-- | @P.id@
|
||||
instance MimeRender MimePlainText BL.ByteString where mimeRender _ = P.id
|
||||
-- | @BL.fromStrict . T.encodeUtf8@
|
||||
instance MimeRender MimePlainText T.Text where mimeRender _ = BL.fromStrict . T.encodeUtf8
|
||||
-- | @BCL.pack@
|
||||
instance MimeRender MimePlainText String where mimeRender _ = BCL.pack
|
||||
|
||||
-- | @P.id@
|
||||
instance MimeRender MimeOctetStream BL.ByteString where mimeRender _ = P.id
|
||||
-- | @BL.fromStrict . T.encodeUtf8@
|
||||
instance MimeRender MimeOctetStream T.Text where mimeRender _ = BL.fromStrict . T.encodeUtf8
|
||||
-- | @BCL.pack@
|
||||
instance MimeRender MimeOctetStream String where mimeRender _ = BCL.pack
|
||||
|
||||
-- | @P.id@
|
||||
instance MimeRender MimeMultipartFormData BL.ByteString where mimeRender _ = P.id
|
||||
-- | @BL.fromStrict . T.encodeUtf8@
|
||||
instance MimeRender MimeMultipartFormData T.Text where mimeRender _ = BL.fromStrict . T.encodeUtf8
|
||||
-- | @BCL.pack@
|
||||
instance MimeRender MimeMultipartFormData String where mimeRender _ = BCL.pack
|
||||
|
||||
-- | @P.Right . P.const NoContent@
|
||||
instance MimeRender MimeNoContent NoContent where mimeRender _ = P.const BCL.empty
|
||||
|
||||
-- instance MimeRender MimeOctetStream Double where mimeRender _ = BB.toLazyByteString . BB.doubleDec
|
||||
-- instance MimeRender MimeOctetStream Float where mimeRender _ = BB.toLazyByteString . BB.floatDec
|
||||
-- instance MimeRender MimeOctetStream Int where mimeRender _ = BB.toLazyByteString . BB.intDec
|
||||
-- instance MimeRender MimeOctetStream Integer where mimeRender _ = BB.toLazyByteString . BB.integerDec
|
||||
|
||||
{{#x-unknownMimeTypes}}
|
||||
-- instance MimeRender {{{x-mediaDataType}}} T.Text where mimeRender _ = undefined
|
||||
{{/x-unknownMimeTypes}}
|
||||
|
||||
-- ** MimeUnrender Class
|
||||
|
||||
class MimeType mtype => MimeUnrender mtype o where
|
||||
mimeUnrender :: P.Proxy mtype -> BL.ByteString -> P.Either String o
|
||||
mimeUnrender' :: mtype -> BL.ByteString -> P.Either String o
|
||||
mimeUnrender' _ x = mimeUnrender (P.Proxy :: P.Proxy mtype) x
|
||||
|
||||
-- ** MimeUnrender Instances
|
||||
|
||||
-- | @A.eitherDecode@
|
||||
instance A.FromJSON a => MimeUnrender MimeJSON a where mimeUnrender _ = A.eitherDecode
|
||||
-- | @P.left T.unpack . WH.urlDecodeAsForm@
|
||||
instance WH.FromForm a => MimeUnrender MimeFormUrlEncoded a where mimeUnrender _ = P.left T.unpack . WH.urlDecodeAsForm
|
||||
-- | @P.Right . P.id@
|
||||
|
||||
instance MimeUnrender MimePlainText BL.ByteString where mimeUnrender _ = P.Right . P.id
|
||||
-- | @P.left P.show . TL.decodeUtf8'@
|
||||
instance MimeUnrender MimePlainText T.Text where mimeUnrender _ = P.left P.show . T.decodeUtf8' . BL.toStrict
|
||||
-- | @P.Right . BCL.unpack@
|
||||
instance MimeUnrender MimePlainText String where mimeUnrender _ = P.Right . BCL.unpack
|
||||
|
||||
-- | @P.Right . P.id@
|
||||
instance MimeUnrender MimeOctetStream BL.ByteString where mimeUnrender _ = P.Right . P.id
|
||||
-- | @P.left P.show . T.decodeUtf8' . BL.toStrict@
|
||||
instance MimeUnrender MimeOctetStream T.Text where mimeUnrender _ = P.left P.show . T.decodeUtf8' . BL.toStrict
|
||||
-- | @P.Right . BCL.unpack@
|
||||
instance MimeUnrender MimeOctetStream String where mimeUnrender _ = P.Right . BCL.unpack
|
||||
|
||||
-- | @P.Right . P.const NoContent@
|
||||
instance MimeUnrender MimeNoContent NoContent where mimeUnrender _ = P.Right . P.const NoContent
|
||||
|
||||
{{#x-unknownMimeTypes}}
|
||||
-- instance MimeUnrender {{{x-mediaDataType}}} T.Text where mimeUnrender _ = undefined
|
||||
{{/x-unknownMimeTypes}}
|
||||
|
||||
-- ** Request Consumes
|
||||
|
||||
class MimeType mtype => Consumes req mtype where
|
||||
|
||||
-- ** Request Produces
|
||||
|
||||
class MimeType mtype => Produces req mtype where
|
||||
@@ -0,0 +1,170 @@
|
||||
{-|
|
||||
Module : {{title}}.Model
|
||||
-}
|
||||
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE DeriveFoldable #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DeriveTraversable #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unused-matches -fno-warn-unused-binds -fno-warn-unused-imports #-}
|
||||
|
||||
module {{title}}.Model where
|
||||
|
||||
import Data.Aeson ((.:),(.:!),(.:?),(.=))
|
||||
import Data.Text (Text)
|
||||
|
||||
import Data.Aeson (Value)
|
||||
import Data.ByteString.Lazy (ByteString)
|
||||
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.Data as P (Data, Typeable)
|
||||
import qualified Data.HashMap.Lazy as HM
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Maybe as P
|
||||
import qualified Data.Foldable as P
|
||||
import qualified Web.FormUrlEncoded as WH
|
||||
import qualified Web.HttpApiData as WH
|
||||
|
||||
import qualified Data.Time as TI
|
||||
import qualified Data.Time.ISO8601 as TI
|
||||
import Data.Time (UTCTime)
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Applicative (Alternative)
|
||||
import Prelude (($), (.),(<$>),(<*>),(>>=),Maybe(..),Bool(..),Char,Double,FilePath,Float,Int,Integer,String,fmap,undefined,mempty,maybe,pure,Monad,Applicative,Functor)
|
||||
import qualified Prelude as P
|
||||
|
||||
|
||||
{{#imports}}import {{import}}
|
||||
{{/imports}}
|
||||
|
||||
-- * Models
|
||||
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
|
||||
-- ** {{classname}}
|
||||
-- |{{#title}}
|
||||
-- {{{.}}}
|
||||
-- {{/title}}{{#description}}
|
||||
-- {{{.}}}{{/description}}
|
||||
{{^vendorExtensions.x-customNewtype}}
|
||||
data {{classname}} = {{classname}}
|
||||
{ {{#vars}}{{name}} :: {{^required}}Maybe {{/required}}{{datatype}} -- ^ {{#required}}/Required/ {{/required}}{{#readOnly}}/ReadOnly/ {{/readOnly}}"{{baseName}}"{{#description}} - {{description}}{{/description}}{{#hasMore}}
|
||||
, {{/hasMore}}{{/vars}}
|
||||
} deriving (P.Show,P.Eq,P.Typeable{{#modelDeriving}},{{modelDeriving}}{{/modelDeriving}})
|
||||
|
||||
instance A.FromJSON {{classname}} where
|
||||
parseJSON = A.withObject "{{classname}}" $ \o ->
|
||||
{{classname}}
|
||||
<$>{{#vars}} (o {{#required}}.: {{/required}}{{^required}}{{^allowFromJsonNulls}}.:!{{/allowFromJsonNulls}}{{#allowFromJsonNulls}}.:?{{/allowFromJsonNulls}}{{/required}} "{{baseName}}"{{#isDateTime}} >>={{^required}} P.mapM{{/required}} _readDateTime{{/isDateTime}}{{#isDate}} >>={{^required}} P.mapM{{/required}} _showDate{{/isDate}}){{#hasMore}}
|
||||
<*>{{/hasMore}}{{/vars}}
|
||||
|
||||
instance A.ToJSON {{classname}} where
|
||||
toJSON {{classname}} {..} =
|
||||
{{^allowToJsonNulls}}_omitNulls{{/allowToJsonNulls}}{{#allowToJsonNulls}}A.object{{/allowToJsonNulls}}
|
||||
[ {{#vars}}"{{baseName}}" .= {{#isDateTime}}{{^required}}P.fmap {{/required}}_showDateTime{{/isDateTime}}{{#isDate}}{{^required}}P.fmap {{/required}}_showDate{{/isDate}} {{name}}{{#hasMore}}
|
||||
, {{/hasMore}}{{/vars}}
|
||||
]
|
||||
|
||||
{{#vendorExtensions.x-hasMimeFormUrlEncoded}}
|
||||
instance WH.FromForm {{classname}} where
|
||||
fromForm f =
|
||||
{{classname}}
|
||||
<$>{{#vars}} ({{#required}}WH.parseUnique {{/required}}{{^required}}WH.parseMaybe {{/required}}"{{baseName}}" f{{#isDateTime}} >>={{^required}} P.mapM{{/required}} _readDateTime{{/isDateTime}}{{#isDate}} >>={{^required}} P.mapM{{/required}} _showDate{{/isDate}}){{#hasMore}}
|
||||
<*>{{/hasMore}}{{/vars}}
|
||||
|
||||
instance WH.ToForm {{classname}} where
|
||||
toForm {{classname}} {..} =
|
||||
WH.Form $ HM.fromList $ P.catMaybes $
|
||||
[ {{#vars}}_toFormItem "{{baseName}}" ({{#required}}Just $ {{/required}}{{#isDateTime}}{{^required}}P.fmap {{/required}}_showDateTime {{/isDateTime}}{{#isDate}}{{^required}}P.fmap {{/required}}_showDate {{/isDate}}{{name}}){{#hasMore}}
|
||||
, {{/hasMore}}{{/vars}}
|
||||
]
|
||||
{{/vendorExtensions.x-hasMimeFormUrlEncoded}}
|
||||
|
||||
{{#generateModelConstructors}}
|
||||
-- | Construct a value of type '{{classname}}' (by applying it's required fields, if any)
|
||||
mk{{classname}}
|
||||
:: {{#requiredVars}}{{{datatype}}} -- ^ '{{name}}'{{#description}}:{{/description}} {{{description}}}
|
||||
-> {{/requiredVars}}{{classname}}
|
||||
mk{{classname}} {{#requiredVars}}{{name}} {{/requiredVars}}=
|
||||
{{classname}}
|
||||
{ {{#vars}}{{#required}}{{name}}{{/required}}{{^required}}{{name}} = {{#isListContainer}}Nothing{{/isListContainer}}{{#isMapContainer}}Nothing{{/isMapContainer}}{{^isContainer}}Nothing{{/isContainer}}{{/required}}{{#hasMore}}
|
||||
, {{/hasMore}}{{/vars}}
|
||||
}
|
||||
{{/generateModelConstructors}}
|
||||
|
||||
{{/vendorExtensions.x-customNewtype}}
|
||||
{{#vendorExtensions.x-customNewtype}}
|
||||
newtype {{classname}} = {{classname}} {{vendorExtensions.x-customNewtype}} deriving (P.Show, P.Eq, P.Data, P.Typeable, P.Generic, A.FromJSON, A.ToJSON)
|
||||
{-# WARNING {{classname}} "untested/unimlemented behavior" #-}
|
||||
{{/vendorExtensions.x-customNewtype}}
|
||||
|
||||
{{/model}}
|
||||
{{/models}}
|
||||
|
||||
-- * Utils
|
||||
|
||||
-- | Removes Null fields. (OpenAPI-Specification 2.0 does not allow Null in JSON)
|
||||
|
||||
_omitNulls :: [(Text, A.Value)] -> A.Value
|
||||
_omitNulls = A.object . P.filter notNull
|
||||
where
|
||||
notNull (_, A.Null) = False
|
||||
notNull _ = True
|
||||
|
||||
_toFormItem :: (WH.ToHttpApiData a, Functor f) => t -> f a -> f (t, [Text])
|
||||
_toFormItem name x = (name,) . (:[]) . WH.toQueryParam <$> x
|
||||
|
||||
_emptyToNothing :: Maybe String -> Maybe String
|
||||
_emptyToNothing (Just "") = Nothing
|
||||
_emptyToNothing x = x
|
||||
{-# INLINE _emptyToNothing #-}
|
||||
|
||||
_memptyToNothing :: (P.Monoid a, P.Eq a) => Maybe a -> Maybe a
|
||||
_memptyToNothing (Just x) | x P.== P.mempty = Nothing
|
||||
_memptyToNothing x = x
|
||||
{-# INLINE _memptyToNothing #-}
|
||||
|
||||
-- * DateTime Formatting
|
||||
|
||||
-- | @{{^dateTimeFormat}}_parseISO8601{{/dateTimeFormat}}{{#dateTimeFormat}}TI.parseTimeM True TI.defaultTimeLocale "{{{dateTimeFormat}}}"{{/dateTimeFormat}}@
|
||||
_readDateTime :: (TI.ParseTime t, Monad m, {{^dateTimeFormat}}Alternative m{{/dateTimeFormat}}) => String -> m t
|
||||
_readDateTime =
|
||||
{{^dateTimeFormat}}_parseISO8601{{/dateTimeFormat}}{{#dateTimeFormat}}TI.parseTimeM True TI.defaultTimeLocale "{{{dateTimeFormat}}}"{{/dateTimeFormat}}
|
||||
{-# INLINE _readDateTime #-}
|
||||
|
||||
-- | @{{^dateTimeFormat}}TI.formatISO8601Millis{{/dateTimeFormat}}{{#dateTimeFormat}}TI.formatTime TI.defaultTimeLocale "{{{dateTimeFormat}}}"{{/dateTimeFormat}}@
|
||||
_showDateTime :: ({{^dateTimeFormat}}t ~ UTCTime, {{/dateTimeFormat}}TI.FormatTime t) => t -> String
|
||||
_showDateTime =
|
||||
{{^dateTimeFormat}}TI.formatISO8601Millis{{/dateTimeFormat}}{{#dateTimeFormat}}TI.formatTime TI.defaultTimeLocale "{{{dateTimeFormat}}}"{{/dateTimeFormat}}
|
||||
{-# INLINE _showDateTime #-}
|
||||
|
||||
_parseISO8601 :: (TI.ParseTime t, Monad m, Alternative m) => String -> m t
|
||||
_parseISO8601 t =
|
||||
P.asum $
|
||||
P.flip (TI.parseTimeM True TI.defaultTimeLocale) t <$>
|
||||
["%FT%T%QZ", "%FT%T%Q%z", "%FT%T%Q%Z"]
|
||||
{-# INLINE _parseISO8601 #-}
|
||||
|
||||
-- * Date Formatting
|
||||
|
||||
-- | @TI.parseTimeM True TI.defaultTimeLocale "{{{dateFormat}}}"@
|
||||
_readDate :: (TI.ParseTime t, Monad m) => String -> m t
|
||||
_readDate =
|
||||
TI.parseTimeM True TI.defaultTimeLocale "{{{dateFormat}}}"
|
||||
{-# INLINE _readDate #-}
|
||||
|
||||
-- | @TI.formatTime TI.defaultTimeLocale "{{{dateFormat}}}"@
|
||||
_showDate :: TI.FormatTime t => t -> String
|
||||
_showDate =
|
||||
TI.formatTime TI.defaultTimeLocale "{{{dateFormat}}}"
|
||||
{-# INLINE _showDate #-}
|
||||
@@ -0,0 +1,163 @@
|
||||
## Swagger Auto-Generated [http-client](https://www.stackage.org/lts-9.0/package/http-client-0.5.7.0) Bindings to `{{title}}`
|
||||
|
||||
The library in `lib` provides auto-generated-from-Swagger [http-client](https://www.stackage.org/lts-9.0/package/http-client-0.5.7.0) bindings to the {{title}} API.
|
||||
|
||||
Targeted swagger version: {{swaggerVersion}}
|
||||
|
||||
OpenAPI-Specification: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/{{swaggerVersion}}.md
|
||||
|
||||
## Installation
|
||||
|
||||
Installation follows the standard approach to installing Stack-based projects.
|
||||
|
||||
1. Install the [Haskell `stack` tool](http://docs.haskellstack.org/en/stable/README).
|
||||
2. To build the package, and generate the documentation (recommended):
|
||||
```
|
||||
stack haddock
|
||||
```
|
||||
which will generate docs for this lib in the `docs` folder.
|
||||
|
||||
To generate the docs in the normal location (to enable hyperlinks to external libs), remove
|
||||
```
|
||||
build:
|
||||
haddock-arguments:
|
||||
haddock-args:
|
||||
- "--odir=./docs"
|
||||
```
|
||||
from the stack.yaml file and run `stack haddock` again.
|
||||
|
||||
3. To run unit tests:
|
||||
```
|
||||
stack test
|
||||
```
|
||||
|
||||
## Swagger-Codegen
|
||||
|
||||
The code generator that produced this library, and which explains how
|
||||
to obtain and use the swagger-codegen cli tool lives at
|
||||
|
||||
https://github.com/swagger-api/swagger-codegen
|
||||
|
||||
The _language_ argument (`--lang`) passed to the cli tool used should be
|
||||
|
||||
```
|
||||
haskell-http-client
|
||||
```
|
||||
|
||||
### Unsupported Swagger Features
|
||||
|
||||
* Auth Methods (https://swagger.io/docs/specification/2-0/authentication/)
|
||||
|
||||
- use `setHeader` to add any required headers to requests
|
||||
|
||||
* Default Parameter Values
|
||||
|
||||
* Enum Parameters
|
||||
|
||||
This is beta software; other cases may not be supported.
|
||||
|
||||
### Codegen "config option" parameters
|
||||
|
||||
These options allow some customization of the code generation process.
|
||||
|
||||
**haskell-http-client specific options:**
|
||||
|
||||
| OPTION | DESCRIPTION | DEFAULT | ACTUAL |
|
||||
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------- |
|
||||
| allowFromJsonNulls | allow JSON Null during model decoding from JSON | true | {{allowFromJsonNulls}} |
|
||||
| allowToJsonNulls | allow emitting JSON Null during model encoding to JSON | false | {{allowToJsonNulls}} |
|
||||
| dateFormat | format string used to parse/render a date | %Y-%m-%d | {{dateFormat}} |
|
||||
| dateTimeFormat | format string used to parse/render a datetime. (Defaults to [formatISO8601Millis][1] when not provided) | | {{dateTimeFormat}} |
|
||||
| generateFormUrlEncodedInstances | Generate FromForm/ToForm instances for models used by x-www-form-urlencoded operations (model fields must be primitive types) | true | {{generateFormUrlEncodedInstances}} |
|
||||
| generateLenses | Generate Lens optics for Models | true | {{generateLenses}} |
|
||||
| generateModelConstructors | Generate smart constructors (only supply required fields) for models | true | {{generateModelConstructors}} |
|
||||
| modelDeriving | Additional classes to include in the deriving() clause of Models | | {{modelDeriving}} |
|
||||
|
||||
[1]: https://www.stackage.org/haddock/lts-9.0/iso8601-time-0.1.4/Data-Time-ISO8601.html#v:formatISO8601Millis
|
||||
|
||||
View the full list of Codegen "config option" parameters with the command:
|
||||
|
||||
```
|
||||
java -jar modules/swagger-codegen-cli/target/swagger-codegen-cli.jar config-help -l haskell-http-client
|
||||
```
|
||||
|
||||
### Example SwaggerPetstore Haddock documentation
|
||||
|
||||
An example of the generated haddock documentation targeting the server http://petstore.swagger.io/ (SwaggerPetstore) can be found [here][2]
|
||||
|
||||
[2]: https://jonschoning.github.io/swaggerpetstore-haskell-http-client/
|
||||
|
||||
### Example SwaggerPetstore App
|
||||
|
||||
An example application using the auto-generated haskell-http-client bindings for the server http://petstore.swagger.io/ can be found [here][3]
|
||||
|
||||
[3]: https://github.com/jonschoning/swagger-codegen/tree/haskell-http-client/samples/client/petstore/haskell-http-client/example-app
|
||||
|
||||
### Usage Notes
|
||||
|
||||
This library is intended to be imported qualified.
|
||||
|
||||
| MODULE | NOTES |
|
||||
| ------------------- | --------------------------------------------------- |
|
||||
| {{title}}.Client | use the "dispatch" functions to send requests |
|
||||
| {{title}}.API | construct requetss |
|
||||
| {{title}}.Model | describes models |
|
||||
| {{title}}.MimeTypes | encoding/decoding MIME types (content-types/accept) |
|
||||
| {{title}}.Lens | lenses & traversals for model fields |
|
||||
|
||||
This library adds type safety around what swagger specifies as
|
||||
Produces and Consumes for each Operation (e.g. the list of MIME types an
|
||||
Operation can Produce (using 'accept' headers) and Consume (using 'content-type' headers).
|
||||
|
||||
For example, if there is an Operation named _addFoo_, there will be a
|
||||
data type generated named _AddFoo_ (note the capitalization) which
|
||||
describes additional constraints and actions on the _addFoo_
|
||||
operation, which can be viewed in GHCi or via the Haddocks.
|
||||
|
||||
* requried parameters are included as function arguments to _addFoo_
|
||||
* optional non-body parameters are included by using `applyOptionalParam`
|
||||
* optional body parameters are set by using `setBodyParam`
|
||||
|
||||
Example for pretend _addFoo_ operation:
|
||||
|
||||
```haskell
|
||||
data AddFoo
|
||||
instance Consumes AddFoo MimeJSON
|
||||
instance Produces AddFoo MimeJSON
|
||||
instance Produces AddFoo MimeXML
|
||||
instance HasBodyParam AddFoo FooModel
|
||||
instance HasOptionalParam AddFoo FooName
|
||||
instance HasOptionalParam AddFoo FooId
|
||||
```
|
||||
|
||||
this would indicate that:
|
||||
|
||||
* the _addFoo_ operation can consume JSON
|
||||
* the _addFoo_ operation produces JSON or XML, depending on the argument passed to the dispatch function
|
||||
* the _addFoo_ operation can set it's body param of _FooModel_ via `setBodyParam`
|
||||
* the _addFoo_ operation can set 2 different optional parameters via `applyOptionalParam`
|
||||
|
||||
putting this together:
|
||||
|
||||
```haskell
|
||||
let addFooRequest = addFoo MimeJSON foomodel requiredparam1 requiredparam2
|
||||
`applyOptionalParam` FooId 1
|
||||
`applyOptionalParam` FooName "name"
|
||||
`setHeader` [("api_key","xxyy")]
|
||||
addFooResult <- dispatchMime mgr config addFooRequest MimeXML
|
||||
```
|
||||
|
||||
If the swagger spec doesn't declare it can accept or produce a certain
|
||||
MIME type for a given Operation, you should either add a Produces or
|
||||
Consumes instance for the desired MIME types (assuming the server
|
||||
supports it), use `dispatchLbsUnsafe` or modify the swagger spec and
|
||||
run the generator again.
|
||||
|
||||
New MIME type instances can be added via MimeType/MimeRender/MimeUnrender
|
||||
|
||||
Only JSON instances are generated by default, and in some case
|
||||
x-www-form-urlencoded instances (FromFrom, ToForm) will also be
|
||||
generated if the model fields are primitive types, and there are
|
||||
Operations using x-www-form-urlencoded which use those models.
|
||||
|
||||
See the example app and the haddocks for details.
|
||||
@@ -0,0 +1,2 @@
|
||||
import Distribution.Simple
|
||||
main = defaultMain
|
||||
@@ -0,0 +1,17 @@
|
||||
{-|
|
||||
Module : {{title}}
|
||||
-}
|
||||
|
||||
module {{title}}
|
||||
( module {{title}}.Client
|
||||
, module {{title}}.API
|
||||
, module {{title}}.Model
|
||||
, module {{title}}.MimeTypes
|
||||
, {{#generateLenses}}module {{title}}.Lens{{/generateLenses}}
|
||||
) where
|
||||
|
||||
import {{title}}.API
|
||||
import {{title}}.Client
|
||||
import {{title}}.Model
|
||||
import {{title}}.MimeTypes
|
||||
{{#generateLenses}}import {{title}}.Lens{{/generateLenses}}
|
||||
@@ -0,0 +1 @@
|
||||
toForm{{#collectionFormat}}Coll {{vendorExtensions.x-collectionFormat}}{{/collectionFormat}}
|
||||
@@ -0,0 +1 @@
|
||||
toHeader{{#collectionFormat}}Coll {{vendorExtensions.x-collectionFormat}}{{/collectionFormat}}
|
||||
@@ -0,0 +1 @@
|
||||
toQuery{{#collectionFormat}}Coll {{vendorExtensions.x-collectionFormat}}{{/collectionFormat}}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||
#
|
||||
# Usage example: /bin/sh ./git_push.sh wing328 swagger-petstore-perl "minor update"
|
||||
|
||||
git_user_id=$1
|
||||
git_repo_id=$2
|
||||
release_note=$3
|
||||
|
||||
if [ "$git_user_id" = "" ]; then
|
||||
git_user_id="{{{gitUserId}}}"
|
||||
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||
fi
|
||||
|
||||
if [ "$git_repo_id" = "" ]; then
|
||||
git_repo_id="{{{gitRepoId}}}"
|
||||
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||
fi
|
||||
|
||||
if [ "$release_note" = "" ]; then
|
||||
release_note="{{{releaseNote}}}"
|
||||
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||
fi
|
||||
|
||||
# Initialize the local directory as a Git repository
|
||||
git init
|
||||
|
||||
# Adds the files in the local repository and stages them for commit.
|
||||
git add .
|
||||
|
||||
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||
git commit -m "$release_note"
|
||||
|
||||
# Sets the new remote
|
||||
git_remote=`git remote`
|
||||
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||
|
||||
if [ "$GIT_TOKEN" = "" ]; then
|
||||
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git crediential in your environment."
|
||||
git remote add origin https://github.com/${git_user_id}/${git_repo_id}.git
|
||||
else
|
||||
git remote add origin https://${git_user_id}:${GIT_TOKEN}@github.com/${git_user_id}/${git_repo_id}.git
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
git pull origin master
|
||||
|
||||
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||
echo "Git pushing to https://github.com/${git_user_id}/${git_repo_id}.git"
|
||||
git push origin master 2>&1 | grep -v 'To https'
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
-- This file has been generated from package.yaml by hpack version 0.17.1.
|
||||
--
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: {{package}}
|
||||
version: 0.1.0.0
|
||||
synopsis: Auto-generated {{package}} API Client
|
||||
description: .
|
||||
Client library for calling the {{package}} API based on http-client.
|
||||
.
|
||||
base path: {{basePath}}
|
||||
.
|
||||
apiVersion: {{apiVersion}}
|
||||
.
|
||||
swagger version: {{swaggerVersion}}
|
||||
.
|
||||
{{^hideGenerationTimestamp}}Generated on: {{generatedDate}}
|
||||
.
|
||||
{{/hideGenerationTimestamp}}OpenAPI-Specification: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/{{swaggerVersion}}.md
|
||||
category: Web
|
||||
homepage: https://github.com/swagger-api/swagger-codegen#readme
|
||||
author: Author Name Here
|
||||
maintainer: author.name@email.com
|
||||
copyright: YEAR - AUTHOR
|
||||
license: UnspecifiedLicense
|
||||
build-type: Simple
|
||||
cabal-version: >= 1.10
|
||||
|
||||
extra-source-files:
|
||||
README.md
|
||||
swagger.json
|
||||
|
||||
library
|
||||
hs-source-dirs:
|
||||
lib
|
||||
ghc-options: -Wall
|
||||
build-depends:
|
||||
base >=4.7 && <5.0
|
||||
, transformers >=0.4.0.0
|
||||
, mtl >=2.2.1
|
||||
, unordered-containers
|
||||
, aeson >=1.0 && <2.0
|
||||
, bytestring >=0.10.0 && <0.11
|
||||
, containers >=0.5.0.0 && <0.6
|
||||
, http-types >=0.8 && <0.10
|
||||
, http-client >=0.5 && <0.6
|
||||
, http-client-tls
|
||||
, http-api-data >= 0.3.4 && <0.4
|
||||
, http-media >= 0.4 && < 0.8
|
||||
, text >=0.11 && <1.3
|
||||
, time >=1.5 && <1.9
|
||||
, iso8601-time >=0.1.3 && <0.2.0
|
||||
, vector >=0.10.9 && <0.13
|
||||
, network >=2.6.2 && <2.7
|
||||
, random >=1.1
|
||||
, exceptions >= 0.4
|
||||
, monad-logger >=0.3 && <0.4
|
||||
, safe-exceptions <0.2
|
||||
, case-insensitive
|
||||
exposed-modules:
|
||||
{{title}}
|
||||
{{title}}.API
|
||||
{{title}}.Client
|
||||
{{title}}.Model
|
||||
{{title}}.MimeTypes
|
||||
{{#generateLenses}}{{title}}.Lens{{/generateLenses}}
|
||||
other-modules:
|
||||
Paths_{{pathsName}}
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite tests
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Test.hs
|
||||
hs-source-dirs:
|
||||
tests
|
||||
ghc-options: -fno-warn-orphans
|
||||
build-depends:
|
||||
base >=4.7 && <5.0
|
||||
, transformers >=0.4.0.0
|
||||
, mtl >=2.2.1
|
||||
, unordered-containers
|
||||
, {{package}}
|
||||
, bytestring >=0.10.0 && <0.11
|
||||
, containers
|
||||
, hspec >=1.8
|
||||
, text
|
||||
, time
|
||||
, iso8601-time
|
||||
, aeson
|
||||
, semigroups
|
||||
, QuickCheck
|
||||
other-modules:
|
||||
ApproxEq
|
||||
Instances
|
||||
PropMime
|
||||
default-language: Haskell2010
|
||||
@@ -0,0 +1,84 @@
|
||||
name: {{package}}
|
||||
version: '0.1.0.0'
|
||||
synopsis: Auto-generated {{package}} API Client
|
||||
description: ! '
|
||||
|
||||
Client library for calling the {{package}} API based on http-client.
|
||||
|
||||
host: {{host}}
|
||||
|
||||
|
||||
base path: {{basePath}}
|
||||
|
||||
|
||||
apiVersion: {{apiVersion}}
|
||||
|
||||
|
||||
swagger version: {{swaggerVersion}}
|
||||
|
||||
|
||||
{{^hideGenerationTimestamp}}Generated on: {{generatedDate}}
|
||||
|
||||
|
||||
{{/hideGenerationTimestamp}}OpenAPI-Specification: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/{{swaggerVersion}}.md
|
||||
'
|
||||
category: Web
|
||||
author: Author Name Here
|
||||
maintainer: author.name@email.com
|
||||
copyright: YEAR - AUTHOR
|
||||
license: UnspecifiedLicense
|
||||
homepage: https://github.com/swagger-api/swagger-codegen#readme
|
||||
extra-source-files:
|
||||
- README.md
|
||||
- swagger.json
|
||||
dependencies:
|
||||
- base >=4.7 && <5.0
|
||||
- transformers >=0.4.0.0
|
||||
- mtl >=2.2.1
|
||||
- unordered-containers
|
||||
library:
|
||||
source-dirs: lib
|
||||
ghc-options: -Wall
|
||||
exposed-modules:
|
||||
- {{title}}
|
||||
- {{title}}.API
|
||||
- {{title}}.Client
|
||||
- {{title}}.Model
|
||||
- {{title}}.MimeTypes
|
||||
{{#generateLenses}}- {{title}}.Lens{{/generateLenses}}
|
||||
dependencies:
|
||||
- aeson >=1.0 && <2.0
|
||||
- bytestring >=0.10.0 && <0.11
|
||||
- containers >=0.5.0.0 && <0.6
|
||||
- http-types >=0.8 && <0.10
|
||||
- http-client >=0.5 && <0.6
|
||||
- http-client-tls
|
||||
- http-api-data >= 0.3.4 && <0.4
|
||||
- http-media >= 0.4 && < 0.8
|
||||
- text >=0.11 && <1.3
|
||||
- time >=1.5 && <1.9
|
||||
- iso8601-time >=0.1.3 && <0.2.0
|
||||
- vector >=0.10.9 && <0.13
|
||||
- network >=2.6.2 && <2.7
|
||||
- random >=1.1
|
||||
- exceptions >= 0.4
|
||||
- monad-logger >=0.3 && <0.4
|
||||
- safe-exceptions <0.2
|
||||
- case-insensitive
|
||||
tests:
|
||||
tests:
|
||||
main: Test.hs
|
||||
source-dirs: tests
|
||||
ghc-options:
|
||||
- -fno-warn-orphans
|
||||
dependencies:
|
||||
- {{package}}
|
||||
- bytestring >=0.10.0 && <0.11
|
||||
- containers
|
||||
- hspec >=1.8
|
||||
- text
|
||||
- time
|
||||
- iso8601-time
|
||||
- aeson
|
||||
- semigroups
|
||||
- QuickCheck
|
||||
@@ -0,0 +1,8 @@
|
||||
resolver: lts-9.0
|
||||
build:
|
||||
haddock-arguments:
|
||||
haddock-args:
|
||||
- "--odir=./docs"
|
||||
extra-deps: []
|
||||
packages:
|
||||
- '.'
|
||||
@@ -0,0 +1,81 @@
|
||||
{-# LANGUAGE DefaultSignatures #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module ApproxEq where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock
|
||||
import Test.QuickCheck
|
||||
import GHC.Generics as G
|
||||
|
||||
(==~)
|
||||
:: (ApproxEq a, Show a)
|
||||
=> a -> a -> Property
|
||||
a ==~ b = counterexample (show a ++ " !=~ " ++ show b) (a =~ b)
|
||||
|
||||
class GApproxEq f where
|
||||
gApproxEq :: f a -> f a -> Bool
|
||||
|
||||
instance GApproxEq U1 where
|
||||
gApproxEq U1 U1 = True
|
||||
|
||||
instance (GApproxEq a, GApproxEq b) =>
|
||||
GApproxEq (a :+: b) where
|
||||
gApproxEq (L1 a) (L1 b) = gApproxEq a b
|
||||
gApproxEq (R1 a) (R1 b) = gApproxEq a b
|
||||
gApproxEq _ _ = False
|
||||
|
||||
instance (GApproxEq a, GApproxEq b) =>
|
||||
GApproxEq (a :*: b) where
|
||||
gApproxEq (a1 :*: b1) (a2 :*: b2) = gApproxEq a1 a2 && gApproxEq b1 b2
|
||||
|
||||
instance (ApproxEq a) =>
|
||||
GApproxEq (K1 i a) where
|
||||
gApproxEq (K1 a) (K1 b) = a =~ b
|
||||
|
||||
instance (GApproxEq f) =>
|
||||
GApproxEq (M1 i t f) where
|
||||
gApproxEq (M1 a) (M1 b) = gApproxEq a b
|
||||
|
||||
class ApproxEq a where
|
||||
(=~) :: a -> a -> Bool
|
||||
default (=~) :: (Generic a, GApproxEq (Rep a)) => a -> a -> Bool
|
||||
a =~ b = gApproxEq (G.from a) (G.from b)
|
||||
|
||||
instance ApproxEq Text where
|
||||
(=~) = (==)
|
||||
|
||||
instance ApproxEq Char where
|
||||
(=~) = (==)
|
||||
|
||||
instance ApproxEq Bool where
|
||||
(=~) = (==)
|
||||
|
||||
instance ApproxEq Int where
|
||||
(=~) = (==)
|
||||
|
||||
instance ApproxEq Double where
|
||||
(=~) = (==)
|
||||
|
||||
instance ApproxEq a =>
|
||||
ApproxEq (Maybe a)
|
||||
|
||||
instance ApproxEq UTCTime where
|
||||
(=~) = (==)
|
||||
|
||||
instance ApproxEq a =>
|
||||
ApproxEq [a] where
|
||||
as =~ bs = and (zipWith (=~) as bs)
|
||||
|
||||
instance (ApproxEq l, ApproxEq r) =>
|
||||
ApproxEq (Either l r) where
|
||||
Left a =~ Left b = a =~ b
|
||||
Right a =~ Right b = a =~ b
|
||||
_ =~ _ = False
|
||||
|
||||
instance (ApproxEq l, ApproxEq r) =>
|
||||
ApproxEq (l, r) where
|
||||
(=~) (l1, r1) (l2, r2) = l1 =~ l2 && r1 =~ r2
|
||||
@@ -0,0 +1,53 @@
|
||||
module Instances where
|
||||
|
||||
import Data.Text (Text, pack)
|
||||
import Data.Char (isSpace)
|
||||
import Data.List (sort)
|
||||
import Data.Time.Calendar (Day(..))
|
||||
import Data.Time.Clock (UTCTime(..), secondsToDiffTime)
|
||||
import Test.QuickCheck
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Set as Set
|
||||
|
||||
import ApproxEq
|
||||
import {{title}}.Model
|
||||
|
||||
instance Arbitrary Text where
|
||||
arbitrary = pack <$> arbitrary
|
||||
|
||||
instance Arbitrary Day where
|
||||
arbitrary = ModifiedJulianDay . (2000 +) <$> arbitrary
|
||||
shrink = (ModifiedJulianDay <$>) . shrink . toModifiedJulianDay
|
||||
|
||||
instance Arbitrary UTCTime where
|
||||
arbitrary =
|
||||
UTCTime <$> arbitrary <*> (secondsToDiffTime <$> choose (0, 86401))
|
||||
|
||||
-- | Checks if a given list has no duplicates in _O(n log n)_.
|
||||
hasNoDups
|
||||
:: (Ord a)
|
||||
=> [a] -> Bool
|
||||
hasNoDups = go Set.empty
|
||||
where
|
||||
go _ [] = True
|
||||
go s (x:xs)
|
||||
| s' <- Set.insert x s
|
||||
, Set.size s' > Set.size s = go s' xs
|
||||
| otherwise = False
|
||||
|
||||
instance ApproxEq Day where
|
||||
(=~) = (==)
|
||||
|
||||
-- * Models
|
||||
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
instance Arbitrary {{classname}} where
|
||||
arbitrary =
|
||||
{{classname}}
|
||||
<$> {{#vars}}arbitrary -- {{name}} :: {{^required}}Maybe {{/required}}{{datatype}}
|
||||
{{#hasMore}}<*> {{/hasMore}}{{/vars}}
|
||||
|
||||
{{/model}}
|
||||
{{/models}}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
|
||||
module PropMime where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Types (parseEither)
|
||||
import Data.Monoid ((<>))
|
||||
import Data.Typeable (Proxy(..), typeOf, Typeable)
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL8
|
||||
import Test.Hspec
|
||||
import Test.QuickCheck
|
||||
import Test.QuickCheck.Property
|
||||
import Test.Hspec.QuickCheck (prop)
|
||||
|
||||
import {{title}}.MimeTypes
|
||||
|
||||
import ApproxEq
|
||||
|
||||
-- * Type Aliases
|
||||
|
||||
type ArbitraryMime mime a = ArbitraryRoundtrip (MimeUnrender mime) (MimeRender mime) a
|
||||
|
||||
type ArbitraryRoundtrip from to a = (from a, to a, Arbitrary' a)
|
||||
|
||||
type Arbitrary' a = (Arbitrary a, Show a, Typeable a)
|
||||
|
||||
-- * Mime
|
||||
|
||||
propMime
|
||||
:: forall a b mime.
|
||||
(ArbitraryMime mime a, Testable b)
|
||||
=> String -> (a -> a -> b) -> mime -> Proxy a -> Spec
|
||||
propMime eqDescr eq m _ =
|
||||
prop
|
||||
(show (typeOf (undefined :: a)) <> " " <> show (typeOf (undefined :: mime)) <> " roundtrip " <> eqDescr) $
|
||||
\(x :: a) ->
|
||||
let rendered = mimeRender' m x
|
||||
actual = mimeUnrender' m rendered
|
||||
expected = Right x
|
||||
failMsg =
|
||||
"ACTUAL: " <> show actual <> "\nRENDERED: " <> BL8.unpack rendered
|
||||
in counterexample failMsg $
|
||||
either reject property (eq <$> actual <*> expected)
|
||||
where
|
||||
reject = property . const rejected
|
||||
|
||||
propMimeEq :: (ArbitraryMime mime a, Eq a) => mime -> Proxy a -> Spec
|
||||
propMimeEq = propMime "(EQ)" (==)
|
||||
@@ -0,0 +1,23 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE PartialTypeSignatures #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Data.Typeable (Proxy(..))
|
||||
import Test.Hspec
|
||||
import Test.Hspec.QuickCheck
|
||||
|
||||
import PropMime
|
||||
import Instances ()
|
||||
|
||||
import {{title}}.Model
|
||||
import {{title}}.MimeTypes
|
||||
|
||||
main :: IO ()
|
||||
main =
|
||||
hspec $ modifyMaxSize (const 10) $
|
||||
do describe "JSON instances" $
|
||||
do {{#models}}{{#model}}propMimeEq MimeJSON (Proxy :: Proxy {{classname}})
|
||||
{{/model}}{{/models}}
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.swagger.codegen.haskellhttpclient;
|
||||
|
||||
import io.swagger.codegen.AbstractOptionsTest;
|
||||
import io.swagger.codegen.CodegenConfig;
|
||||
import io.swagger.codegen.languages.HaskellHttpClientCodegen;
|
||||
import io.swagger.codegen.options.HaskellHttpClientOptionsProvider;
|
||||
import mockit.Expectations;
|
||||
import mockit.Tested;
|
||||
|
||||
public class HaskellHttpClientOptionsTest extends AbstractOptionsTest {
|
||||
|
||||
@Tested
|
||||
private HaskellHttpClientCodegen clientCodegen;
|
||||
|
||||
public HaskellHttpClientOptionsTest() {
|
||||
super(new HaskellHttpClientOptionsProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CodegenConfig getCodegenConfig() {
|
||||
return clientCodegen;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setExpectations() {
|
||||
new Expectations(clientCodegen) {{
|
||||
clientCodegen.setModelPackage(HaskellHttpClientOptionsProvider.MODEL_PACKAGE_VALUE);
|
||||
times = 1;
|
||||
clientCodegen.setApiPackage(HaskellHttpClientOptionsProvider.API_PACKAGE_VALUE);
|
||||
times = 1;
|
||||
clientCodegen.setSortParamsByRequiredFlag(Boolean.valueOf(HaskellHttpClientOptionsProvider.SORT_PARAMS_VALUE));
|
||||
times = 1;
|
||||
|
||||
clientCodegen.setAllowFromJsonNulls(Boolean.valueOf(HaskellHttpClientOptionsProvider.ALLOW_FROMJSON_NULLS));
|
||||
times = 1;
|
||||
clientCodegen.setAllowToJsonNulls(Boolean.valueOf(HaskellHttpClientOptionsProvider.ALLOW_TOJSON_NULLS));
|
||||
times = 1;
|
||||
clientCodegen.setGenerateModelConstructors(Boolean.valueOf(HaskellHttpClientOptionsProvider.GENERATE_MODEL_CONSTRUCTORS));
|
||||
times = 1;
|
||||
clientCodegen.setGenerateFormUrlEncodedInstances(Boolean.valueOf(HaskellHttpClientOptionsProvider.GENERATE_FORM_URLENCODED_INSTANCES));
|
||||
times = 1;
|
||||
clientCodegen.setGenerateLenses(Boolean.valueOf(HaskellHttpClientOptionsProvider.GENERATE_LENSES));
|
||||
times = 1;
|
||||
clientCodegen.setModelDeriving(HaskellHttpClientOptionsProvider.MODEL_DERIVING);
|
||||
times = 1;
|
||||
clientCodegen.setDateTimeFormat(HaskellHttpClientOptionsProvider.DATETIME_FORMAT);
|
||||
times = 1;
|
||||
clientCodegen.setDateFormat(HaskellHttpClientOptionsProvider.DATE_FORMAT);
|
||||
times = 1;
|
||||
|
||||
}};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.swagger.codegen.haskellhttpclient;
|
||||
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class HaskellHttpClientTest {
|
||||
|
||||
@Test(description = "convert a haskell model with dots")
|
||||
public void modelTest() {
|
||||
Assert.assertEquals(true, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.swagger.codegen.options;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import io.swagger.codegen.CodegenConstants;
|
||||
import io.swagger.codegen.languages.HaskellHttpClientCodegen;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class HaskellHttpClientOptionsProvider implements OptionsProvider {
|
||||
public static final String MODEL_PACKAGE_VALUE = "Model";
|
||||
public static final String API_PACKAGE_VALUE = "Api";
|
||||
public static final String SORT_PARAMS_VALUE = "false";
|
||||
public static final String ENSURE_UNIQUE_PARAMS_VALUE = "true";
|
||||
public static final String ALLOW_UNICODE_IDENTIFIERS_VALUE = "false";
|
||||
public static final String HIDE_GENERATION_TIMESTAMP = "true";
|
||||
|
||||
public static final String ALLOW_FROMJSON_NULLS = "true";
|
||||
public static final String ALLOW_TOJSON_NULLS = "false";
|
||||
public static final String DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%Q%z";
|
||||
public static final String DATE_FORMAT = "%Y-%m-%d";
|
||||
public static final String MODEL_DERIVING = "";
|
||||
public static final String GENERATE_FORM_URLENCODED_INSTANCES = "true";
|
||||
public static final String GENERATE_LENSES = "true";
|
||||
public static final String GENERATE_MODEL_CONSTRUCTORS = "true";
|
||||
|
||||
@Override
|
||||
public String getLanguage() {
|
||||
return "haskell-http-client";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> createOptions() {
|
||||
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<String, String>();
|
||||
return builder.put(CodegenConstants.MODEL_PACKAGE, MODEL_PACKAGE_VALUE)
|
||||
.put(CodegenConstants.API_PACKAGE, API_PACKAGE_VALUE)
|
||||
.put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, SORT_PARAMS_VALUE)
|
||||
.put(CodegenConstants.ENSURE_UNIQUE_PARAMS, ENSURE_UNIQUE_PARAMS_VALUE)
|
||||
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
|
||||
.put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, HIDE_GENERATION_TIMESTAMP)
|
||||
|
||||
.put(HaskellHttpClientCodegen.ALLOW_FROMJSON_NULLS, ALLOW_FROMJSON_NULLS)
|
||||
.put(HaskellHttpClientCodegen.ALLOW_TOJSON_NULLS, ALLOW_TOJSON_NULLS)
|
||||
.put(HaskellHttpClientCodegen.DATETIME_FORMAT, DATETIME_FORMAT)
|
||||
.put(HaskellHttpClientCodegen.DATE_FORMAT, DATE_FORMAT)
|
||||
.put(HaskellHttpClientCodegen.MODEL_DERIVING, MODEL_DERIVING)
|
||||
.put(HaskellHttpClientCodegen.GENERATE_FORM_URLENCODED_INSTANCES, GENERATE_FORM_URLENCODED_INSTANCES)
|
||||
.put(HaskellHttpClientCodegen.GENERATE_LENSES, GENERATE_LENSES)
|
||||
.put(HaskellHttpClientCodegen.GENERATE_MODEL_CONSTRUCTORS, GENERATE_MODEL_CONSTRUCTORS)
|
||||
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isServer() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user