From a694dfb8fbcc5442c7b0f920d2baad073fc6d180 Mon Sep 17 00:00:00 2001 From: Andrew Gibiansky Date: Mon, 29 Feb 2016 09:05:15 -0800 Subject: [PATCH] Create new Haskell codegen implementation. --- modules/swagger-codegen/pom.xml | 8 + .../languages/HaskellServantCodegen.java | 689 +++++++++++------- .../resources/haskell-servant/API.mustache | 150 ++++ .../resources/haskell-servant/Apis.mustache | 26 - .../resources/haskell-servant/Client.mustache | 35 - .../main/resources/haskell-servant/LICENSE | 202 ----- .../resources/haskell-servant/README.mustache | 82 ++- .../resources/haskell-servant/Server.mustache | 13 - .../resources/haskell-servant/Types.mustache | 63 ++ .../resources/haskell-servant/Utils.mustache | 27 - .../resources/haskell-servant/api.mustache | 82 --- .../haskell-servant-codegen.mustache | 51 +- .../resources/haskell-servant/model.mustache | 34 - .../resources/haskell-servant/stack.mustache | 32 +- 14 files changed, 725 insertions(+), 769 deletions(-) create mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/API.mustache delete mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/Apis.mustache delete mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/Client.mustache delete mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/LICENSE delete mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/Server.mustache create mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/Types.mustache delete mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/Utils.mustache delete mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/api.mustache delete mode 100644 modules/swagger-codegen/src/main/resources/haskell-servant/model.mustache diff --git a/modules/swagger-codegen/pom.xml b/modules/swagger-codegen/pom.xml index a5c4eedd7406..2f05b690f187 100644 --- a/modules/swagger-codegen/pom.xml +++ b/modules/swagger-codegen/pom.xml @@ -115,6 +115,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/HaskellServantCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/HaskellServantCodegen.java index 23f431d9a93b..ebfce0c763cd 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/HaskellServantCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/HaskellServantCodegen.java @@ -1,345 +1,490 @@ package io.swagger.codegen.languages; import io.swagger.codegen.*; +import io.swagger.models.ModelImpl; +import io.swagger.models.parameters.Parameter; import io.swagger.models.properties.*; import io.swagger.models.Model; import io.swagger.models.Operation; import io.swagger.models.Swagger; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; import java.util.*; import java.io.File; public class HaskellServantCodegen extends DefaultCodegen implements CodegenConfig { - // source folder where to write the files - protected String sourceFolder = "src"; - protected String apiVersion = "0.0.1"; + // source folder where to write the files + protected String sourceFolder = "src"; + protected String apiVersion = "0.0.1"; - /** - * Configures the type of generator. - * - * @return the CodegenType for this generator - * @see io.swagger.codegen.CodegenType - */ - public CodegenType getTag() { - return CodegenType.SERVER; - } - /** - * Configures a friendly name for the generator. This will be used by the generator - * to select the library with the -l flag. - * - * @return the friendly name for the generator - */ - public String getName() { - return "haskell-servant"; - } - - /** - * Returns human-friendly help for the generator. Provide the consumer with help - * tips, parameters here - * - * @return A string value for the help message - */ - public String getHelp() { - return "Generates a HaskellServantCodegen library."; - } - - public HaskellServantCodegen() { - super(); - - // set the output folder here - outputFolder = "generated-code/HaskellServantCodegen"; + // How to encode special characters like $ + // They are translated to words like "Dollar" and prefixed with ' + // Then translated back during JSON encoding and decoding + private Map specialCharReplacements = new HashMap(); /** - * Models. You can write model files using the modelTemplateFiles map. - * if you want to create one template for file, you can do so here. - * for multiple files for model, just put another entry in the `modelTemplateFiles` with - * a different extension + * Configures the type of generator. + * + * @return the CodegenType for this generator + * @see io.swagger.codegen.CodegenType */ - modelTemplateFiles.put( - "model.mustache", // the template to use - ".hs"); // the extension for each file to write + public CodegenType getTag() { + return CodegenType.SERVER; + } /** - * Api classes. You can write classes for each Api file with the apiTemplateFiles map. - * as with models, add multiple entries with different extensions for multiple files per - * class + * Configures a friendly name for the generator. This will be used by the generator + * to select the library with the -l flag. + * + * @return the friendly name for the generator */ - apiTemplateFiles.put( - "api.mustache", // the template to use - ".hs"); // the extension for each file to write + public String getName() { + return "haskell"; + } /** + * Returns human-friendly help for the generator. Provide the consumer with help + * tips, parameters here + * + * @return A string value for the help message + */ + public String getHelp() { + return "Generates a Haskell server and client library."; + } + + public HaskellServantCodegen() { + super(); + + // Initialize special characters + specialCharReplacements.put('$', "Dollar"); + specialCharReplacements.put('^', "Caret"); + specialCharReplacements.put('|', "Pipe"); + specialCharReplacements.put('=', "Equal"); + specialCharReplacements.put('*', "Star"); + specialCharReplacements.put('-', "Dash"); + specialCharReplacements.put('&', "Ampersand"); + specialCharReplacements.put('%', "Percent"); + specialCharReplacements.put('#', "Hash"); + specialCharReplacements.put('@', "At"); + specialCharReplacements.put('!', "Exclamation"); + specialCharReplacements.put('+', "Plus"); + + + // set the output folder here + outputFolder = "generated-code/haskell-servant"; + + /* * Template Location. This is the location which templates will be read from. The generator * will use the resource stream to attempt to read the templates. */ - embeddedTemplateDir = templateDir = "haskell-servant"; + embeddedTemplateDir = templateDir = "haskell-servant"; - /** + /* * Api Package. Optional, if needed, this can be used in templates */ - apiPackage = "Api"; + apiPackage = "API"; - /** + /* * Model Package. Optional, if needed, this can be used in templates */ - modelPackage = "Model"; + modelPackage = "Types"; - /** - * Reserved words. Override this with reserved words specific to your language - */ - // from https://wiki.haskell.org/Keywords - setReservedWordsLowerCase( - Arrays.asList( - "as", "case", "of", - "class", "data", // "data family", "data instance", - "default", "deriving", // "deriving instance", - "do", - "forall", "foreign", "hiding", - "id", - "if", "then", "else", - "import", "infix", "infixl", "infixr", - "instance", "let", "in", - "mdo", "module", "newtype", - "proc", "qualified", "rec", - "type", // "type family", "type instance", - "where" - ) - ); + // 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" + ) + ); - /** + /* * Additional Properties. These values can be passed to the templates and * are available in models, apis, and supporting files */ - additionalProperties.put("apiVersion", apiVersion); + additionalProperties.put("apiVersion", apiVersion); - /** + /* * Supporting Files. You can write single files for the generator with the * entire object tree available. If the input file has a suffix of `.mustache * it will be processed by the template engine. Otherwise, it will be copied */ - supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); - supportingFiles.add(new SupportingFile("stack.mustache", "", "stack.yaml")); - supportingFiles.add(new SupportingFile("haskell-servant-codegen.mustache", "", "haskell-servant-codegen.cabal")); - supportingFiles.add(new SupportingFile("Setup.mustache", "", "Setup.hs")); - supportingFiles.add(new SupportingFile("LICENSE", "", "LICENSE")); - supportingFiles.add(new SupportingFile("Apis.mustache", "lib", "Apis.hs")); - supportingFiles.add(new SupportingFile("Utils.mustache", "lib", "Utils.hs")); - supportingFiles.add(new SupportingFile("Client.mustache", "client", "Main.hs")); - supportingFiles.add(new SupportingFile("Server.mustache", "server", "Main.hs")); + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("stack.mustache", "", "stack.yaml")); + supportingFiles.add(new SupportingFile("Setup.mustache", "", "Setup.hs")); - /** + /* * Language Specific Primitives. These types will not trigger imports by * the client generator */ - languageSpecificPrimitives = new HashSet( - Arrays.asList( - "Bool", - "String", - "Int", - "Integer", - "Float", - "Char", - "Double", - "List", - "FilePath" - ) - ); + languageSpecificPrimitives = new HashSet( + Arrays.asList( + "Bool", + "String", + "Int", + "Integer", + "Float", + "Char", + "Double", + "List", + "FilePath" + ) + ); - typeMapping.clear(); - // typeMapping.put("enum", "NSString"); - typeMapping.put("array", "List"); - typeMapping.put("set", "Set"); - typeMapping.put("boolean", "Bool"); - typeMapping.put("string", "String"); - typeMapping.put("int", "Int"); - typeMapping.put("long", "Integer"); - typeMapping.put("float", "Float"); - // typeMapping.put("byte", "Byte"); - typeMapping.put("short", "Int"); - typeMapping.put("char", "Char"); - typeMapping.put("double", "Double"); - typeMapping.put("DateTime", "Integer"); - // typeMapping.put("object", "Map"); - typeMapping.put("file", "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("DateTime", "Integer"); + typeMapping.put("file", "FilePath"); + typeMapping.put("number", "Double"); + typeMapping.put("integer", "Int"); - importMapping.clear(); - importMapping.put("Map", "qualified Data.Map as Map"); + 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)); - } - - /** - * Escapes a reserved word as defined in the `reservedWords` array. Handle escaping - * those terms here. This logic is only called if a variable matches the reseved words - * - * @return the escaped term - */ - @Override - public String escapeReservedWord(String name) { - return name + "_"; - } - - /** - * Location to write model files. You can use the modelPackage() as defined when the class is - * instantiated - */ - public String modelFileFolder() { - return outputFolder + File.separatorChar + "lib" + File.separatorChar + modelPackage().replace('.', File.separatorChar); - } - - /** - * Location to write api files. You can use the apiPackage() as defined when the class is - * instantiated - */ - @Override - public String apiFileFolder() { - return outputFolder + File.separatorChar + "lib" + File.separatorChar + apiPackage().replace('.', File.separatorChar); - } - - /** - * Optional - type declaration. This is a String which is used by the templates to instantiate your - * types. There is typically special handling for different property types - * - * @return a string value used as the `dataType` field for model templates, `returnType` for api templates - */ - @Override - public String getTypeDeclaration(Property p) { - if(p instanceof ArrayProperty) { - ArrayProperty ap = (ArrayProperty) p; - Property inner = ap.getItems(); - return "[" + getTypeDeclaration(inner) + "]"; + cliOptions.add(new CliOption(CodegenConstants.MODEL_PACKAGE, CodegenConstants.MODEL_PACKAGE_DESC)); + cliOptions.add(new CliOption(CodegenConstants.API_PACKAGE, CodegenConstants.API_PACKAGE_DESC)); } - else if (p instanceof MapProperty) { - MapProperty mp = (MapProperty) p; - Property inner = mp.getAdditionalProperties(); - return "Map.Map String " + getTypeDeclaration(inner); - } - return super.getTypeDeclaration(p); - } - /** - * Optional - swagger type conversion. This is used to map swagger types in a `Property` into - * either language specific types via `typeMapping` or into complex models if there is not a mapping. - * - * @return a string value of the type or complex model for this property - * @see io.swagger.models.properties.Property - */ - @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)) + /** + * Escapes a reserved word as defined in the `reservedWords` array. Handle escaping + * those terms here. This logic is only called if a variable matches the reseved words + * + * @return the escaped term + */ + @Override + public String escapeReservedWord(String name) { + return name + "_"; + } + + @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 + 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 wordsLower = new ArrayList(); + for (String word : words) { + wordsLower.add(word.toLowerCase()); + } + String cabalName = joinStrings("-", wordsLower); + + // The API name is made by appending the capitalized words of the title + List wordsCaps = new ArrayList(); + for (String word : words) { + wordsCaps.add(word.substring(0, 1).toUpperCase() + word.substring(1)); + } + String apiName = joinStrings("", wordsCaps); + + // Set the filenames to write for the API + supportingFiles.add(new SupportingFile("haskell-servant-codegen.mustache", "", cabalName + ".cabal")); + supportingFiles.add(new SupportingFile("API.mustache", "lib/" + apiName, "API.hs")); + supportingFiles.add(new SupportingFile("Types.mustache", "lib/" + apiName, "Types.hs")); + + + additionalProperties.put("title", apiName); + additionalProperties.put("titleLower", apiName.substring(0, 1).toLowerCase() + apiName.substring(1)); + additionalProperties.put("package", cabalName); + + List> replacements = new ArrayList<>(); + Object[] replacementChars = specialCharReplacements.keySet().toArray(); + for(int i = 0; i < replacementChars.length; i++) { + Character c = (Character) replacementChars[i]; + Map o = new HashMap<>(); + o.put("char", Character.toString(c)); + o.put("replacement", "'" + specialCharReplacements.get(c)); + o.put("hasMore", i != replacementChars.length - 1); + replacements.add(o); + } + additionalProperties.put("specialCharReplacements", replacements); + + super.preprocessSwagger(swagger); + } + + + /** + * Optional - type declaration. This is a String which is used by the templates to instantiate your + * types. There is typically special handling for different property types + * + * @return a string value used as the `dataType` field for model templates, `returnType` for api templates + */ + @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 super.getTypeDeclaration(p); + } + + /** + * Optional - swagger type conversion. This is used to map swagger types in a `Property` into + * either language specific types via `typeMapping` or into complex models if there is not a mapping. + * + * @return a string value of the type or complex model for this property + * @see io.swagger.models.properties.Property + */ + @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 { + type = swaggerType; + } return toModelName(type); } - else - type = swaggerType; - return toModelName(type); - } - private String capturePath(String path, List pathParams) { - for (CodegenParameter p : pathParams) { - String pName = "{"+p.baseName+"}"; - if (path.indexOf(pName) >= 0) { - path = path.replace(pName, "Capture " + "\""+p.baseName+"\" " + p.dataType); - } + @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; + } } - return path; - } - private String queryPath(String path, List queryParams) { - for (CodegenParameter p : queryParams) { - path += " :> QueryParam \"" + p.baseName + "\" " + p.dataType; + + // Intersperse a separator string between a list of strings, like String.join. + private String joinStrings(String sep, List ss) { + StringBuilder sb = new StringBuilder(); + for (String s : ss) { + if (sb.length() > 0) { + sb.append(sep); + } + sb.append(s); + } + return sb.toString(); } - return path; - } - private String bodyPath(String path, List bodyParams) { - for (CodegenParameter p : bodyParams) { - path += " :> ReqBody '[JSON] " + p.dataType; + // Convert an HTTP path to a Servant route, including captured parameters. + // For example, the path /api/jobs/info/{id}/last would become: + // "api" :> "jobs" :> "info" :> Capture "id" IdType :> "last" + // IdType is provided by the capture params. + private List pathToServantRoute(String path, List pathParams) { + // Map the capture params by their names. + HashMap captureTypes = new HashMap(); + for (CodegenParameter param : pathParams) { + captureTypes.put(param.baseName, param.dataType); + } + + // Cut off the leading slash, if it is present. + if (path.startsWith("/")) { + path = path.substring(1); + } + + // Convert the path into a list of servant route components. + List pathComponents = new ArrayList(); + for (String piece : path.split("/")) { + if (piece.startsWith("{") && piece.endsWith("}")) { + String name = piece.substring(1, piece.length() - 1); + pathComponents.add("Capture \"" + name + "\" " + captureTypes.get(name)); + } else { + pathComponents.add("\"" + piece + "\""); + } + } + + // Intersperse the servant route pieces with :> to construct the final API type + return pathComponents; } - return path; - } - private String formPath(String path, List formParams) { - String names = "Form"; - for (CodegenParameter p : formParams) { - if(p.dataType.equals("FilePath")){ - // file data processing - } - names += p.baseName; + // Extract the arguments that are passed in the route path parameters + private List pathToClientType(String path, List pathParams) { + // Map the capture params by their names. + HashMap captureTypes = new HashMap(); + for (CodegenParameter param : pathParams) { + captureTypes.put(param.baseName, param.dataType); + } + + // Cut off the leading slash, if it is present. + if (path.startsWith("/")) { + path = path.substring(1); + } + + // Convert the path into a list of servant route components. + List type = new ArrayList(); + for (String piece : path.split("/")) { + if (piece.startsWith("{") && piece.endsWith("}")) { + String name = piece.substring(1, piece.length() - 1); + type.add(captureTypes.get(name)); + } + } + + return type; } - if(formParams.size() > 0){ - path += " :> ReqBody '[FormUrlEncoded] " + names; + + + @Override + public CodegenOperation fromOperation(String resourcePath, String httpMethod, Operation operation, Map definitions, Swagger swagger) { + CodegenOperation op = super.fromOperation(resourcePath, httpMethod, operation, definitions, swagger); + + List path = pathToServantRoute(op.path, op.pathParams); + List type = pathToClientType(op.path, op.pathParams); + + // Query parameters appended to routes + for (CodegenParameter param : op.queryParams) { + String paramType = param.dataType; + if(param.isListContainer != null && param.isListContainer) { + paramType = makeQueryListType(paramType, param.collectionFormat); + } + path.add("QueryParam \"" + param.baseName + "\" " + paramType); + type.add("Maybe " + param.dataType); + } + + // Either body or form data parameters appended to route + // As far as I know, you cannot have two ReqBody routes. + // Is it possible to have body params AND have form params? + String bodyType = null; + if (op.getHasBodyParam()) { + for (CodegenParameter param : op.bodyParams) { + path.add("ReqBody '[JSON] " + param.dataType); + bodyType = param.dataType; + } + } else if(op.getHasFormParams()) { + // Use the FormX data type, where X is the conglomerate of all things being passed + String formName = "Form" + camelize(op.operationId); + bodyType = formName; + path.add("ReqBody '[FormUrlEncoded] " + formName); + } + if(bodyType != null) { + type.add(bodyType); + } + + // Special headers appended to route + for (CodegenParameter param : op.headerParams) { + path.add("Header \"" + param.baseName + "\" " + param.dataType); + + String paramType = param.dataType; + if(param.isListContainer != null && param.isListContainer) { + paramType = makeQueryListType(paramType, param.collectionFormat); + } + type.add("Maybe " + paramType); + } + + // Add the HTTP method and return type + String returnType = op.returnType; + if (returnType == null || returnType.equals("null")) { + returnType = "()"; + } + if (returnType.indexOf(" ") >= 0) { + returnType = "(" + returnType + ")"; + } + path.add(camelize(op.httpMethod.toLowerCase()) + " '[JSON] " + returnType); + type.add("m " + returnType); + + op.vendorExtensions.put("x-routeType", joinStrings(" :> ", path)); + op.vendorExtensions.put("x-clientType", joinStrings(" -> ", type)); + op.vendorExtensions.put("x-formName", "Form" + camelize(op.operationId)); + for(CodegenParameter param : op.formParams) { + param.vendorExtensions.put("x-formPrefix", camelize(op.operationId, true)); + } + return op; } - return path; - } - private String headerPath(String path, List headerParams) { - for (CodegenParameter p : headerParams) { - path += " :> Header \"" + p.baseName + "\" " + p.dataType; + private String makeQueryListType(String type, String collectionFormat) { + type = type.substring(1, type.length() - 1); + switch(collectionFormat) { + case "csv": return "(QueryList 'CommaSeparated (" + type + "))"; + case "tsv": return "(QueryList 'TabSeparated (" + type + "))"; + case "ssv": return "(QueryList 'SpaceSeparated (" + type + "))"; + case "pipes": return "(QueryList 'PipeSeparated (" + type + "))"; + case "multi": return "(QueryList 'MultiParamArray (" + type + "))"; + default: + throw new NotImplementedException(); + } } - return path; - } - - private String filterReturnType(String rt) { - if (rt == null || rt.equals("null")) { - return "()"; - } else if (rt.indexOf(" ") >= 0) { - return "(" + rt + ")"; + private String fixOperatorChars(String string) { + StringBuilder sb = new StringBuilder(); + for (char c : string.toCharArray()) { + if (specialCharReplacements.containsKey(c)) { + sb.append(specialCharReplacements.get(c)); + } else { + sb.append(c); + } + } + return sb.toString(); } - return rt; - } - private String addReturnPath(String path, String httpMethod, String returnType) { - return path + " :> " + upperCaseFirst(httpMethod) + " '[JSON] " + filterReturnType(returnType); - } + // Override fromModel to create the appropriate model namings + @Override + public CodegenModel fromModel(String name, Model mod, Map allDefinitions) { + CodegenModel model = super.fromModel(name, mod, allDefinitions); - private String joinStrings(String sep, List ss) { - StringBuilder sb = new StringBuilder(); - for (String s : ss) { - if (sb.length() > 0) { - sb.append(sep); - } - sb.append(s); + // From the model name, compute the prefix for the fields. + String prefix = camelize(model.classname, true); + for(CodegenProperty prop : model.vars) { + prop.name = prefix + camelize(fixOperatorChars(prop.name)); + } + + // Create newtypes for things with non-object types + String dataOrNewtype = "data"; + String modelType = ((ModelImpl) mod).getType(); + if(modelType != "object" && typeMapping.containsKey(modelType)) { + String newtype = typeMapping.get(modelType); + model.vendorExtensions.put("x-customNewtype", newtype); + } + + // Provide the prefix as a vendor extension, so that it can be used in the ToJSON and FromJSON instances. + model.vendorExtensions.put("x-prefix", prefix); + model.vendorExtensions.put("x-data", dataOrNewtype); + + return model; } - return sb.toString(); - } - private String replacePathSplitter(String path) { - String[] ps = path.replaceFirst("/", "").split("/", 0); - List rs = new ArrayList(); - for (String p : ps) { - if (p.indexOf("{") < 0) { - rs.add("\"" + p + "\""); - } else { - rs.add(p); - } + @Override + public CodegenParameter fromParameter(Parameter param, Set imports) { + CodegenParameter p = super.fromParameter(param, imports); + p.vendorExtensions.put("x-formParamName", camelize(p.baseName)); + return p; } - return joinStrings(" :> ", rs); - } - - private String upperCaseFirst(String str) { - char[] array = str.toLowerCase().toCharArray(); - array[0] = Character.toUpperCase(array[0]); - return new String(array); - } - - private String parseScheme(String basePath) { - return "Http"; - } - - @Override - public CodegenOperation fromOperation(String resourcePath, String httpMethod, Operation operation, Map definitions, Swagger swagger){ - CodegenOperation op = super.fromOperation(resourcePath, httpMethod, operation, definitions, swagger); - String path = op.path; - op.nickname = addReturnPath(headerPath(formPath(bodyPath(queryPath(capturePath(replacePathSplitter(path), op.pathParams), op.queryParams), op.bodyParams), op.formParams), op.headerParams), op.httpMethod, op.returnType); - return op; - } } diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/API.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/API.mustache new file mode 100644 index 000000000000..fef77e9b1cb0 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/haskell-servant/API.mustache @@ -0,0 +1,150 @@ +{-# LANGUAGE DataKinds, TypeFamilies, TypeOperators, FlexibleInstances, OverloadedStrings, ViewPatterns #-} +{-# LANGUAGE RecordWildCards, GeneralizedNewtypeDeriving, DeriveTraversable, FlexibleContexts, DeriveGeneric #-} +module {{title}}.API ( + -- * Client and Server + ServerConfig(..), + {{title}}Backend, + create{{title}}Client, + run{{title}}Server, + -- ** Servant + {{title}}API, + ) where + +import {{title}}.Types + +import Data.Coerce (coerce) +import Servant.API +import Servant (serve, ServantErr) +import qualified Network.Wai.Handler.Warp as Warp +import Control.Monad.Trans.Either (EitherT) +import qualified Data.Text as T +import Data.Text (Text) +import Servant.Common.BaseUrl(BaseUrl(..)) +import Servant.Client (ServantError, client, Scheme(..)) +import Data.Proxy (Proxy(..)) +import Control.Monad.IO.Class +import Data.Function ((&)) +import GHC.Exts (IsString(..)) +import qualified Data.Map as Map +import GHC.Generics (Generic) +import Data.Monoid ((<>)) + + +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}{{#hasFormParams}} +data {{vendorExtensions.x-formName}} = {{vendorExtensions.x-formName}} + { {{#formParams}}{{vendorExtensions.x-formPrefix}}{{vendorExtensions.x-formParamName}} :: {{dataType}}{{#hasMore}} + , {{/hasMore}}{{/formParams}} + } deriving (Show, Eq, Generic) + +instance FromFormUrlEncoded {{vendorExtensions.x-formName}} where + fromFormUrlEncoded inputs = {{vendorExtensions.x-formName}} <$> {{#formParams}} lookupEither "{{baseName}}" inputs{{#hasMore}} <*> {{/hasMore}}{{/formParams}} +instance ToFormUrlEncoded {{vendorExtensions.x-formName}} where + toFormUrlEncoded value = [{{#formParams}}("{{baseName}}", toText $ {{vendorExtensions.x-formPrefix}}{{vendorExtensions.x-formParamName}} value){{#hasMore}}, {{/hasMore}}{{/formParams}}] +{{/hasFormParams}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} + +-- For the form data code generation. +lookupEither :: FromText b => Text -> [(Text, Text)] -> Either String b +lookupEither key assocs = + case lookup key assocs >>= fromText of + Nothing -> Left $ T.unpack $ "Could not find parameter " <> key <> " in form data" + Just value -> Right value + +{{#apiInfo}} +-- | Servant type-level API, generated from the Swagger spec for {{title}}. +type {{title}}API + = {{#apis}}{{#operations}}{{#operation}}{{& vendorExtensions.x-routeType}} -- '{{operationId}}' route{{#hasMore}} + :<|> {{/hasMore}}{{/operation}}{{/operations}}{{#hasMore}} + :<|> {{/hasMore}}{{/apis}} +{{/apiInfo}} + +-- | Server or client configuration, specifying the host and port to query or serve on. +data ServerConfig = ServerConfig { + configHost :: String, -- ^ Hostname to serve on, e.g. "127.0.0.1" + configPort :: Int -- ^ Port to serve on, e.g. 8080 + } deriving (Eq, Ord, Show, Read) + +-- | List of elements parsed from a query. +newtype QueryList (p :: CollectionFormat) a = QueryList { fromQueryList :: [a] } + deriving (Functor, Applicative, Monad, Foldable, Traversable) + +-- | Formats in which a list can be encoded into a HTTP path. +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`. Only for GET params. + +instance FromText a => FromText (QueryList 'CommaSeparated a) where + fromText = parseSeparatedQueryList ',' + +instance FromText a => FromText (QueryList 'TabSeparated a) where + fromText = parseSeparatedQueryList '\t' + +instance FromText a => FromText (QueryList 'SpaceSeparated a) where + fromText = parseSeparatedQueryList ' ' + +instance FromText a => FromText (QueryList 'PipeSeparated a) where + fromText = parseSeparatedQueryList '|' + +instance FromText a => FromText (QueryList 'MultiParamArray a) where + fromText = error "unimplemented FromText for MultiParamArray collection format" + +parseSeparatedQueryList :: FromText a => Char -> Text -> Maybe (QueryList p a) +parseSeparatedQueryList char = fmap QueryList . mapM fromText . T.split (== char) + +instance ToText a => ToText (QueryList 'CommaSeparated a) where + toText = formatSeparatedQueryList ',' + +instance ToText a => ToText (QueryList 'TabSeparated a) where + toText = formatSeparatedQueryList '\t' + +instance ToText a => ToText (QueryList 'SpaceSeparated a) where + toText = formatSeparatedQueryList ' ' + +instance ToText a => ToText (QueryList 'PipeSeparated a) where + toText = formatSeparatedQueryList '|' + +instance ToText a => ToText (QueryList 'MultiParamArray a) where + toText = error "unimplemented ToText for MultiParamArray collection format" + +formatSeparatedQueryList :: ToText a => Char -> QueryList p a -> Text +formatSeparatedQueryList char = T.intercalate (T.singleton char) . map toText . fromQueryList + + +{{#apiInfo}} +-- | Backend for {{title}}. +-- The backend can be used both for the client and the server. The client generated from the {{title}} Swagger spec +-- is a backend that executes actions by sending HTTP requests (see @create{{title}}Client@). Alternatively, provided +-- a backend, the API can be served using @run{{title}}Server@. +data {{title}}Backend m = {{title}}Backend { + {{#apis}}{{#operations}}{{#operation}}{{operationId}} :: {{& vendorExtensions.x-clientType}}{- ^ {{& notes}} -}{{#hasMore}}, + {{/hasMore}}{{/operation}}{{/operations}}{{#hasMore}}, + {{/hasMore}}{{/apis}} + } +{{/apiInfo}} + + +{{#apiInfo}} +create{{title}}Client :: ServerConfig -> {{title}}Backend (EitherT ServantError IO) +create{{title}}Client clientConfig = {{title}}Backend{..} + where + -- Use a strange variable name to avoid conflicts in autogenerated code... (no hygienic templates) + servantBaseUrlForClient3928 = BaseUrl Http (configHost clientConfig) (configPort clientConfig) + ({{#apis}}{{#operations}}{{#operation}}(coerce -> {{operationId}}){{#hasMore}} :<|> + {{/hasMore}}{{/operation}}{{/operations}}{{#hasMore}} :<|> + {{/hasMore}}{{/apis}}) = client (Proxy :: Proxy {{title}}API) servantBaseUrlForClient3928 +{{/apiInfo}} + +{{#apiInfo}} +-- | Run the {{title}} server at the provided host and port. +run{{title}}Server :: MonadIO m => ServerConfig -> {{title}}Backend (EitherT ServantErr IO) -> m () +run{{title}}Server ServerConfig{..} backend = + liftIO $ Warp.runSettings warpSettings $ serve (Proxy :: Proxy {{title}}API) (serverFromBackend backend) + + where + warpSettings = Warp.defaultSettings & Warp.setPort configPort & Warp.setHost (fromString configHost) + serverFromBackend {{title}}Backend{..} = + ({{#apis}}{{#operations}}{{#operation}}coerce {{operationId}}{{#hasMore}} :<|> + {{/hasMore}}{{/operation}}{{/operations}}{{#hasMore}} :<|> + {{/hasMore}}{{/apis}}) +{{/apiInfo}} diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/Apis.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/Apis.mustache deleted file mode 100644 index 3b60afafec67..000000000000 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/Apis.mustache +++ /dev/null @@ -1,26 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE FlexibleInstances #-} -module Apis ( - api - , API - ) where - -{{#apiInfo}} -{{#apis}} -import {{package}}.{{classname}} ({{classname}}) -{{/apis}} -{{/apiInfo}} - -import Data.Proxy -import Servant.API -import Test.QuickCheck -import qualified Data.Map as Map -import Utils - -type API = {{#apiInfo}}{{#apis}}{{classname}}{{#hasMore}} :<|> {{/hasMore}}{{/apis}}{{/apiInfo}} - -api :: Proxy API -api = Proxy diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/Client.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/Client.mustache deleted file mode 100644 index ab48bcbc43e9..000000000000 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/Client.mustache +++ /dev/null @@ -1,35 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeOperators #-} - -module Main where - -import Control.Monad (void) -import Control.Monad.Trans.Either -import Control.Monad.IO.Class -import Servant.API -import Servant.Client - -import Data.List.Split (splitOn) -import Network.URI (URI (..), URIAuth (..), parseURI) -import Data.Maybe (fromMaybe) -import Test.QuickCheck -import Control.Monad -{{#models}} -import {{importPath}} -{{/models}} -{{#apiInfo}} -{{#apis}} -import {{package}}.{{classname}} -{{/apis}} -{{/apiInfo}} - --- userClient :: IO () --- userClient = do --- users <- sample' (arbitrary :: Gen String) --- let user = last users --- void . runEitherT $ do --- getUserByName user >>= (liftIO . putStrLn . show) - -main :: IO () -main = putStrLn "Hello Server!" diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/LICENSE b/modules/swagger-codegen/src/main/resources/haskell-servant/LICENSE deleted file mode 100644 index b0033f5f837b..000000000000 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2015 Masahiro Yamauchi - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/README.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/README.mustache index c8f4b4bbb9db..67068d96ccd4 100644 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/README.mustache +++ b/modules/swagger-codegen/src/main/resources/haskell-servant/README.mustache @@ -1,7 +1,79 @@ -# Generated Servant Codes +# Auto-Generated Swagger Bindings to `{{title}}` -## How to use +The library in `lib` provides auto-generated-from-Swagger bindings to the {{title}} API. -0. Install haskell-stack -1. stack build -2. stack exec client +## 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. Run `stack install` to install this package. + +## Main Interface + +The main interface to this library is in the `{{title}}.API` module, which exports the {{title}}Backend type. The {{title}}Backend +type can be used to create and define servers and clients for the API. + +## Creating a Client + +A client can be created via the `create{{title}}Client` function, which, if provided with a hostname and a port, will generate +a client that can be used to access the API if it is being served at that hostname / port combination. For example, if +`localhost:8080` is serving the {{title}} API, you can write: + +```haskell +{-# LANGUAGE RecordWildCards #-} + +import {{title}}.API + +main :: IO () +main = do + {{title}}Backend{..} <- create{{title}}Client (ServerConfig "localhost" 8080) + -- Any {{title}} API call can go here. + return () +``` + +## Creating a Server + +In order to create a server, you must use the `run{{title}}Server` function. However, you unlike the client, in which case you *got* a `{{title}}Backend` +from the library, you must instead *provide* a `{{title}}Backend`. For example, if you have defined handler functions for all the +functions in `{{title}}.Handlers`, you can write: + +```haskell +{-# LANGUAGE RecordWildCards #-} + +import {{title}}.API + +-- A module you wrote yourself, containing all handlers needed for the {{title}}Backend type. +import {{title}}.Handlers + +-- Run a {{title}} server on localhost:8080 +main :: IO () +main = do + let server = {{title}}Backend{..} + run{{title}}Server (ServerConfig "localhost" 8080) server +``` + +You could use `optparse-applicative` or a similar library to read the host and port from command-line arguments: +``` +{-# LANGUAGE RecordWildCards #-} + +module Main (main) where + +import {{title}}.API (run{{title}}Server, {{title}}Backend(..), ServerConfig(..)) + +import Control.Applicative ((<$>), (<*>)) +import Options.Applicative (execParser, option, str, auto, long, metavar, help) + +main :: IO () +main = do + config <- parseArguments + run{{title}}Server config {{title}}Backend{} + +-- | Parse host and port from the command line arguments. +parseArguments :: IO ServerConfig +parseArguments = + execParser $ + ServerConfig + <$> option str (long "host" <> metavar "HOST" <> help "Host to serve on") + <*> option auto (long "port" <> metavar "PORT" <> help "Port to serve on") +``` diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/Server.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/Server.mustache deleted file mode 100644 index 68b4ff6ce33d..000000000000 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/Server.mustache +++ /dev/null @@ -1,13 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeOperators #-} - -module Main where - -import Apis -import Servant -import Servant.Mock -import qualified Network.Wai.Handler.Warp as Warp - -main :: IO () -main = Warp.run 8080 $ serve api (mock api) diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/Types.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/Types.mustache new file mode 100644 index 000000000000..96abb461fb93 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/haskell-servant/Types.mustache @@ -0,0 +1,63 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +module {{title}}.Types ( +{{#models}} +{{#model}} + {{classname}} (..), +{{/model}} +{{/models}} + ) where + +import Data.List (stripPrefix) +import Data.Maybe (fromMaybe) +import Data.Aeson (FromJSON(..), ToJSON(..), genericToJSON, genericParseJSON) +import Data.Aeson.Types (Options(..), defaultOptions) +import Data.Text (Text) +import qualified Data.Text as T +import GHC.Generics (Generic) +import Data.Function ((&)) +{{#imports}}import {{import}} +{{/imports}} + +{{#models}} +{{#model}} + +-- | {{description}} +{{^vendorExtensions.x-customNewtype}} +{{^parent}} +{{vendorExtensions.x-data}} {{classname}} = {{classname}} + { {{#vars}}{{& name}} :: {{datatype}} -- ^ {{& description}}{{#hasMore}} + , {{/hasMore}}{{/vars}} + } deriving (Show, Eq, Generic) + +instance FromJSON {{classname}} where + parseJSON = genericParseJSON (removeFieldLabelPrefix True "{{vendorExtensions.x-prefix}}") +instance ToJSON {{classname}} where + toJSON = genericToJSON (removeFieldLabelPrefix False "{{vendorExtensions.x-prefix}}") +{{/parent}} +{{#parent}} +newtype {{classname}} = {{classname}} { un{{classname}} :: {{parent}} } + deriving (Show, Eq, FromJSON, ToJSON, Generic) +{{/parent}} +{{/vendorExtensions.x-customNewtype}} +{{#vendorExtensions.x-customNewtype}} +newtype {{classname}} = {{classname}} {{vendorExtensions.x-customNewtype}} deriving (Show, Eq, FromJSON, ToJSON, Generic) +{{/vendorExtensions.x-customNewtype}} +{{/model}} +{{/models}} + +-- Remove a field label prefix during JSON parsing. +-- Also perform any replacements for special characters. +removeFieldLabelPrefix :: Bool -> String -> Options +removeFieldLabelPrefix forParsing prefix = + defaultOptions + { fieldLabelModifier = fromMaybe (error ("did not find prefix " ++ prefix)) . stripPrefix prefix . replaceSpecialChars + } + where + replaceSpecialChars field = foldl (&) field (map mkCharReplacement specialChars) + specialChars = [{{#specialCharReplacements}}("{{char}}", "{{&replacement}}"){{#hasMore}}, {{/hasMore}}{{/specialCharReplacements}}] + mkCharReplacement (replaceStr, searchStr) = T.unpack . replacer (T.pack searchStr) (T.pack replaceStr) . T.pack + replacer = if forParsing then flip T.replace else T.replace + + diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/Utils.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/Utils.mustache deleted file mode 100644 index f6db2602ce89..000000000000 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/Utils.mustache +++ /dev/null @@ -1,27 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE FlexibleInstances #-} -module Utils where - -import GHC.Generics -import Servant.API -import Data.List (intercalate) -import Data.List.Split (splitOn) -import qualified Data.Map as Map -import qualified Data.Text as T -import Test.QuickCheck - -instance FromText [String] where - fromText = Just . splitOn "," . T.unpack - -instance ToText [String] where - toText = T.pack . intercalate "," - -lkp inputs l = case lookup l inputs of - Nothing -> Left $ "label " ++ T.unpack l ++ " not found" - Just v -> Right $ read (T.unpack v) - -instance (Ord k, Arbitrary k, Arbitrary v) => Arbitrary (Map.Map k v) where - arbitrary = Map.fromList <$> arbitrary diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/api.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/api.mustache deleted file mode 100644 index c49fcd5eff8c..000000000000 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/api.mustache +++ /dev/null @@ -1,82 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE OverloadedStrings #-} - -{{#operations}} -module {{package}}.{{classname}} ( - {{#operation}}{{operationId}}{{#hasMore}} - , {{/hasMore}}{{/operation}} - , proxy{{classname}} - , {{classname}} - ) where -{{/operations}} - -import GHC.Generics -import Data.Proxy -import Servant.API -import Servant.Client -import Network.URI (URI (..), URIAuth (..), parseURI) -import Data.Maybe (fromMaybe) -import Servant.Common.Text -import Data.List (intercalate) -import qualified Data.Text as T -import Utils -import Test.QuickCheck -{{#imports}}import {{import}} -{{/imports}} - -{{#operations}} -{{#operation}} -{{#hasFormParams}} -data Form{{#formParams}}{{baseName}}{{/formParams}} = Form{{#formParams}}{{baseName}}{{/formParams}} - { {{#formParams}}{{baseName}} :: {{dataType}}{{#hasMore}} - , {{/hasMore}}{{/formParams}} - } deriving (Show, Eq, Generic) - -instance FromFormUrlEncoded Form{{#formParams}}{{baseName}}{{/formParams}} where - fromFormUrlEncoded inputs = Form{{#formParams}}{{baseName}}{{/formParams}} <$> {{#formParams}} lkp inputs "{{baseName}}"{{#hasMore}} <*> {{/hasMore}}{{/formParams}} -instance ToFormUrlEncoded Form{{#formParams}}{{baseName}}{{/formParams}} where - toFormUrlEncoded x = [({{#formParams}}(T.pack $ show $ {{package}}.{{classname}}.{{baseName}} x){{#hasMore}}, {{/hasMore}}{{/formParams}})] -instance Arbitrary Form{{#formParams}}{{baseName}}{{/formParams}} where - arbitrary = Form{{#formParams}}{{baseName}}{{/formParams}} <$> {{#formParams}}arbitrary{{#hasMore}} <*> {{/hasMore}}{{/formParams}} -{{/hasFormParams}} - -{{/operation}} -{{/operations}} - -{{#operations}} -type {{classname}} = {{#operation}}{{& nickname}} -- {{operationId}}{{#hasMore}} - :<|> {{/hasMore}}{{/operation}} -{{/operations}} - -proxy{{classname}} :: Proxy {{classname}} -proxy{{classname}} = Proxy - -{{#operations}} - -serverPath :: String -serverPath = "{{basePath}}" - -parseHostPort :: String -> (String, Int) -parseHostPort path = (host,port) - where - authority = case parseURI path of - Just x -> uriAuthority x - _ -> Nothing - (host, port) = case authority of - Just y -> (uriRegName y, (getPort . uriPort) y) - _ -> ("localhost", 8080) - getPort p = case (length p) of - 0 -> 80 - _ -> (read . drop 1) p - -(host, port) = parseHostPort serverPath - -{{#operation}} -{{operationId}}{{#hasMore}} - :<|> {{/hasMore}}{{/operation}} - = client proxy{{classname}} $ BaseUrl Http host port -{{/operations}} diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/haskell-servant-codegen.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/haskell-servant-codegen.mustache index 8b7d7fc54b20..eef9ce0b10b3 100644 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/haskell-servant-codegen.mustache +++ b/modules/swagger-codegen/src/main/resources/haskell-servant/haskell-servant-codegen.mustache @@ -1,62 +1,29 @@ -name: haskell-servant-codegen +name: {{package}} version: 0.1.0.0 -synopsis: Swagger-codegen example for Haskell servant +synopsis: Auto-generated API bindings for {{package}} description: Please see README.md homepage: https://github.com/swagger-api/swagger-codegen#readme -license: Apache-2.0 -license-file: LICENSE -author: Masahiro Yamauchi -maintainer: sgt.yamauchi@gmail.com -copyright: 2015- Masahiro Yamauchi +author: Author Name Here +maintainer: author.name@email.com +copyright: YEAR - AUTHOR category: Web build-type: Simple --- extra-source-files: cabal-version: >=1.10 library hs-source-dirs: lib - exposed-modules: Utils{{#models}}{{#model}} - , {{importPath}}{{/model}}{{/models}} -{{#apiInfo}} -{{#apis}} - , {{package}}.{{classname}} -{{/apis}} -{{/apiInfo}} - , Apis + exposed-modules: {{title}}.API + , {{title}}.Types ghc-options: -Wall build-depends: base , aeson , text - , split , containers , network-uri - , QuickCheck , servant , servant-client - default-language: Haskell2010 - -executable client - hs-source-dirs: client - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: base - , either - , transformers - , split - , network-uri - , QuickCheck - , servant - , servant-client - , haskell-servant-codegen - default-language: Haskell2010 - -executable server - hs-source-dirs: server - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: base , warp , servant-server - , servant-mock - , haskell-servant-codegen + , transformers + , either default-language: Haskell2010 diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/model.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/model.mustache deleted file mode 100644 index 1f76c197ae29..000000000000 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/model.mustache +++ /dev/null @@ -1,34 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -{{#models}} -{{#model}} -module {{package}}.{{classname}} - ( {{classname}} (..) - ) where -{{/model}} -{{/models}} - -import Data.Aeson -import GHC.Generics -import Test.QuickCheck -{{#imports}}import {{import}} -{{/imports}} - -{{#models}} -{{#model}} - -data {{classname}} = {{classname}} - { {{#vars}}{{& name}} :: {{datatype}}{{#hasMore}} - , {{/hasMore}}{{/vars}} - } deriving (Show, Eq, Generic) - -instance FromJSON {{classname}} -instance ToJSON {{classname}} -instance Arbitrary {{classname}} where - arbitrary = {{classname}} <$> {{#vars}}arbitrary{{#hasMore}} <*> {{/hasMore}}{{/vars}} -{{/model}} -{{/models}} diff --git a/modules/swagger-codegen/src/main/resources/haskell-servant/stack.mustache b/modules/swagger-codegen/src/main/resources/haskell-servant/stack.mustache index 00448df3e959..e06adf4ba3d3 100644 --- a/modules/swagger-codegen/src/main/resources/haskell-servant/stack.mustache +++ b/modules/swagger-codegen/src/main/resources/haskell-servant/stack.mustache @@ -1,33 +1,3 @@ -# For more information, see: https://github.com/commercialhaskell/stack/blob/release/doc/yaml_configuration.md - -# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) -resolver: lts-3.17 - -# Local packages, usually specified by relative directory name +resolver: lts-5.5 packages: - '.' - -# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3) -extra-deps: -- servant-mock-0.4.4.6 - -# Override default flag values for local packages and extra-deps -flags: {} - -# Extra package databases containing global packages -extra-package-dbs: [] - -# Control whether we use the GHC we find on the path -# system-ghc: true - -# Require a specific version of stack, using version ranges -# require-stack-version: -any # Default -# require-stack-version: >= 0.1.10.0 - -# Override the architecture used by stack, especially useful on Windows -# arch: i386 -# arch: x86_64 - -# Extra directories used by stack for building -# extra-include-dirs: [/path/to/dir] -# extra-lib-dirs: [/path/to/dir]