Skip to content

Commit

Permalink
Added support for enums in Dart. (#6516)
Browse files Browse the repository at this point in the history
* Added support for enums in Dart.

* Pick non-private names for enum values.

The _ prefix denotes a private member in Dart, so avoid generating enum values starting with this character.

* Properly encode enum values into query paramters.

* Various cleanups.

* Add support for x-enum-values extension.
Use class instead of enum for better ergonomy.
Better generated enum names.

* Fixed test.

* Support enum descriptions.
  • Loading branch information
pylaligand authored and wing328 committed Oct 16, 2017
1 parent 8b70f24 commit 1050aa9
Show file tree
Hide file tree
Showing 20 changed files with 234 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,35 @@
import io.swagger.codegen.CliOption;
import io.swagger.codegen.CodegenConfig;
import io.swagger.codegen.CodegenConstants;
import io.swagger.codegen.CodegenModel;
import io.swagger.codegen.CodegenProperty;
import io.swagger.codegen.CodegenType;
import io.swagger.codegen.DefaultCodegen;
import io.swagger.codegen.SupportingFile;
import io.swagger.models.Model;
import io.swagger.models.properties.ArrayProperty;
import io.swagger.models.properties.MapProperty;
import io.swagger.models.properties.Property;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

public class DartClientCodegen extends DefaultCodegen implements CodegenConfig {
public static final String BROWSER_CLIENT = "browserClient";
public static final String PUB_NAME = "pubName";
public static final String PUB_VERSION = "pubVersion";
public static final String PUB_DESCRIPTION = "pubDescription";
public static final String USE_ENUM_EXTENSION = "useEnumExtension";
protected boolean browserClient = true;
protected String pubName = "swagger";
protected String pubVersion = "1.0.0";
protected String pubDescription = "Swagger API client";
protected boolean useEnumExtension = false;
protected String sourceFolder = "";
protected String apiDocPath = "docs/";
protected String modelDocPath = "docs/";
Expand Down Expand Up @@ -95,6 +103,7 @@ public DartClientCodegen() {
cliOptions.add(new CliOption(PUB_NAME, "Name in generated pubspec"));
cliOptions.add(new CliOption(PUB_VERSION, "Version in generated pubspec"));
cliOptions.add(new CliOption(PUB_DESCRIPTION, "Description in generated pubspec"));
cliOptions.add(new CliOption(USE_ENUM_EXTENSION, "Allow the 'x-enum-values' extension for enums"));
cliOptions.add(new CliOption(CodegenConstants.SOURCE_FOLDER, "source folder for generated code"));
}

Expand Down Expand Up @@ -145,6 +154,13 @@ public void processOpts() {
additionalProperties.put(PUB_DESCRIPTION, pubDescription);
}

if (additionalProperties.containsKey(USE_ENUM_EXTENSION)) {
this.setUseEnumExtension(convertPropertyToBooleanAndWriteBack(USE_ENUM_EXTENSION));
} else {
// Not set, use to be passed to template.
additionalProperties.put(USE_ENUM_EXTENSION, useEnumExtension);
}

if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) {
this.setSourceFolder((String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER));
}
Expand Down Expand Up @@ -177,16 +193,11 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh"));
supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore"));
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));

}


@Override
public String escapeReservedWord(String name) {
if(this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "_" + name;
public String escapeReservedWord(String name) {
return name + "_";
}

@Override
Expand Down Expand Up @@ -223,8 +234,11 @@ public String toVarName(String name) {
// pet_id => petId
name = camelize(name, true);

// for reserved word or word starting with number, append _
if (isReservedWord(name) || name.matches("^\\d.*")) {
if (name.matches("^\\d.*")) {
name = "n" + name;
}

if (isReservedWord(name)) {
name = escapeReservedWord(name);
}

Expand Down Expand Up @@ -300,6 +314,117 @@ public String getSwaggerType(Property p) {
return toModelName(type);
}

@Override
public Map<String, Object> postProcessModels(Map<String, Object> objs) {
return postProcessModelsEnum(objs);
}

@Override
public Map<String, Object> postProcessModelsEnum(Map<String, Object> objs) {
List<Object> models = (List<Object>) objs.get("models");
for (Object _mo : models) {
Map<String, Object> mo = (Map<String, Object>) _mo;
CodegenModel cm = (CodegenModel) mo.get("model");
boolean succes = buildEnumFromVendorExtension(cm) ||
buildEnumFromValues(cm);
for (CodegenProperty var : cm.vars) {
updateCodegenPropertyEnum(var);
}
}
return objs;
}

/**
* Builds the set of enum members from their declared value.
*
* @return {@code true} if the enum was built
*/
private boolean buildEnumFromValues(CodegenModel cm) {
if (!cm.isEnum || cm.allowableValues == null) {
return false;
}
Map<String, Object> allowableValues = cm.allowableValues;
List<Object> values = (List<Object>) allowableValues.get("values");
List<Map<String, String>> enumVars =
new ArrayList<Map<String, String>>();
String commonPrefix = findCommonPrefixOfVars(values);
int truncateIdx = commonPrefix.length();
for (Object value : values) {
Map<String, String> enumVar = new HashMap<String, String>();
String enumName;
if (truncateIdx == 0) {
enumName = value.toString();
} else {
enumName = value.toString().substring(truncateIdx);
if ("".equals(enumName)) {
enumName = value.toString();
}
}
enumVar.put("name", toEnumVarName(enumName, cm.dataType));
enumVar.put("value", toEnumValue(value.toString(), cm.dataType));
enumVars.add(enumVar);
}
cm.allowableValues.put("enumVars", enumVars);
return true;
}

/**
* Builds the set of enum members from a vendor extension.
*
* @return {@code true} if the enum was built
*/
private boolean buildEnumFromVendorExtension(CodegenModel cm) {
if (!cm.isEnum || cm.allowableValues == null ||
!useEnumExtension ||
!cm.vendorExtensions.containsKey("x-enum-values")) {
return false;
}
Object extension = cm.vendorExtensions.get("x-enum-values");
List<Map<String, Object>> values =
(List<Map<String, Object>>) extension;
List<Map<String, String>> enumVars =
new ArrayList<Map<String, String>>();
for (Map<String, Object> value : values) {
Map<String, String> enumVar = new HashMap<String, String>();
String name = camelize((String) value.get("identifier"), true);
if (isReservedWord(name)) {
name = escapeReservedWord(name);
}
enumVar.put("name", name);
enumVar.put("value", toEnumValue(
value.get("numericValue").toString(), cm.dataType));
if (value.containsKey("description")) {
enumVar.put("description", value.get("description").toString());
}
enumVars.add(enumVar);
}
cm.allowableValues.put("enumVars", enumVars);
return true;
}

@Override
public String toEnumVarName(String value, String datatype) {
if (value.length() == 0) {
return "empty";
}
String var = value.replaceAll("\\W+", "_");
if ("number".equalsIgnoreCase(datatype) ||
"int".equalsIgnoreCase(datatype)) {
var = "Number" + var;
}
return escapeReservedWord(camelize(var, true));
}

@Override
public String toEnumValue(String value, String datatype) {
if ("number".equalsIgnoreCase(datatype) ||
"int".equalsIgnoreCase(datatype)) {
return value;
} else {
return "\"" + escapeText(value) + "\"";
}
}

@Override
public String toOperationId(String operationId) {
// method name cannot use reserved keyword, e.g. return
Expand Down Expand Up @@ -328,6 +453,10 @@ public void setPubDescription(String pubDescription) {
this.pubDescription = pubDescription;
}

public void setUseEnumExtension(boolean useEnumExtension) {
this.useEnumExtension = useEnumExtension;
}

public void setSourceFolder(String sourceFolder) {
this.sourceFolder = sourceFolder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ class ApiClient {
Map<String, Authentication> _authentications = {};

final dson = new Dartson.JSON()
..addTransformer(new DateTimeParser(), DateTime);
{{#models}}
{{#model}}
{{#isEnum}}
..addTransformer(new {{classname}}TypeTransformer(), {{classname}})
{{/isEnum}}
{{/model}}
{{/models}}
..addTransformer(new DateTimeParser(), DateTime);

final _RegList = new RegExp(r'^List<(.*)>$');
final _RegMap = new RegExp(r'^Map<String,(.*)>$');
Expand Down Expand Up @@ -46,7 +53,16 @@ class ApiClient {
{{#models}}
{{#model}}
case '{{classname}}':
{{#isEnum}}
// Enclose the value in a list so that Dartson can use a transformer
// to decode it.
final listValue = [value];
final List<dynamic> listResult = dson.map(listValue, []);
return listResult[0];
{{/isEnum}}
{{^isEnum}}
return dson.map(value, new {{classname}}());
{{/isEnum}}
{{/model}}
{{/models}}
default:
Expand Down Expand Up @@ -116,7 +132,7 @@ class ApiClient {
headerParams['Content-Type'] = contentType;
if(body is MultipartRequest) {
var request = new MultipartRequest(method, Uri.parse(url));
var request = new MultipartRequest(method, Uri.parse(url));
request.fields.addAll(body.fields);
request.files.addAll(body.files);
request.headers.addAll(body.headers);
Expand All @@ -141,7 +157,7 @@ class ApiClient {
}

/// Update query and header parameters based on authentication settings.
/// @param authNames The authentications to apply
/// @param authNames The authentications to apply
void _updateParamsForAuth(List<String> authNames, List<QueryParam> queryParams, Map<String, String> headerParams) {
authNames.forEach((authName) {
Authentication auth = _authentications[authName];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ String _parameterToString(dynamic value) {
return '';
} else if (value is DateTime) {
return value.toUtc().toIso8601String();
{{#models}}
{{#model}}
{{#isEnum}}
} else if (value is {{classname}}) {
return new {{classname}}TypeTransformer().encode(value).toString();
{{/isEnum}}
{{/model}}
{{/models}}
} else {
return value.toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:http/browser_client.dart';{{/browserClient}}
import 'package:http/http.dart';
import 'package:dartson/dartson.dart';
import 'package:dartson/transformers/date_time.dart';
import 'package:dartson/type_transformer.dart';

part 'api_client.dart';
part 'api_helper.dart';
Expand All @@ -21,4 +22,3 @@ part 'auth/http_basic_auth.dart';
{{/model}}{{/models}}

ApiClient defaultApiClient = new ApiClient();

14 changes: 14 additions & 0 deletions modules/swagger-codegen/src/main/resources/dart/class.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@Entity()
class {{classname}} {
{{#vars}}{{#description}}/* {{{description}}} */{{/description}}
@Property(name: '{{baseName}}')
{{{datatype}}} {{name}} = {{{defaultValue}}};
{{#allowableValues}}{{#min}} // range from {{min}} to {{max}}{{/min}}//{{^min}}enum {{name}}Enum { {{#values}} {{.}}, {{/values}} };{{/min}}{{/allowableValues}}
{{/vars}}
{{classname}}();

@override
String toString() {
return '{{classname}}[{{#vars}}{{name}}=${{name}}, {{/vars}}]';
}
}
36 changes: 36 additions & 0 deletions modules/swagger-codegen/src/main/resources/dart/enum.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@Entity()
class {{classname}} {
/// The underlying value of this enum member.
final {{dataType}} value;

const {{classname}}._internal(this.value);

{{#allowableValues}}
{{#enumVars}}
{{#description}}
/// {{description}}
{{/description}}
static const {{classname}} {{name}} = const {{classname}}._internal({{value}});
{{/enumVars}}
{{/allowableValues}}
}

class {{classname}}TypeTransformer extends TypeTransformer<{{classname}}> {
@override
dynamic encode({{classname}} data) {
return data.value;
}

@override
{{classname}} decode(dynamic data) {
switch (data) {
{{#allowableValues}}
{{#enumVars}}
case {{value}}: return {{classname}}.{{name}};
{{/enumVars}}
{{/allowableValues}}
default: throw('Unknown enum value to decode: $data');
}
}
}
22 changes: 5 additions & 17 deletions modules/swagger-codegen/src/main/resources/dart/model.mustache
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
part of {{pubName}}.api;

{{#models}}{{#model}}
@Entity()
class {{classname}} {
{{#vars}}{{#description}}/* {{{description}}} */{{/description}}
@Property(name: '{{baseName}}')
{{{datatype}}} {{name}} = {{{defaultValue}}};
{{#allowableValues}}{{#min}} // range from {{min}} to {{max}}{{/min}}//{{^min}}enum {{name}}Enum { {{#values}} {{.}}, {{/values}} };{{/min}}{{/allowableValues}}
{{/vars}}
{{classname}}();

@override
String toString() {
return '{{classname}}[{{#vars}}{{name}}=${{name}}, {{/vars}}]';
}

}
{{/model}}{{/models}}
{{#models}}
{{#model}}
{{#isEnum}}{{>enum}}{{/isEnum}}{{^isEnum}}{{>class}}{{/isEnum}}
{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ protected void setExpectations() {
times = 1;
clientCodegen.setSourceFolder(DartClientOptionsProvider.SOURCE_FOLDER_VALUE);
times = 1;
clientCodegen.setUseEnumExtension(Boolean.valueOf(DartClientOptionsProvider.USE_ENUM_EXTENSION));
times = 1;
}};
}
}

Loading

0 comments on commit 1050aa9

Please sign in to comment.