Skip to content

Commit

Permalink
Adds support for initializing constructor parameters. Issue dart-arch…
Browse files Browse the repository at this point in the history
  • Loading branch information
seanmccleary committed Oct 14, 2016
1 parent 0cb7a88 commit 3a464c8
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 18 deletions.
7 changes: 6 additions & 1 deletion lib/src/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ class ApiProperty {
class ApiMessage {
final bool includeSuper;

const ApiMessage({this.includeSuper: false});
/// Whether to figure out constructor parameters upon instantiation
final bool withConstructorParameters;

const ApiMessage(
{this.withConstructorParameters: false,
this.includeSuper: false});
}

/// Special API Message to use when a method doesn't need a request or doesn't
Expand Down
1 change: 1 addition & 0 deletions lib/src/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'utils.dart';
import 'discovery/config.dart' as discovery;
import 'http_body_parser.dart';
import 'media_message.dart';
import 'annotations.dart';

part 'config/api.dart';
part 'config/method.dart';
Expand Down
60 changes: 53 additions & 7 deletions lib/src/config/schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,16 @@ class ApiConfigSchema {
'Invalid parameter: \'$request\', should be an instance of type '
'\'$schemaName\'.');
}
InstanceMirror schema = schemaClass.newInstance(new Symbol(''), []);
bool initializeConstructorParameters = false;
for (InstanceMirror im in schemaClass.metadata) {
if (im.reflectee is ApiMessage
&& im.reflectee.withConstructorParameters) {
initializeConstructorParameters = true;
break;
}
}

Map objectFieldValues = new Map();
for (Symbol sym in _properties.keys) {
final prop = _properties[sym];

Expand All @@ -56,27 +65,64 @@ class ApiConfigSchema {
// If in form, there is an (input[type="file"] multiple) and the user
// put only one file. It's not an error and it should be accept.
// Maybe it cans be optimized.
if (schema.type.instanceMembers[sym]
if (schemaClass.instanceMembers[sym]
.returnType
.reflectedType
.toString() ==
'List<MediaMessage>' &&
request[prop.name] is MediaMessage) {
schema.setField(sym, [request[prop.name]]);
objectFieldValues[sym] = [request[prop.name]];
} else if (request[prop.name] is List) {
schema.setField(sym, prop.fromRequest(request[prop.name]));
objectFieldValues[sym] = prop.fromRequest(request[prop.name]);
} else {
schema.setField(sym, request[prop.name]);
objectFieldValues[sym] = request[prop.name];
}
} else {
schema.setField(sym, prop.fromRequest(request[prop.name]));
objectFieldValues[sym] = prop.fromRequest(request[prop.name]);
}
} else if (prop.hasDefault) {
schema.setField(sym, prop.fromRequest(prop.defaultValue));
objectFieldValues[sym] = prop.fromRequest(prop.defaultValue);
} else if (prop.required) {
throw new BadRequestError('Required field ${prop.name} is missing');
}
}

List positionalArguments = new List();
Map namedArguments = new Map();
var constructors;
if (initializeConstructorParameters && (constructors =
schemaClass.declarations.values.where((mm) => mm is MethodMirror &&
mm.isConstructor && mm.simpleName == schemaClass.simpleName &&
mm.parameters.isNotEmpty)).isNotEmpty) {

var constructor = constructors.first;
for (ParameterMirror pm in constructor.parameters) {

if (!objectFieldValues.containsKey(pm.simpleName)) {
if (pm.isOptional) {
continue;
} else {
String parameterName = MirrorSystem.getName(pm.simpleName);
throw new BadRequestError('Required field $parameterName is missing');
}
}

if (pm.isNamed) {
namedArguments[pm.simpleName] = objectFieldValues[pm.simpleName];
} else {
positionalArguments.add(objectFieldValues[pm.simpleName]);
}
}
}

InstanceMirror schema = schemaClass.newInstance(new Symbol(''),
positionalArguments, namedArguments);


objectFieldValues.forEach((Symbol sym, var value) {
schema.setField(sym, value);
});

return schema.reflectee;
}

Expand Down
30 changes: 21 additions & 9 deletions lib/src/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -526,18 +526,30 @@ class ApiParser {
}
}

bool initializeConstructorParameters = false;
for (InstanceMirror im in schemaClass.metadata) {
if (im.reflectee is ApiMessage
&& im.reflectee.withConstructorParameters) {
initializeConstructorParameters = true;
break;
}
}

// If the schema is used as a request check that it has an unnamed default
// constructor.
// constructor, or a single default constructor if they've specified that
// constructor parameters should be initialized.
if (isRequest) {
var methods = schemaClass.declarations.values
.where((mm) => mm is MethodMirror && mm.isConstructor);
if (!methods.isEmpty &&
methods
.where((mm) => (mm.simpleName == schemaClass.simpleName &&
mm.parameters.isEmpty))
.isEmpty) {
addError('Schema \'$name\' must have an unnamed constructor taking no '
'arguments.');
.where((mm) => mm is MethodMirror && mm.isConstructor &&
mm.simpleName == schemaClass.simpleName);

if (initializeConstructorParameters) {
if (methods.isEmpty) {
addError('Schema \'$name\' must have exactly one unnamed constructor.');
}
} else if (methods.where((mm) => (mm.parameters.isEmpty)).isEmpty) {
addError('Schema \'$name\' must have an unnamed constructor taking no '
'arguments.');
}
}
schemaConfig = new ApiConfigSchema(name, schemaClass, isRequest);
Expand Down
169 changes: 168 additions & 1 deletion test/src/invocation/invoke_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,66 @@ class InheritanceChildClassBaseExcluded extends InheritanceBaseClass {
String stringFromChild = 'default from child, base excluded';
}

@ApiMessage(withConstructorParameters: true)
class ConstructorPositionalParametersMessage {
String aString;
int anInt;
DateTime aDateTime;
List<int> aList;
Map<String, int> aMap;
String anOptionalString;
int anOptionalInt;
DateTime anOptionalDateTime;
List<int> anOptionalList;
Map<String, int> anOptionalMap;

ConstructorPositionalParametersMessage(
this.aString,
this.anInt,
this.aDateTime,
this.aList,
this.aMap,
[this.anOptionalString = 'default',
this.anOptionalInt = -1,
this.anOptionalDateTime,
this.anOptionalList = const [1, 2, 3],
this.anOptionalMap = const {"one": 1, "two": 2, "three": 3}]) {
if (anOptionalDateTime == null) {
anOptionalDateTime = DateTime.parse('1969-07-20T20:18:00.000Z');
}
}
}

@ApiMessage(withConstructorParameters: true)
class ConstructorNamedParametersMessage {
String aString;
int anInt;
DateTime aDateTime;
List<int> aList;
Map<String, int> aMap;
String anOptionalString;
int anOptionalInt;
DateTime anOptionalDateTime;
List<int> anOptionalList;
Map<String, int> anOptionalMap;

ConstructorNamedParametersMessage(
this.aString,
this.anInt,
this.aDateTime,
this.aList,
this.aMap,
{this.anOptionalString: 'default',
this.anOptionalInt: -1,
this.anOptionalDateTime,
this.anOptionalList: const [1, 2, 3],
this.anOptionalMap: const {"one": 1, "two": 2, "three": 3}}) {
if (anOptionalDateTime == null) {
anOptionalDateTime = DateTime.parse('1969-07-20T20:18:00.000Z');
}
}
}

@ApiClass(version: 'v1')
class TestAPI {
@ApiResource()
Expand Down Expand Up @@ -275,6 +335,18 @@ class PostAPI {
InheritanceChildClassBaseExcluded message) {
return message;
}

@ApiMethod(method: 'POST', path: 'post/constructorPositionalParametersMessage')
ConstructorPositionalParametersMessage constructorPositionalParametersMessagePost(
ConstructorPositionalParametersMessage message) {
return message;
}

@ApiMethod(method: 'POST', path: 'post/constructorNamedParametersMessage')
ConstructorNamedParametersMessage constructorNamedParametersMessagePost(
ConstructorNamedParametersMessage message) {
return message;
}
}

class PutAPI {
Expand Down Expand Up @@ -683,6 +755,39 @@ main() async {
var resultBody = await _decodeBody(response.body);
expect(resultBody, {'stringFromChild': 'posted string from child'});
});
test('add-constructor-positional-parameters-message', () async {

for (String route in ['post/constructorPositionalParametersMessage',
'post/constructorNamedParametersMessage']) {
var objectFields = {
'aString': 'posted string',
'anInt': 42,
'aDateTime': '1979-12-20T01:02:03.456Z',
'aList': [2, 3, 5, 7],
'aMap': {"two": 2, "three": 3, "five": 5, "seven": 7}
};

HttpApiResponse response = await _sendRequest('POST', route,
body: objectFields);
var resultBody = await _decodeBody(response.body);

objectFields['anOptionalString'] = 'default';
objectFields['anOptionalInt'] = -1;
objectFields['anOptionalDateTime'] = '1969-07-20T20:18:00.000Z';
objectFields['anOptionalList'] = [1, 2, 3];
objectFields['anOptionalMap'] = {"one": 1, "two": 2, "three": 3};
expect(resultBody, objectFields);

objectFields['anOptionalString'] = 'from test';
objectFields['anOptionalInt'] = -42;
objectFields['anOptionalDateTime'] = '1979-12-20T01:03:04.456Z';
objectFields['anOptionalList'] = [4, 5, 6];
objectFields['anOptionalMap'] = {"four": 4, "five": 5, "six": 6};
response = await _sendRequest('POST', route, body: objectFields);
resultBody = await _decodeBody(response.body);
expect(resultBody, objectFields);
}
});
});

group('api-invoke-put', () {
Expand Down Expand Up @@ -737,7 +842,7 @@ main() async {
var result = await _decodeBody(response.body);
var expectedResult = {
'kind': 'discovery#restDescription',
'etag': 'b84bbbc4efcd363eccdc86066621cc34bdb49e1c',
'etag': '073af250fc0f0d51ec66fe3b215801631140e489',
'discoveryVersion': 'v1',
'id': 'testAPI:v1',
'name': 'testAPI',
Expand Down Expand Up @@ -854,6 +959,50 @@ main() async {
'id': 'MapOfint',
'type': 'object',
'additionalProperties': {'type': 'integer', 'format': 'int32'}
},
'ConstructorPositionalParametersMessage': {
'id': 'ConstructorPositionalParametersMessage',
'type': 'object',
'properties': {
'aString': {'type': 'string'},
'anInt': {'type': 'integer', 'format': 'int32'},
'aDateTime': {'type': 'string', 'format': 'date-time'},
'aList': {'type': 'array', 'items': {'type': 'integer', 'format': 'int32'}},
'aMap': {
'type': 'object',
'additionalProperties': {'type': 'integer', 'format': 'int32'}
},
'anOptionalString': {'type': 'string'},
'anOptionalInt': {'type': 'integer', 'format': 'int32'},
'anOptionalDateTime': {'type': 'string', 'format': 'date-time'},
'anOptionalList': {'type': 'array', 'items': {'type': 'integer', 'format': 'int32'}},
'anOptionalMap': {
'type': 'object',
'additionalProperties': {'type': 'integer', 'format': 'int32'}
}
}
},
'ConstructorNamedParametersMessage': {
'id': 'ConstructorNamedParametersMessage',
'type': 'object',
'properties': {
'aString': {'type': 'string'},
'anInt': {'type': 'integer', 'format': 'int32'},
'aDateTime': {'type': 'string', 'format': 'date-time'},
'aList': {'type': 'array', 'items': {'type': 'integer', 'format': 'int32'}},
'aMap': {
'type': 'object',
'additionalProperties': {'type': 'integer', 'format': 'int32'}
},
'anOptionalString': {'type': 'string'},
'anOptionalInt': {'type': 'integer', 'format': 'int32'},
'anOptionalDateTime': {'type': 'string', 'format': 'date-time'},
'anOptionalList': {'type': 'array', 'items': {'type': 'integer', 'format': 'int32'}},
'anOptionalMap': {
'type': 'object',
'additionalProperties': {'type': 'integer', 'format': 'int32'}
}
}
}
},
'methods': {},
Expand Down Expand Up @@ -1105,6 +1254,24 @@ main() async {
'parameterOrder': [],
'request': {r'$ref': 'InheritanceChildClassBaseExcluded'},
'response': {r'$ref': 'InheritanceChildClassBaseExcluded'}
},
'constructorPositionalParametersMessagePost': {
'id': 'TestAPI.post.constructorPositionalParametersMessagePost',
'path': 'post/constructorPositionalParametersMessage',
'httpMethod': 'POST',
'parameters': {},
'parameterOrder': [],
'request': {r'$ref': 'ConstructorPositionalParametersMessage'},
'response': {r'$ref': 'ConstructorPositionalParametersMessage'}
},
'constructorNamedParametersMessagePost': {
'id': 'TestAPI.post.constructorNamedParametersMessagePost',
'path': 'post/constructorNamedParametersMessage',
'httpMethod': 'POST',
'parameters': {},
'parameterOrder': [],
'request': {r'$ref': 'ConstructorNamedParametersMessage'},
'response': {r'$ref': 'ConstructorNamedParametersMessage'}
}
},
'resources': {}
Expand Down

0 comments on commit 3a464c8

Please sign in to comment.