Skip to content

Commit

Permalink
Merge pull request #2009 from algas/support-haskell
Browse files Browse the repository at this point in the history
Code generator for haskell-servant framework
  • Loading branch information
wing328 committed Feb 1, 2016
2 parents 0e0508c + 30e8154 commit 70cce74
Show file tree
Hide file tree
Showing 35 changed files with 1,791 additions and 0 deletions.
1 change: 1 addition & 0 deletions bin/all-petstore.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ cd $APP_DIR
./bin/clojure-petstore.sh
./bin/csharp-petstore.sh
./bin/dynamic-html.sh
./bin/haskell-petstore.sh
./bin/html-petstore.sh
./bin/java-petstore.sh
./bin/java-petstore-jersey2.sh
Expand Down
31 changes: 31 additions & 0 deletions bin/haskell-servant-petstore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/sh

SCRIPT="$0"

while [ -h "$SCRIPT" ] ; do
ls=`ls -ld "$SCRIPT"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=`dirname "$SCRIPT"`/"$link"
fi
done

if [ ! -d "${APP_DIR}" ]; then
APP_DIR=`dirname "$SCRIPT"`/..
APP_DIR=`cd "${APP_DIR}"; pwd`
fi

executable="./modules/swagger-codegen-cli/target/swagger-codegen-cli.jar"

if [ ! -f "$executable" ]
then
mvn clean package
fi

# if you've executed sbt assembly previously it will use that instead.
export JAVA_OPTS="${JAVA_OPTS} -XX:MaxPermSize=256M -Xmx1024M -DloggerPath=conf/log4j.properties"
ags="$@ generate -t modules/swagger-codegen/src/main/resources/haskell-servant -i modules/swagger-codegen/src/test/resources/2_0/petstore.json -l haskell-servant -o samples/server/petstore/haskell-servant"

java $JAVA_OPTS -jar $executable $ags
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
package io.swagger.codegen.languages;

import io.swagger.codegen.*;
import io.swagger.models.properties.*;
import io.swagger.models.Model;
import io.swagger.models.Operation;
import io.swagger.models.Swagger;

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";

/**
* 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";

/**
* 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
*/
modelTemplateFiles.put(
"model.mustache", // the template to use
".hs"); // the extension for each file to write

/**
* 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
*/
apiTemplateFiles.put(
"api.mustache", // the template to use
".hs"); // the extension for each file to write

/**
* 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";

/**
* Api Package. Optional, if needed, this can be used in templates
*/
apiPackage = "Api";

/**
* Model Package. Optional, if needed, this can be used in templates
*/
modelPackage = "Model";

/**
* Reserved words. Override this with reserved words specific to your language
*/
// from https://wiki.haskell.org/Keywords
reservedWords = new HashSet<String>(
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"
)
);

/**
* Additional Properties. These values can be passed to the templates and
* are available in models, apis, and supporting files
*/
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"));

/**
* Language Specific Primitives. These types will not trigger imports by
* the client generator
*/
languageSpecificPrimitives = new HashSet<String>(
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");

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

private String capturePath(String path, List<CodegenParameter> pathParams) {
for (CodegenParameter p : pathParams) {
String pName = "{"+p.baseName+"}";
if (path.indexOf(pName) >= 0) {
path = path.replace(pName, "Capture " + "\""+p.baseName+"\" " + p.dataType);
}
}
return path;
}

private String queryPath(String path, List<CodegenParameter> queryParams) {
for (CodegenParameter p : queryParams) {
path += " :> QueryParam \"" + p.baseName + "\" " + p.dataType;
}
return path;
}

private String bodyPath(String path, List<CodegenParameter> bodyParams) {
for (CodegenParameter p : bodyParams) {
path += " :> ReqBody '[JSON] " + p.dataType;
}
return path;
}

private String formPath(String path, List<CodegenParameter> formParams) {
String names = "Form";
for (CodegenParameter p : formParams) {
if(p.dataType.equals("FilePath")){
// file data processing
}
names += p.baseName;
}
if(formParams.size() > 0){
path += " :> ReqBody '[FormUrlEncoded] " + names;
}
return path;
}

private String headerPath(String path, List<CodegenParameter> headerParams) {
for (CodegenParameter p : headerParams) {
path += " :> Header \"" + p.baseName + "\" " + p.dataType;
}
return path;
}


private String filterReturnType(String rt) {
if (rt == null || rt.equals("null")) {
return "()";
} else if (rt.indexOf(" ") >= 0) {
return "(" + rt + ")";
}
return rt;
}

private String addReturnPath(String path, String httpMethod, String returnType) {
return path + " :> " + upperCaseFirst(httpMethod) + " '[JSON] " + filterReturnType(returnType);
}

private String joinStrings(String sep, List<String> ss) {
StringBuilder sb = new StringBuilder();
for (String s : ss) {
if (sb.length() > 0) {
sb.append(sep);
}
sb.append(s);
}
return sb.toString();
}

private String replacePathSplitter(String path) {
String[] ps = path.replaceFirst("/", "").split("/", 0);
List<String> rs = new ArrayList<String>();
for (String p : ps) {
if (p.indexOf("{") < 0) {
rs.add("\"" + p + "\"");
} else {
rs.add(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<String, Model> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ io.swagger.codegen.languages.TypeScriptNodeClientCodegen
io.swagger.codegen.languages.AkkaScalaClientCodegen
io.swagger.codegen.languages.CsharpDotNet2ClientCodegen
io.swagger.codegen.languages.ClojureClientCodegen
io.swagger.codegen.languages.HaskellServantCodegen
Loading

0 comments on commit 70cce74

Please sign in to comment.