Added support for enums in Dart. (#6516)

* 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.
This commit is contained in:
P.Y. Laligand
2017-10-16 06:28:21 -07:00
committed by wing328
parent 8b70f24371
commit 1050aa9a06
20 changed files with 234 additions and 52 deletions

View File

@@ -3,27 +3,35 @@ package io.swagger.codegen.languages;
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/";
@@ -95,6 +103,7 @@ public class DartClientCodegen extends DefaultCodegen implements CodegenConfig {
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"));
}
@@ -145,6 +154,13 @@ public class DartClientCodegen extends DefaultCodegen implements CodegenConfig {
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));
}
@@ -177,16 +193,11 @@ public class DartClientCodegen extends DefaultCodegen implements CodegenConfig {
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
@@ -223,8 +234,11 @@ public class DartClientCodegen extends DefaultCodegen implements CodegenConfig {
// 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);
}
@@ -300,6 +314,117 @@ public class DartClientCodegen extends DefaultCodegen implements CodegenConfig {
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
@@ -328,6 +453,10 @@ public class DartClientCodegen extends DefaultCodegen implements CodegenConfig {
this.pubDescription = pubDescription;
}
public void setUseEnumExtension(boolean useEnumExtension) {
this.useEnumExtension = useEnumExtension;
}
public void setSourceFolder(String sourceFolder) {
this.sourceFolder = sourceFolder;
}

View File

@@ -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,(.*)>$');
@@ -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:
@@ -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);
@@ -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];

View File

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

View File

@@ -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';
@@ -21,4 +22,3 @@ part 'auth/http_basic_auth.dart';
{{/model}}{{/models}}
ApiClient defaultApiClient = new ApiClient();

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,8 @@ public class DartClientOptionsTest extends AbstractOptionsTest {
times = 1;
clientCodegen.setSourceFolder(DartClientOptionsProvider.SOURCE_FOLDER_VALUE);
times = 1;
clientCodegen.setUseEnumExtension(Boolean.valueOf(DartClientOptionsProvider.USE_ENUM_EXTENSION));
times = 1;
}};
}
}

View File

@@ -15,6 +15,7 @@ public class DartClientOptionsProvider implements OptionsProvider {
public static final String PUB_VERSION_VALUE = "1.0.0-SNAPSHOT";
public static final String PUB_DESCRIPTION_VALUE = "Swagger API client dart";
public static final String SOURCE_FOLDER_VALUE = "src";
public static final String USE_ENUM_EXTENSION = "true";
public static final String ALLOW_UNICODE_IDENTIFIERS_VALUE = "false";
@@ -33,6 +34,7 @@ public class DartClientOptionsProvider implements OptionsProvider {
.put(DartClientCodegen.PUB_VERSION, PUB_VERSION_VALUE)
.put(DartClientCodegen.PUB_DESCRIPTION, PUB_DESCRIPTION_VALUE)
.put(CodegenConstants.SOURCE_FOLDER, SOURCE_FOLDER_VALUE)
.put(DartClientCodegen.USE_ENUM_EXTENSION, USE_ENUM_EXTENSION)
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
.build();
}

View File

@@ -1 +1 @@
2.2.3-SNAPSHOT
2.3.0-SNAPSHOT

View File

@@ -87,7 +87,7 @@ This endpoint does not need any parameter.
### Return type
[**Map<String, int>**](Map.md)
**Map<String, int>**
### Authorization

View File

@@ -6,6 +6,7 @@ import 'package:http/browser_client.dart';
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';
@@ -28,4 +29,3 @@ part 'model/user.dart';
ApiClient defaultApiClient = new ApiClient();

View File

@@ -16,7 +16,7 @@ class ApiClient {
Map<String, Authentication> _authentications = {};
final dson = new Dartson.JSON()
..addTransformer(new DateTimeParser(), DateTime);
..addTransformer(new DateTimeParser(), DateTime);
final _RegList = new RegExp(r'^List<(.*)>$');
final _RegMap = new RegExp(r'^Map<String,(.*)>$');
@@ -121,7 +121,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);
@@ -146,7 +146,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];

View File

@@ -41,4 +41,4 @@ String _parameterToString(dynamic value) {
} else {
return value.toString();
}
}
}

View File

@@ -1,6 +1,5 @@
part of swagger.api;
@Entity()
class ApiResponse {
@@ -21,6 +20,5 @@ class ApiResponse {
String toString() {
return 'ApiResponse[code=$code, type=$type, message=$message, ]';
}
}

View File

@@ -1,6 +1,5 @@
part of swagger.api;
@Entity()
class Category {
@@ -17,6 +16,5 @@ class Category {
String toString() {
return 'Category[id=$id, name=$name, ]';
}
}

View File

@@ -1,6 +1,5 @@
part of swagger.api;
@Entity()
class Order {
@@ -33,6 +32,5 @@ class Order {
String toString() {
return 'Order[id=$id, petId=$petId, quantity=$quantity, shipDate=$shipDate, status=$status, complete=$complete, ]';
}
}

View File

@@ -1,6 +1,5 @@
part of swagger.api;
@Entity()
class Pet {
@@ -33,6 +32,5 @@ class Pet {
String toString() {
return 'Pet[id=$id, category=$category, name=$name, photoUrls=$photoUrls, tags=$tags, status=$status, ]';
}
}

View File

@@ -1,6 +1,5 @@
part of swagger.api;
@Entity()
class Tag {
@@ -17,6 +16,5 @@ class Tag {
String toString() {
return 'Tag[id=$id, name=$name, ]';
}
}

View File

@@ -1,6 +1,5 @@
part of swagger.api;
@Entity()
class User {
@@ -41,6 +40,5 @@ class User {
String toString() {
return 'User[id=$id, username=$username, firstName=$firstName, lastName=$lastName, email=$email, password=$password, phone=$phone, userStatus=$userStatus, ]';
}
}