Skip to content

Commit 70cce74

Browse files
committed
Merge pull request #2009 from algas/support-haskell
Code generator for haskell-servant framework
2 parents 0e0508c + 30e8154 commit 70cce74

35 files changed

+1791
-0
lines changed

bin/all-petstore.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ cd $APP_DIR
2323
./bin/clojure-petstore.sh
2424
./bin/csharp-petstore.sh
2525
./bin/dynamic-html.sh
26+
./bin/haskell-petstore.sh
2627
./bin/html-petstore.sh
2728
./bin/java-petstore.sh
2829
./bin/java-petstore-jersey2.sh

bin/haskell-servant-petstore.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/bin/sh
2+
3+
SCRIPT="$0"
4+
5+
while [ -h "$SCRIPT" ] ; do
6+
ls=`ls -ld "$SCRIPT"`
7+
link=`expr "$ls" : '.*-> \(.*\)$'`
8+
if expr "$link" : '/.*' > /dev/null; then
9+
SCRIPT="$link"
10+
else
11+
SCRIPT=`dirname "$SCRIPT"`/"$link"
12+
fi
13+
done
14+
15+
if [ ! -d "${APP_DIR}" ]; then
16+
APP_DIR=`dirname "$SCRIPT"`/..
17+
APP_DIR=`cd "${APP_DIR}"; pwd`
18+
fi
19+
20+
executable="./modules/swagger-codegen-cli/target/swagger-codegen-cli.jar"
21+
22+
if [ ! -f "$executable" ]
23+
then
24+
mvn clean package
25+
fi
26+
27+
# if you've executed sbt assembly previously it will use that instead.
28+
export JAVA_OPTS="${JAVA_OPTS} -XX:MaxPermSize=256M -Xmx1024M -DloggerPath=conf/log4j.properties"
29+
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"
30+
31+
java $JAVA_OPTS -jar $executable $ags
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
package io.swagger.codegen.languages;
2+
3+
import io.swagger.codegen.*;
4+
import io.swagger.models.properties.*;
5+
import io.swagger.models.Model;
6+
import io.swagger.models.Operation;
7+
import io.swagger.models.Swagger;
8+
9+
import java.util.*;
10+
import java.io.File;
11+
12+
public class HaskellServantCodegen extends DefaultCodegen implements CodegenConfig {
13+
14+
// source folder where to write the files
15+
protected String sourceFolder = "src";
16+
protected String apiVersion = "0.0.1";
17+
18+
/**
19+
* Configures the type of generator.
20+
*
21+
* @return the CodegenType for this generator
22+
* @see io.swagger.codegen.CodegenType
23+
*/
24+
public CodegenType getTag() {
25+
return CodegenType.SERVER;
26+
}
27+
28+
/**
29+
* Configures a friendly name for the generator. This will be used by the generator
30+
* to select the library with the -l flag.
31+
*
32+
* @return the friendly name for the generator
33+
*/
34+
public String getName() {
35+
return "haskell-servant";
36+
}
37+
38+
/**
39+
* Returns human-friendly help for the generator. Provide the consumer with help
40+
* tips, parameters here
41+
*
42+
* @return A string value for the help message
43+
*/
44+
public String getHelp() {
45+
return "Generates a HaskellServantCodegen library.";
46+
}
47+
48+
public HaskellServantCodegen() {
49+
super();
50+
51+
// set the output folder here
52+
outputFolder = "generated-code/HaskellServantCodegen";
53+
54+
/**
55+
* Models. You can write model files using the modelTemplateFiles map.
56+
* if you want to create one template for file, you can do so here.
57+
* for multiple files for model, just put another entry in the `modelTemplateFiles` with
58+
* a different extension
59+
*/
60+
modelTemplateFiles.put(
61+
"model.mustache", // the template to use
62+
".hs"); // the extension for each file to write
63+
64+
/**
65+
* Api classes. You can write classes for each Api file with the apiTemplateFiles map.
66+
* as with models, add multiple entries with different extensions for multiple files per
67+
* class
68+
*/
69+
apiTemplateFiles.put(
70+
"api.mustache", // the template to use
71+
".hs"); // the extension for each file to write
72+
73+
/**
74+
* Template Location. This is the location which templates will be read from. The generator
75+
* will use the resource stream to attempt to read the templates.
76+
*/
77+
embeddedTemplateDir = templateDir = "haskell";
78+
79+
/**
80+
* Api Package. Optional, if needed, this can be used in templates
81+
*/
82+
apiPackage = "Api";
83+
84+
/**
85+
* Model Package. Optional, if needed, this can be used in templates
86+
*/
87+
modelPackage = "Model";
88+
89+
/**
90+
* Reserved words. Override this with reserved words specific to your language
91+
*/
92+
// from https://wiki.haskell.org/Keywords
93+
reservedWords = new HashSet<String>(
94+
Arrays.asList(
95+
"as", "case", "of",
96+
"class", "data", // "data family", "data instance",
97+
"default", "deriving", // "deriving instance",
98+
"do",
99+
"forall", "foreign", "hiding",
100+
"id",
101+
"if", "then", "else",
102+
"import", "infix", "infixl", "infixr",
103+
"instance", "let", "in",
104+
"mdo", "module", "newtype",
105+
"proc", "qualified", "rec",
106+
"type", // "type family", "type instance",
107+
"where"
108+
)
109+
);
110+
111+
/**
112+
* Additional Properties. These values can be passed to the templates and
113+
* are available in models, apis, and supporting files
114+
*/
115+
additionalProperties.put("apiVersion", apiVersion);
116+
117+
/**
118+
* Supporting Files. You can write single files for the generator with the
119+
* entire object tree available. If the input file has a suffix of `.mustache
120+
* it will be processed by the template engine. Otherwise, it will be copied
121+
*/
122+
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
123+
supportingFiles.add(new SupportingFile("stack.mustache", "", "stack.yaml"));
124+
supportingFiles.add(new SupportingFile("haskell-servant-codegen.mustache", "", "haskell-servant-codegen.cabal"));
125+
supportingFiles.add(new SupportingFile("Setup.mustache", "", "Setup.hs"));
126+
supportingFiles.add(new SupportingFile("LICENSE", "", "LICENSE"));
127+
supportingFiles.add(new SupportingFile("Apis.mustache", "lib", "Apis.hs"));
128+
supportingFiles.add(new SupportingFile("Utils.mustache", "lib", "Utils.hs"));
129+
supportingFiles.add(new SupportingFile("Client.mustache", "client", "Main.hs"));
130+
supportingFiles.add(new SupportingFile("Server.mustache", "server", "Main.hs"));
131+
132+
/**
133+
* Language Specific Primitives. These types will not trigger imports by
134+
* the client generator
135+
*/
136+
languageSpecificPrimitives = new HashSet<String>(
137+
Arrays.asList(
138+
"Bool",
139+
"String",
140+
"Int",
141+
"Integer",
142+
"Float",
143+
"Char",
144+
"Double",
145+
"List",
146+
"FilePath"
147+
)
148+
);
149+
150+
typeMapping.clear();
151+
// typeMapping.put("enum", "NSString");
152+
typeMapping.put("array", "List");
153+
typeMapping.put("set", "Set");
154+
typeMapping.put("boolean", "Bool");
155+
typeMapping.put("string", "String");
156+
typeMapping.put("int", "Int");
157+
typeMapping.put("long", "Integer");
158+
typeMapping.put("float", "Float");
159+
// typeMapping.put("byte", "Byte");
160+
typeMapping.put("short", "Int");
161+
typeMapping.put("char", "Char");
162+
typeMapping.put("double", "Double");
163+
typeMapping.put("DateTime", "Integer");
164+
// typeMapping.put("object", "Map");
165+
typeMapping.put("file", "FilePath");
166+
167+
importMapping.clear();
168+
importMapping.put("Map", "qualified Data.Map as Map");
169+
170+
cliOptions.add(new CliOption(CodegenConstants.MODEL_PACKAGE, CodegenConstants.MODEL_PACKAGE_DESC));
171+
cliOptions.add(new CliOption(CodegenConstants.API_PACKAGE, CodegenConstants.API_PACKAGE_DESC));
172+
}
173+
174+
/**
175+
* Escapes a reserved word as defined in the `reservedWords` array. Handle escaping
176+
* those terms here. This logic is only called if a variable matches the reseved words
177+
*
178+
* @return the escaped term
179+
*/
180+
@Override
181+
public String escapeReservedWord(String name) {
182+
return name + "_";
183+
}
184+
185+
/**
186+
* Location to write model files. You can use the modelPackage() as defined when the class is
187+
* instantiated
188+
*/
189+
public String modelFileFolder() {
190+
return outputFolder + File.separatorChar + "lib" + File.separatorChar + modelPackage().replace('.', File.separatorChar);
191+
}
192+
193+
/**
194+
* Location to write api files. You can use the apiPackage() as defined when the class is
195+
* instantiated
196+
*/
197+
@Override
198+
public String apiFileFolder() {
199+
return outputFolder + File.separatorChar + "lib" + File.separatorChar + apiPackage().replace('.', File.separatorChar);
200+
}
201+
202+
/**
203+
* Optional - type declaration. This is a String which is used by the templates to instantiate your
204+
* types. There is typically special handling for different property types
205+
*
206+
* @return a string value used as the `dataType` field for model templates, `returnType` for api templates
207+
*/
208+
@Override
209+
public String getTypeDeclaration(Property p) {
210+
if(p instanceof ArrayProperty) {
211+
ArrayProperty ap = (ArrayProperty) p;
212+
Property inner = ap.getItems();
213+
return "[" + getTypeDeclaration(inner) + "]";
214+
}
215+
else if (p instanceof MapProperty) {
216+
MapProperty mp = (MapProperty) p;
217+
Property inner = mp.getAdditionalProperties();
218+
return "Map.Map String " + getTypeDeclaration(inner);
219+
}
220+
return super.getTypeDeclaration(p);
221+
}
222+
223+
/**
224+
* Optional - swagger type conversion. This is used to map swagger types in a `Property` into
225+
* either language specific types via `typeMapping` or into complex models if there is not a mapping.
226+
*
227+
* @return a string value of the type or complex model for this property
228+
* @see io.swagger.models.properties.Property
229+
*/
230+
@Override
231+
public String getSwaggerType(Property p) {
232+
String swaggerType = super.getSwaggerType(p);
233+
String type = null;
234+
if(typeMapping.containsKey(swaggerType)) {
235+
type = typeMapping.get(swaggerType);
236+
if(languageSpecificPrimitives.contains(type))
237+
return toModelName(type);
238+
}
239+
else
240+
type = swaggerType;
241+
return toModelName(type);
242+
}
243+
244+
private String capturePath(String path, List<CodegenParameter> pathParams) {
245+
for (CodegenParameter p : pathParams) {
246+
String pName = "{"+p.baseName+"}";
247+
if (path.indexOf(pName) >= 0) {
248+
path = path.replace(pName, "Capture " + "\""+p.baseName+"\" " + p.dataType);
249+
}
250+
}
251+
return path;
252+
}
253+
254+
private String queryPath(String path, List<CodegenParameter> queryParams) {
255+
for (CodegenParameter p : queryParams) {
256+
path += " :> QueryParam \"" + p.baseName + "\" " + p.dataType;
257+
}
258+
return path;
259+
}
260+
261+
private String bodyPath(String path, List<CodegenParameter> bodyParams) {
262+
for (CodegenParameter p : bodyParams) {
263+
path += " :> ReqBody '[JSON] " + p.dataType;
264+
}
265+
return path;
266+
}
267+
268+
private String formPath(String path, List<CodegenParameter> formParams) {
269+
String names = "Form";
270+
for (CodegenParameter p : formParams) {
271+
if(p.dataType.equals("FilePath")){
272+
// file data processing
273+
}
274+
names += p.baseName;
275+
}
276+
if(formParams.size() > 0){
277+
path += " :> ReqBody '[FormUrlEncoded] " + names;
278+
}
279+
return path;
280+
}
281+
282+
private String headerPath(String path, List<CodegenParameter> headerParams) {
283+
for (CodegenParameter p : headerParams) {
284+
path += " :> Header \"" + p.baseName + "\" " + p.dataType;
285+
}
286+
return path;
287+
}
288+
289+
290+
private String filterReturnType(String rt) {
291+
if (rt == null || rt.equals("null")) {
292+
return "()";
293+
} else if (rt.indexOf(" ") >= 0) {
294+
return "(" + rt + ")";
295+
}
296+
return rt;
297+
}
298+
299+
private String addReturnPath(String path, String httpMethod, String returnType) {
300+
return path + " :> " + upperCaseFirst(httpMethod) + " '[JSON] " + filterReturnType(returnType);
301+
}
302+
303+
private String joinStrings(String sep, List<String> ss) {
304+
StringBuilder sb = new StringBuilder();
305+
for (String s : ss) {
306+
if (sb.length() > 0) {
307+
sb.append(sep);
308+
}
309+
sb.append(s);
310+
}
311+
return sb.toString();
312+
}
313+
314+
private String replacePathSplitter(String path) {
315+
String[] ps = path.replaceFirst("/", "").split("/", 0);
316+
List<String> rs = new ArrayList<String>();
317+
for (String p : ps) {
318+
if (p.indexOf("{") < 0) {
319+
rs.add("\"" + p + "\"");
320+
} else {
321+
rs.add(p);
322+
}
323+
}
324+
return joinStrings(" :> ", rs);
325+
}
326+
327+
private String upperCaseFirst(String str) {
328+
char[] array = str.toLowerCase().toCharArray();
329+
array[0] = Character.toUpperCase(array[0]);
330+
return new String(array);
331+
}
332+
333+
private String parseScheme(String basePath) {
334+
return "Http";
335+
}
336+
337+
@Override
338+
public CodegenOperation fromOperation(String resourcePath, String httpMethod, Operation operation, Map<String, Model> definitions, Swagger swagger){
339+
CodegenOperation op = super.fromOperation(resourcePath, httpMethod, operation, definitions, swagger);
340+
String path = op.path;
341+
op.nickname = addReturnPath(headerPath(formPath(bodyPath(queryPath(capturePath(replacePathSplitter(path), op.pathParams), op.queryParams), op.bodyParams), op.formParams), op.headerParams), op.httpMethod, op.returnType);
342+
return op;
343+
}
344+
345+
}

modules/swagger-codegen/src/main/resources/META-INF/services/io.swagger.codegen.CodegenConfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ io.swagger.codegen.languages.TypeScriptNodeClientCodegen
3535
io.swagger.codegen.languages.AkkaScalaClientCodegen
3636
io.swagger.codegen.languages.CsharpDotNet2ClientCodegen
3737
io.swagger.codegen.languages.ClojureClientCodegen
38+
io.swagger.codegen.languages.HaskellServantCodegen

0 commit comments

Comments
 (0)