add haskell-http-client-generator (#6429)

This commit is contained in:
Jon Schoning
2017-09-05 11:33:48 -05:00
committed by wing328
parent 4eab5406c5
commit c7d145a4ba
117 changed files with 10499 additions and 0 deletions

View File

@@ -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);
// }
}

View File

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

View File

@@ -0,0 +1,8 @@
.stack-work
src/highlight.js
src/style.css
dist
dist-newstyle
cabal.project.local
.cabal-sandbox
cabal.sandbox.config

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

View File

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

View File

@@ -0,0 +1 @@
toForm{{#collectionFormat}}Coll {{vendorExtensions.x-collectionFormat}}{{/collectionFormat}}

View File

@@ -0,0 +1 @@
toHeader{{#collectionFormat}}Coll {{vendorExtensions.x-collectionFormat}}{{/collectionFormat}}

View File

@@ -0,0 +1 @@
toQuery{{#collectionFormat}}Coll {{vendorExtensions.x-collectionFormat}}{{/collectionFormat}}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
resolver: lts-9.0
build:
haddock-arguments:
haddock-args:
- "--odir=./docs"
extra-deps: []
packages:
- '.'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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