├── .travis.yml ├── lib ├── database.dart ├── src │ ├── json_codec.dart │ ├── mapper_impl.dart │ ├── metadata.dart │ └── validation_impl.dart ├── plugin.dart ├── mapper_factory_static.dart ├── mapper.dart ├── mapper_factory.dart └── transformer.dart ├── test ├── client_test.dart ├── src │ ├── redstone_service.dart │ ├── domain.dart │ └── common_tests.dart ├── run.sh └── server │ └── server_test.dart ├── .gitignore ├── pubspec.yaml ├── LICENSE ├── CHANGELOG.md └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis CI Support 2 | language: dart 3 | sudo: false 4 | dart: 5 | - stable 6 | - dev 7 | with_content_shell: true 8 | script: export DISPLAY=:99.0 && sh -e /etc/init.d/xvfb start && pub run test -p vm,content-shell 9 | -------------------------------------------------------------------------------- /lib/database.dart: -------------------------------------------------------------------------------- 1 | library redstone_mapper_database; 2 | 3 | import 'dart:async'; 4 | 5 | ///Manage connections with a database. 6 | abstract class DatabaseManager { 7 | 8 | Future getConnection(); 9 | 10 | void closeConnection(T connection, {dynamic error}); 11 | 12 | } -------------------------------------------------------------------------------- /test/client_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn("browser") 2 | library client_test; 3 | 4 | import 'package:redstone_mapper/mapper_factory.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import 'src/common_tests.dart'; 8 | 9 | main() { 10 | bootstrapMapper(); 11 | 12 | installCommonTests(); 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don’t commit the following files and directories created by pub, Dart Editor, and dart2js 2 | **/packages 3 | .packages 4 | .project 5 | .buildlog 6 | .idea 7 | .pub 8 | *.js_ 9 | *.js.deps 10 | *.js.map 11 | build 12 | 13 | # Include when developing application packages 14 | pubspec.lock 15 | -------------------------------------------------------------------------------- /test/src/redstone_service.dart: -------------------------------------------------------------------------------- 1 | library redstone_service; 2 | 3 | import 'package:redstone/redstone.dart'; 4 | import 'package:redstone_mapper/plugin.dart'; 5 | 6 | import 'domain.dart'; 7 | 8 | @Route("/service", methods: const [POST]) 9 | @Encode() 10 | service(@Decode() User user) { 11 | var resp = []; 12 | for (var i = 0; i < 3; i++) { 13 | resp.add(user); 14 | } 15 | return resp; 16 | } 17 | 18 | @Route("/service_list", methods: const [POST]) 19 | @Encode() 20 | serviceList(@Decode() List users) { 21 | return users; 22 | } 23 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: redstone_mapper 2 | version: 0.2.0 3 | authors: 4 | - Luiz Mineo 5 | - Joel Trottier-Hebert 6 | description: A mapper plugin for Redstone.dart 7 | homepage: http://redstonedart.org 8 | environment: 9 | sdk: '>=1.9.0-dev.9.1 <2.0.0' 10 | dependencies: 11 | redstone: 12 | analyzer: "^0.27.0" 13 | code_transformers: ">= 0.3.1 <= 0.4.0" 14 | di: "^3.3.6" 15 | dev_dependencies: 16 | test: "^0.12.6" 17 | transformers: 18 | - redstone_mapper 19 | - test/pub_serve: 20 | $include: test/client_test.dart 21 | -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run server tests 4 | results=$(dart test/server/server_test.dart 2>&1) 5 | echo -e "$results" 6 | 7 | if [[ "$results" == *"FAIL"* ]] 8 | then 9 | exit 1 10 | fi 11 | 12 | #compile to javascript 13 | results=$(pub build --mode debug test/ 2>&1) 14 | echo "$results" 15 | if [[ "$results" == *"Build failed"* ]] 16 | then 17 | exit 1 18 | fi 19 | 20 | #run client tests 21 | which content_shell 22 | if [[ $? -ne 0 ]]; then 23 | $DART_SDK/../chromium/download_contentshell.sh 24 | unzip content_shell-linux-x64-release.zip 25 | 26 | cs_path=$(ls -d drt-*) 27 | PATH=$cs_path:$PATH 28 | fi 29 | 30 | results=$(content_shell --dump-render-tree build/test/client_test.html 2>&1) 31 | echo -e "$results" 32 | 33 | if [[ "$results" == *"FAIL"* ]] 34 | then 35 | exit 1 36 | fi -------------------------------------------------------------------------------- /lib/src/json_codec.dart: -------------------------------------------------------------------------------- 1 | part of redstone_mapper; 2 | 3 | /** 4 | * A JSON codec. 5 | * 6 | * This codec can be used to transfer objects between client and 7 | * server. It recursively encode objects to Maps and Lists, which 8 | * can be easily converted to json. 9 | * 10 | * When using on the client side, be sure to set the redstone_mapper's 11 | * transformer in your pubspec.yaml. 12 | */ 13 | final GenericTypeCodec jsonCodec = new GenericTypeCodec(typeCodecs: { 14 | DateTime: new Iso8601Codec() 15 | }); 16 | 17 | ///A codec to convert between DateTime objects and strings. 18 | class Iso8601Codec extends Codec { 19 | 20 | final _decoder = new Iso8601Decoder(); 21 | final _encoder = new Iso8601Encoder(); 22 | 23 | @override 24 | Converter get decoder => _decoder; 25 | 26 | @override 27 | Converter get encoder => _encoder; 28 | 29 | } 30 | 31 | class Iso8601Encoder extends Converter { 32 | 33 | @override 34 | convert(input) => input.toIso8601String(); 35 | 36 | } 37 | 38 | class Iso8601Decoder extends Converter { 39 | 40 | @override 41 | convert(input) => DateTime.parse(input); 42 | 43 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Luiz Mineo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/server/server_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | library server_test; 3 | 4 | import 'dart:convert' as conv; 5 | 6 | import 'package:redstone_mapper/mapper.dart'; 7 | import 'package:redstone_mapper/mapper_factory.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | import 'package:redstone/redstone.dart'; 11 | import 'package:redstone_mapper/plugin.dart'; 12 | import '../src/redstone_service.dart'; 13 | import '../src/common_tests.dart'; 14 | import '../src/domain.dart'; 15 | 16 | main() { 17 | bootstrapMapper(); 18 | 19 | installCommonTests(); 20 | 21 | test("Redstone Plugin", () async { 22 | addPlugin(getMapperPlugin()); 23 | await redstoneSetUp([#redstone_service]); 24 | 25 | var user = new User() 26 | ..username = "user" 27 | ..password = "1234"; 28 | var req = new MockRequest("/service", 29 | method: POST, bodyType: JSON, body: encode(user)); 30 | var req2 = new MockRequest("/service_list", 31 | method: POST, bodyType: JSON, body: encode([user, user, user])); 32 | 33 | var expected = conv.JSON.encode([ 34 | {"username": "user", "password": "1234"}, 35 | {"username": "user", "password": "1234"}, 36 | {"username": "user", "password": "1234"} 37 | ]); 38 | 39 | var resp = await dispatch(req); 40 | 41 | expect(resp.mockContent, equals(expected)); 42 | 43 | resp = await dispatch(req2); 44 | 45 | expect(resp.mockContent, equals(expected)); 46 | 47 | redstoneTearDown(); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v.0.2.0 2 | * Added annotation for ignoring value : `IgnoreValue`. 3 | * Bumped dependencies 4 | 5 | ## v0.2.0-beta.1+2 6 | * Updated dependency to work with the new code_transformers version. di.dart still depends on a old one, hence the git dependency. 7 | 8 | ## v0.1.13 9 | * Fix transformer crash when encoding a Map or List without type parameters. 10 | 11 | ## v0.1.12 12 | * Fix decode of Map values 13 | 14 | ## v0.1.11 15 | * Fix: decode and encode of null value now returns null, instead of throwing an error. 16 | * Fix: directly encoding or decoding core types (for example: decode(42, int);) does not work when compiled to javascript 17 | 18 | ## v0.1.10 19 | * Fix decoding of inherited properties 20 | 21 | ## v0.1.9 22 | * Updated dependencies. 23 | 24 | **Note:** this version requires Dart 1.7 or above 25 | 26 | ## v0.1.8+1 27 | This release includes fixes and improvements for the client-side support (thanks to [prujohn](https://github.com/prujohn) for all the feedback): 28 | 29 | 30 | * Fix: Compilation errors when using the `view` or `model` parameters on fields. 31 | * Fix: When compiled to javascript, the mapper can't encode or decode objects with nested lists or maps. 32 | * Added the `encodeJson()` and `decodeJson()` top-level functions. 33 | * Improved error handling. 34 | * Improved documentation: 35 | * Fixed some typos (thanks to [sethladd](https://github.com/sethladd)) 36 | * Added information about integration with polymer 37 | 38 | ## v0.1.7 39 | * Widen the version constraint for `code_transformers` 40 | 41 | ## v0.1.6 42 | * Fix: When compiled to Javascript, redstone_mapper is not decoding DateTime objects properly. 43 | * Fix: redstone_mapper should not suppress error messages from the mirrors api. 44 | 45 | ## v0.1.5 46 | * Fix: when mapping json objects, redstone_mapper should handle DateTime objects as ISO 8601 strings. 47 | 48 | ## v0.1.4 49 | * Widen the version constraint for `analyzer` 50 | 51 | ## v0.1.3 52 | * Fix: Properly handle setter methods. 53 | 54 | ## v0.1.2 55 | * The `@Encode` annotation can now be used with groups. If a group is annotated with `@Encode`, then redstone_mapper will encode the response of all routes within the group. 56 | 57 | ## v0.1.1 58 | * Fix: transformer is generating broken code for validators. 59 | 60 | ## v0.1.0 61 | * First release. 62 | -------------------------------------------------------------------------------- /lib/src/mapper_impl.dart: -------------------------------------------------------------------------------- 1 | part of redstone_mapper; 2 | 3 | final _defaultFieldDecoder = 4 | (final Object encodedData, final String fieldName, final Field fieldInfo, final List metadata) { 5 | String name = fieldName; 6 | 7 | if (fieldInfo.view is String) { 8 | if (fieldInfo.view.isEmpty) { 9 | return ignoreValue; 10 | } 11 | 12 | name = fieldInfo.view; 13 | } 14 | 15 | return (encodedData as Map)[name]; 16 | }; 17 | 18 | final _defaultFieldEncoder = (final Map encodedData, final String fieldName, 19 | final Field fieldInfo, final List metadata, final Object value) { 20 | if (value == null) { 21 | return; 22 | } 23 | 24 | String name = fieldName; 25 | 26 | if (fieldInfo.view is String) { 27 | if (fieldInfo.view.isEmpty) { 28 | return; 29 | } 30 | 31 | name = fieldInfo.view; 32 | } 33 | 34 | encodedData[name] = value; 35 | }; 36 | 37 | class _TypeDecoder extends Converter { 38 | final Type type; 39 | final FieldDecoder fieldDecoder; 40 | final Map typeCodecs; 41 | 42 | _TypeDecoder(this.fieldDecoder, {this.type, this.typeCodecs: const {}}); 43 | 44 | @override 45 | convert(input, [Type type]) { 46 | if (type == null) { 47 | type = this.type; 48 | } 49 | 50 | Mapper mapper = _mapperFactory(type); 51 | return mapper.decoder(input, fieldDecoder, typeCodecs, type); 52 | } 53 | } 54 | 55 | class _TypeEncoder extends Converter { 56 | final Type type; 57 | final FieldEncoder fieldEncoder; 58 | final Map typeCodecs; 59 | 60 | _TypeEncoder(this.fieldEncoder, {this.type, this.typeCodecs: const {}}); 61 | 62 | @override 63 | convert(input, [Type type]) { 64 | if (input is List) { 65 | return input.map((data) => _convert(data, type)).toList(); 66 | } else if (input is Map) { 67 | var encodedMap = {}; 68 | input.forEach((key, value) { 69 | encodedMap = _convert(value, type); 70 | }); 71 | return encodedMap; 72 | } else { 73 | return _convert(input, type); 74 | } 75 | } 76 | 77 | _convert(input, Type type) { 78 | if (type == null) { 79 | if (this.type == null) { 80 | type = input.runtimeType; 81 | } else { 82 | type = this.type; 83 | } 84 | } 85 | 86 | Mapper mapper = _mapperFactory(type); 87 | return mapper.encoder(input, fieldEncoder, typeCodecs); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/metadata.dart: -------------------------------------------------------------------------------- 1 | part of redstone_mapper; 2 | 3 | /** 4 | * An annotation to define class members that can 5 | * be encoded or decoded. 6 | * 7 | * The [view] and [model] parameters have the same purpose: 8 | * instruct the codec that the field has a different name 9 | * when the object is encoded. However, it's up to the codec 10 | * to decide which parameter to use. 11 | * 12 | * For example, codecs designed to map data between the client 13 | * and the server, will usually read from [view]. By other hand, 14 | * codecs designed to map data between a database and the server, 15 | * will usually read from [model]. This provides a convenient way 16 | * to map data between the database and the client. 17 | * 18 | * It's important to always define the type of the class member, so 19 | * the codec can properly encode and decode it. If the field is a List 20 | * or a Map, be sure to specify its parameters. Example: 21 | * 22 | * class User { 23 | * 24 | * @Field() 25 | * String name; 26 | * 27 | * @Field() 28 | * List
adresses; 29 | * 30 | * } 31 | * 32 | * class Address { 33 | * 34 | * @Field() 35 | * String description; 36 | * 37 | * @Field() 38 | * int number; 39 | * 40 | * } 41 | * 42 | * However, it's not recommended to use other classes that have 43 | * type parameters, since it's not guaranteed that the codec will 44 | * be able to properly encode and decode it. 45 | * 46 | * Also, every class that can be encoded or decoded must provide 47 | * a default constructor, with no required arguments. 48 | * 49 | */ 50 | class Field { 51 | 52 | final String model; 53 | final String view; 54 | 55 | const Field({this.view, this.model}); 56 | 57 | } 58 | 59 | /** 60 | * A rule that can be executed by a [Validator]. 61 | * 62 | * To build a new rule, you just have to inherit from 63 | * [ValidationRule] and provide a [validate] method. 64 | * 65 | * It's not strictly required to provide a const constructor, 66 | * but it's necessary if you want to use the rule as an annotation. 67 | * 68 | * See also [NotEmpty], [Range], [Matches] and [OnlyNumbers]. 69 | */ 70 | abstract class ValidationRule { 71 | 72 | final String type; 73 | 74 | /** 75 | * construct or get a rule instance 76 | * 77 | * [type] is a name that will be used to identify this 78 | * rule when a validation test fails. 79 | * 80 | */ 81 | const ValidationRule(String this.type); 82 | 83 | ///returns true if [value] is valid, false otherwise. 84 | bool validate(dynamic value); 85 | 86 | } -------------------------------------------------------------------------------- /lib/plugin.dart: -------------------------------------------------------------------------------- 1 | library redstone_mapper_plugin; 2 | 3 | import 'package:redstone/redstone.dart'; 4 | import 'package:shelf/shelf.dart' as shelf; 5 | import 'package:di/di.dart'; 6 | 7 | import 'package:redstone_mapper/database.dart'; 8 | import 'package:redstone_mapper/mapper.dart'; 9 | import 'package:redstone_mapper/mapper_factory.dart'; 10 | 11 | import 'dart:mirrors' as mirrors; 12 | 13 | /** 14 | * An annotation to define a target parameter. 15 | * 16 | * Parameters annotated with this annotation 17 | * can be decoded from the request's body or 18 | * query parameters. 19 | * 20 | * [from] are the body types accepted by this target, and defaults to JSON. 21 | * If [fromQueryParams] is true, then this parameter will be decoded from 22 | * the query parameters. 23 | * 24 | * Example: 25 | * 26 | * @app.Route('/services/users/add', methods: const[app.POST]) 27 | * addUser(@Decode() User user) { 28 | * ... 29 | * } 30 | */ 31 | class Decode { 32 | final List from; 33 | final bool fromQueryParams; 34 | 35 | const Decode( 36 | {List this.from: const [JSON], bool this.fromQueryParams: false}); 37 | } 38 | 39 | /** 40 | * An annotation to define routes whose response 41 | * can be encoded. 42 | * 43 | * Example: 44 | * 45 | * @app.Route('/services/users/list') 46 | * @Encode() 47 | * List listUsers() { 48 | * ... 49 | * } 50 | * 51 | */ 52 | class Encode { 53 | const Encode(); 54 | } 55 | 56 | /** 57 | * Get and configure the redstone_mapper plugin. 58 | * 59 | * If [db] is provided, then the plugin will initialize a database connection for 60 | * every request, and save it as a request attribute. If [dbPathPattern] is 61 | * provided, then the database connection will be initialized only for routes 62 | * that match the pattern. 63 | * 64 | * For more details about database integration, see the 65 | * [redstone_mapper_mongo](https://github.com/luizmineo/redstone_mapper_mongo) 66 | * and [redstone_mapper_pg](https://github.com/luizmineo/redstone_mapper_pg) packages. 67 | * 68 | * Usage: 69 | * 70 | * import 'package:redstone/server.dart' as app; 71 | * import 'package:redstone_mapper/plugin.dart'; 72 | * 73 | * main() { 74 | * 75 | * app.addPlugin(getMapperPlugin()); 76 | * ... 77 | * app.start(); 78 | * 79 | * } 80 | * 81 | */ 82 | RedstonePlugin getMapperPlugin( 83 | [DatabaseManager db, String dbPathPattern = r'/.*']) { 84 | return (Manager manager) { 85 | bootstrapMapper(); 86 | 87 | if (db != null) { 88 | 89 | manager.addInterceptor(new Interceptor(dbPathPattern), "database connection manager", 90 | (Injector injector, Request request) async { 91 | var conn = await db.getConnection(); 92 | request.attributes["dbConn"] = conn; 93 | var resp = await chain.next(); 94 | db.closeConnection(conn, error: chain.error); 95 | return resp; 96 | }); 97 | } 98 | 99 | manager.addParameterProvider(Decode, (dynamic metadata, Type paramType, 100 | String handlerName, String paramName, Request request, 101 | Injector injector) { 102 | var data; 103 | if (metadata.fromQueryParams) { 104 | var params = request.queryParameters; 105 | data = {}; 106 | params.forEach((String k, List v) { 107 | data[k] = v[0]; 108 | }); 109 | } else { 110 | if (!metadata.from.contains(request.bodyType)) { 111 | throw new ErrorResponse(400, 112 | "$handlerName: ${request.bodyType} not supported for this handler"); 113 | } 114 | data = request.body; 115 | } 116 | 117 | try { 118 | return decode(data, paramType); 119 | } catch (e) { 120 | try { 121 | return mirrors.reflectClass(paramType).newInstance(#fromStringMap, [data]).reflectee; 122 | } 123 | catch (e2) { 124 | throw new ErrorResponse( 125 | 400, "$handlerName: Error parsing '$paramName' parameter: $e\n\nError using 'fromJson' constructor: $e2"); 126 | } 127 | } 128 | }); 129 | 130 | manager.addResponseProcessor(Encode, 131 | (metadata, handlerName, response, injector) { 132 | if (response == null || response is shelf.Response) { 133 | return response; 134 | } 135 | 136 | return encode(response); 137 | }, includeGroups: true); 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /test/src/domain.dart: -------------------------------------------------------------------------------- 1 | library domain_test; 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:redstone_mapper/mapper.dart'; 5 | 6 | final dateTest = DateTime.parse("2014-08-11 12:23:00"); 7 | 8 | class GenericProperty { 9 | @Field() 10 | String value; 11 | } 12 | 13 | class SpecializedProperty extends GenericProperty { 14 | @Field() 15 | String value; 16 | } 17 | 18 | class TestObj { 19 | @Field() 20 | String value1; 21 | 22 | @Field(view: "value2") 23 | int value2; 24 | 25 | @Field() 26 | bool value3; 27 | 28 | @Field() 29 | DateTime value4; 30 | 31 | @Field() 32 | GenericProperty property; 33 | 34 | bool operator ==(other) { 35 | return other is TestObj && 36 | other.value1 == value1 && 37 | other.value2 == value2 && 38 | other.value3 == value3 && 39 | other.value4 == value4; 40 | } 41 | 42 | int get hashCode => toString().hashCode; 43 | 44 | String toString() => ''' 45 | value1: $value1 46 | value2: $value2 47 | value3: $value3 48 | value4: $value4 49 | property: $property 50 | '''; 51 | } 52 | 53 | class TestObjIgnore { 54 | @Field() 55 | String value1; 56 | 57 | @Field(view: "value2") 58 | int value2; 59 | 60 | @Field(view: "") 61 | bool value3; 62 | 63 | bool operator ==(other) { 64 | return other is TestObjIgnore && 65 | other.value1 == value1 && 66 | other.value2 == value2 && 67 | other.value3 == value3; 68 | } 69 | 70 | int get hashCode => toString().hashCode; 71 | 72 | String toString() => ''' 73 | value1: $value1 74 | value2: $value2 75 | value3: $value3 76 | '''; 77 | } 78 | 79 | class TestComplexObj extends TestObj { 80 | @Field() 81 | TestInnerObj innerObj; 82 | 83 | @Field() 84 | List innerObjs; 85 | 86 | @Field() 87 | SpecializedProperty property; 88 | 89 | @Field() 90 | Map mapInnerObjs; 91 | 92 | operator ==(other) { 93 | return other is TestComplexObj && 94 | super == (other) && 95 | other.innerObj == innerObj && 96 | const ListEquality().equals(other.innerObjs, innerObjs) && 97 | const MapEquality().equals(other.mapInnerObjs, mapInnerObjs); 98 | } 99 | 100 | int get hashCode => toString().hashCode; 101 | 102 | String toString() => ''' 103 | ${super.toString()} 104 | innerObj: $innerObj 105 | innerObjs: $innerObjs 106 | mapInnerObjs: $mapInnerObjs 107 | property: $property 108 | '''; 109 | } 110 | 111 | class TestInnerObj { 112 | String _innerObjValue; 113 | 114 | @Field() 115 | String get innerObjValue => _innerObjValue; 116 | 117 | @Field() 118 | set innerObjValue(String value) => _innerObjValue = value; 119 | 120 | bool operator ==(other) { 121 | return other is TestInnerObj && other.innerObjValue == innerObjValue; 122 | } 123 | 124 | int get hashCode => toString().hashCode; 125 | 126 | String toString() => ''' 127 | innerObjValue: $innerObjValue 128 | '''; 129 | } 130 | 131 | class TestValidator extends Schema { 132 | @Field() 133 | @Matches(r"\w+") 134 | String value1; 135 | 136 | @Field() 137 | @Range(min: 9, max: 12) 138 | int value2; 139 | 140 | @Field() 141 | @NotEmpty() 142 | bool value3; 143 | } 144 | 145 | class User { 146 | @Field() 147 | String username; 148 | 149 | @Field() 150 | String password; 151 | } 152 | 153 | TestObj createSimpleObj() { 154 | var p = new GenericProperty()..value = "genericProperty"; 155 | var obj = new TestObj() 156 | ..value1 = "str" 157 | ..value2 = 10 158 | ..value3 = true 159 | ..value4 = dateTest 160 | ..property = p; 161 | return obj; 162 | } 163 | 164 | TestComplexObj createComplexObj() { 165 | var p = new SpecializedProperty()..value = "specializedProperty"; 166 | var innerObj1 = new TestInnerObj()..innerObjValue = "obj1"; 167 | var innerObj2 = new TestInnerObj()..innerObjValue = "obj2"; 168 | var innerObj3 = new TestInnerObj()..innerObjValue = "obj3"; 169 | var obj = new TestComplexObj() 170 | ..value1 = "str" 171 | ..value2 = 10 172 | ..value3 = true 173 | ..value4 = dateTest 174 | ..innerObj = innerObj1 175 | ..innerObjs = [innerObj2, innerObj3] 176 | ..mapInnerObjs = {1: innerObj1, 2: innerObj2, 3: innerObj3} 177 | ..property = p; 178 | return obj; 179 | } 180 | 181 | class Dummy { 182 | @Field() 183 | num dummy; 184 | } 185 | 186 | class Identifiable { 187 | @Field() 188 | String id; 189 | } 190 | 191 | class Nameable { 192 | @Field() 193 | String username; 194 | 195 | @Field() 196 | String password; 197 | } 198 | 199 | class MixedUser extends Dummy with Identifiable, Nameable { 200 | @override 201 | toString() => ''' 202 | id: $id 203 | username: $username 204 | password: $password'''; 205 | } 206 | 207 | MixedUser createMixedUser() { 208 | return new MixedUser() 209 | ..id = "me" 210 | ..username = "Alice" 211 | ..password = "thereisnone"; 212 | } 213 | -------------------------------------------------------------------------------- /lib/src/validation_impl.dart: -------------------------------------------------------------------------------- 1 | part of redstone_mapper; 2 | 3 | /** 4 | * Provides a default [Validator] instance. 5 | * 6 | * Usage: 7 | * 8 | * Class User extends Schema { 9 | * 10 | * @Field() 11 | * @NotEmpty() 12 | * String username; 13 | * 14 | * @Field() 15 | * @Range(min: 6, required: true) 16 | * String password; 17 | * 18 | * } 19 | * 20 | * ... 21 | * User user = new User() 22 | * ..username = "user" 23 | * ..password = "pass"; 24 | * var err = user.validate(); 25 | * if (err != null) { 26 | * ... 27 | * } 28 | * 29 | * 30 | */ 31 | abstract class Schema { 32 | 33 | Validator _validator; 34 | 35 | Schema() { 36 | _validator = _validatorFactory(runtimeType, true); 37 | } 38 | 39 | ///validate this object. 40 | ValidationError validate() => _validator.execute(this); 41 | 42 | } 43 | 44 | /** 45 | * Provides a convenient way to join the result of two 46 | * or more [Validator]s. 47 | * 48 | * If all elements of [errors] are null or empty, then 49 | * this function will return null. Otherwise, a [ValidationError] 50 | * with all errors found will be returned. 51 | */ 52 | ValidationError joinErrors(List errors) { 53 | var err = new ValidationError(); 54 | err = errors.fold(err, (prevError, error) => 55 | error == null ? prevError : prevError..join(error)); 56 | return err.invalidFields.isNotEmpty ? err : null; 57 | } 58 | 59 | /** 60 | * An exception generated when an object can't 61 | * be validated. 62 | * 63 | */ 64 | class ValidationException implements Exception { 65 | 66 | String message; 67 | 68 | ValidationException(String this.message); 69 | 70 | String toString() => "ValidationException: $message"; 71 | 72 | } 73 | 74 | 75 | /** 76 | * Validates if a value is not empty. 77 | * 78 | * If the value is a String, then this rule will return 79 | * the result of the following expression: 80 | * 81 | * value != null && value.trim().isNotEmpty; 82 | * 83 | * If the value is an Iterable, then it will return: 84 | * 85 | * value != null && value.isNotEmpty; 86 | * 87 | * For other types: 88 | * 89 | * value != null; 90 | * 91 | */ 92 | class NotEmpty extends ValidationRule { 93 | 94 | const NotEmpty() : super("notEmpty"); 95 | 96 | @override 97 | bool validate(value) { 98 | return value != null && 99 | (value is! String || value.trim().isNotEmpty) && 100 | (value is! Iterable || value.isNotEmpty); 101 | } 102 | } 103 | 104 | /** 105 | * Validates if a value is within a specific range. 106 | * 107 | * If the value is a number, then this rule will validate 108 | * the value itself. If the value is a String or an Iterable, 109 | * then it will validate its length. For other types, it will 110 | * return false. 111 | * 112 | * By default, this rule will return true to null values. If you want 113 | * to change this, you can set [required] to true. 114 | */ 115 | class Range extends ValidationRule { 116 | 117 | final num min; 118 | final num max; 119 | final bool required; 120 | 121 | const Range({num this.min, num this.max, bool this.required: false}) : super("range"); 122 | 123 | @override 124 | bool validate(value) { 125 | if (value == null) { 126 | return !required; 127 | } else if (value is num) { 128 | return (min == null || min <= value) && (max == null || value <= max); 129 | } else if (value is String || value is Iterable) { 130 | int l = value.length; 131 | return (min == null || min <= l) && (max == null || l <= max); 132 | } 133 | return false; 134 | } 135 | } 136 | 137 | /** 138 | * Validates if a value matches a specific regex. 139 | * 140 | * If value is a String, then this rule will return true 141 | * if the value matches the provided regex. Otherwise, it 142 | * will return false. 143 | * 144 | * By default, this rule will return true to null values. If you want 145 | * to change this, you can set [required] to true. 146 | */ 147 | class Matches extends ValidationRule { 148 | 149 | final String regexPattern; 150 | final bool required; 151 | 152 | const Matches(String this.regexPattern, {bool this.required: false}) : super("matches"); 153 | 154 | @override 155 | bool validate(value) { 156 | if (value == null) { 157 | return !required; 158 | } else if (value is String) { 159 | if (!required && value.trim().isEmpty) { 160 | return true; 161 | } 162 | var match = new RegExp(regexPattern).firstMatch(value); 163 | return match != null && match[0] == value; 164 | } 165 | return false; 166 | } 167 | } 168 | 169 | /** 170 | * Validates if a value contains only numbers. 171 | * 172 | * If value is a String, then this rule will return true 173 | * if the value contains only digit characters. Otherwise, 174 | * it will return false. 175 | * 176 | */ 177 | class OnlyNumbers extends Matches { 178 | 179 | const OnlyNumbers({bool required: false}) : 180 | super(r'\d+', required: required); 181 | 182 | } -------------------------------------------------------------------------------- /test/src/common_tests.dart: -------------------------------------------------------------------------------- 1 | library common_tests; 2 | 3 | import "package:redstone_mapper/mapper.dart"; 4 | import 'package:test/test.dart'; 5 | 6 | import "domain.dart"; 7 | 8 | installCommonTests() { 9 | group("Encode:", () { 10 | test("Null value", () { 11 | expect(encode(null), equals(null)); 12 | }); 13 | 14 | test("Core types", () { 15 | expect(encode([1, 2, 3, 4]), equals([1, 2, 3, 4])); 16 | }); 17 | 18 | test("Simple object", () { 19 | var obj = createSimpleObj(); 20 | 21 | var data = encode(obj); 22 | 23 | expect( 24 | data, 25 | equals({ 26 | "value1": "str", 27 | "value2": 10, 28 | "value3": true, 29 | "value4": dateTest.toIso8601String(), 30 | "property": {"value": "genericProperty"} 31 | })); 32 | }); 33 | 34 | test("Ignore", () { 35 | final bImpulse = new TestObjIgnore(); 36 | bImpulse.value1 = "str"; 37 | bImpulse.value2 = 10; 38 | bImpulse.value3 = true; 39 | 40 | var data = encode(bImpulse); 41 | 42 | expect( 43 | data, 44 | equals({ 45 | "value1": "str", 46 | "value2": 10, 47 | })); 48 | }); 49 | 50 | test("Complex object", () { 51 | var obj = createComplexObj(); 52 | 53 | var data = encode(obj); 54 | 55 | expect( 56 | data, 57 | equals({ 58 | "value1": "str", 59 | "value2": 10, 60 | "value3": true, 61 | "value4": dateTest.toIso8601String(), 62 | "innerObj": {"innerObjValue": "obj1"}, 63 | "innerObjs": [ 64 | {"innerObjValue": "obj2"}, 65 | {"innerObjValue": "obj3"} 66 | ], 67 | "mapInnerObjs": { 68 | 1: {"innerObjValue": "obj1"}, 69 | 2: {"innerObjValue": "obj2"}, 70 | 3: {"innerObjValue": "obj3"} 71 | }, 72 | "property": {"value": "specializedProperty"} 73 | })); 74 | }); 75 | 76 | test("Mixin", () { 77 | var obj = createMixedUser(); 78 | 79 | var data = encode(obj); 80 | 81 | expect( 82 | data, 83 | equals({ 84 | "id": "me", 85 | "username": "Alice", 86 | "password": "thereisnone", 87 | }) 88 | ); 89 | }); 90 | 91 | test("List", () { 92 | var list = [createSimpleObj(), createSimpleObj()]; 93 | 94 | var data = encode(list); 95 | var expected = { 96 | "value1": "str", 97 | "value2": 10, 98 | "value3": true, 99 | "value4": dateTest.toIso8601String(), 100 | "property": {"value": "genericProperty"} 101 | }; 102 | 103 | expect(data, equals([expected, expected])); 104 | }); 105 | }); 106 | 107 | group("Decode:", () { 108 | test("Null value", () { 109 | expect(decode(null, TestObj), equals(null)); 110 | }); 111 | 112 | test("Core types", () { 113 | expect(decode([1, 2, 3, 4], int), equals([1, 2, 3, 4])); 114 | }); 115 | 116 | test("Simple object", () { 117 | var obj = createSimpleObj(); 118 | 119 | var data = { 120 | "value1": "str", 121 | "value2": 10, 122 | "value3": true, 123 | "value4": dateTest.toIso8601String(), 124 | "property": {"value": "genericProperty"} 125 | }; 126 | 127 | var decoded = decode(data, TestObj); 128 | 129 | expect(decoded, equals(obj)); 130 | }); 131 | 132 | test("Ignore value", () { 133 | final bImpulse = new TestObjIgnore(); 134 | bImpulse.value1 = "str"; 135 | bImpulse.value2 = 10; 136 | bImpulse.value3 = null; 137 | 138 | var data = { 139 | "value1": "str", 140 | "value2": 10, 141 | "value3": true, 142 | }; 143 | 144 | var decoded = decode(data, TestObjIgnore); 145 | 146 | expect(decoded, equals(bImpulse)); 147 | }); 148 | 149 | test("Complex object", () { 150 | var obj = createComplexObj(); 151 | 152 | var data = { 153 | "value1": "str", 154 | "value2": 10, 155 | "value3": true, 156 | "value4": dateTest.toIso8601String(), 157 | "innerObj": {"innerObjValue": "obj1"}, 158 | "innerObjs": [ 159 | {"innerObjValue": "obj2"}, 160 | {"innerObjValue": "obj3"} 161 | ], 162 | "mapInnerObjs": { 163 | 1: {"innerObjValue": "obj1"}, 164 | 2: {"innerObjValue": "obj2"}, 165 | 3: {"innerObjValue": "obj3"} 166 | }, 167 | "property": {"value": "specializedProperty"} 168 | }; 169 | 170 | var decoded = decode(data, TestComplexObj); 171 | 172 | expect(decoded, equals(obj)); 173 | }); 174 | 175 | test("Mixin", () { 176 | var obj = createMixedUser(); 177 | 178 | var data = { 179 | "id": "me", 180 | "username": "Alice", 181 | "password": "thereisnone", 182 | }; 183 | 184 | var decoded = decode(data, MixedUser); 185 | 186 | // TODO Equality not supported for mixed class 187 | // expect(decoded, equals(obj)); 188 | expect(decoded.runtimeType, equals(obj.runtimeType)); 189 | expect(decoded.toString(), equals(obj.toString())); 190 | }); 191 | 192 | test("List", () { 193 | var data = { 194 | "value1": "str", 195 | "value2": 10, 196 | "value3": true, 197 | "value4": dateTest.toIso8601String(), 198 | "innerObj": {"innerObjValue": "obj1"}, 199 | "innerObjs": [ 200 | {"innerObjValue": "obj2"}, 201 | {"innerObjValue": "obj3"} 202 | ], 203 | "mapInnerObjs": { 204 | 1: {"innerObjValue": "obj1"}, 205 | 2: {"innerObjValue": "obj2"}, 206 | 3: {"innerObjValue": "obj3"} 207 | }, 208 | "property": {"value": "specializedProperty"} 209 | }; 210 | 211 | var list = [data, data]; 212 | 213 | var decoded = decode(list, TestComplexObj); 214 | 215 | expect(decoded, equals([createComplexObj(), createComplexObj()])); 216 | }); 217 | }); 218 | 219 | group("Validator:", () { 220 | test("using validator object", () { 221 | var validator = new Validator(TestObj) 222 | ..add("value1", const Matches(r'\w+')) 223 | ..add("value2", const Range(min: 9, max: 12)) 224 | ..add("value3", const NotEmpty()); 225 | 226 | var testObj = createSimpleObj(); 227 | expect(validator.execute(testObj), isNull); 228 | 229 | testObj.value1 = ",*["; 230 | testObj.value2 = 2; 231 | testObj.value3 = null; 232 | 233 | var invalidFields = { 234 | "value1": ["matches"], 235 | "value2": ["range"], 236 | "value3": ["notEmpty"] 237 | }; 238 | 239 | expect(validator.execute(testObj).invalidFields, equals(invalidFields)); 240 | }); 241 | 242 | test("using schema", () { 243 | var obj = new TestValidator() 244 | ..value1 = "str" 245 | ..value2 = 10 246 | ..value3 = true; 247 | 248 | expect(obj.validate(), isNull); 249 | 250 | obj.value1 = ",*["; 251 | obj.value2 = 2; 252 | obj.value3 = null; 253 | 254 | var invalidFields = { 255 | "value1": ["matches"], 256 | "value2": ["range"], 257 | "value3": ["notEmpty"] 258 | }; 259 | 260 | expect(obj.validate().invalidFields, equals(invalidFields)); 261 | }); 262 | }); 263 | } 264 | -------------------------------------------------------------------------------- /lib/mapper_factory_static.dart: -------------------------------------------------------------------------------- 1 | library redstone_mapper_factory_static; 2 | 3 | import 'dart:convert'; 4 | 5 | import 'package:redstone_mapper/mapper.dart'; 6 | 7 | /** 8 | * initialize the mapper system. 9 | * 10 | * This function provides a mapper implementation that 11 | * uses data generated by the redstone_mapper's transformer, 12 | * instead of relying on the mirrors API. 13 | * 14 | */ 15 | void staticBootstrapMapper(Map types) { 16 | _staticTypeInfo = types; 17 | 18 | configure(_getOrCreateMapper, _createValidator); 19 | } 20 | 21 | typedef dynamic StaticDecoder(Object data, 22 | StaticMapperFactory factory, 23 | FieldDecoder fieldDecoder, 24 | Map typeCodecs); 25 | 26 | typedef dynamic StaticEncoder(Object obj, 27 | StaticMapperFactory factory, 28 | FieldEncoder fieldEncoder, 29 | Map typeCodecs); 30 | 31 | typedef dynamic StaticMapperFactory(Type type, {bool encodable, bool isList, bool isMap}); 32 | 33 | typedef dynamic StaticFieldGetter(Object obj); 34 | 35 | class TypeInfo { 36 | 37 | final StaticEncoder encoder; 38 | final StaticDecoder decoder; 39 | final Map fields; 40 | 41 | TypeInfo(this.encoder, this.decoder, this.fields); 42 | 43 | } 44 | 45 | class FieldWrapper { 46 | 47 | List metadata; 48 | StaticFieldGetter getter; 49 | 50 | FieldWrapper(this.getter, [this.metadata = const [const Field()]]); 51 | 52 | } 53 | 54 | Map _staticTypeInfo = const {}; 55 | Map _cache = { 56 | String: const _StaticMapper.notEncodable(), 57 | int: const _StaticMapper.notEncodable(), 58 | double: const _StaticMapper.notEncodable(), 59 | num: const _StaticMapper.notEncodable(), 60 | bool: const _StaticMapper.notEncodable(), 61 | Object: const _StaticMapper.notEncodable(), 62 | Null: const _StaticMapper.notEncodable() 63 | }; 64 | 65 | class _StaticMapper implements Mapper { 66 | 67 | final MapperDecoder decoder; 68 | final MapperEncoder encoder; 69 | final Map _fields; 70 | 71 | _StaticMapper(this.decoder, this.encoder, this._fields); 72 | 73 | _StaticMapper.list([_StaticMapper wrap]) : 74 | decoder = new _ListDecoder(wrap), 75 | encoder = new _ListEncoder(wrap), 76 | _fields = const {}; 77 | 78 | _StaticMapper.map([_StaticMapper wrap]) : 79 | decoder = new _MapDecoder(wrap), 80 | encoder = new _MapEncoder(wrap), 81 | _fields = const {}; 82 | 83 | const _StaticMapper.notEncodable() : 84 | decoder = const _DefaultDecoder(), 85 | encoder = const _DefaultEncoder(), 86 | _fields = const {}; 87 | 88 | } 89 | 90 | class _DefaultDecoder { 91 | 92 | const _DefaultDecoder(); 93 | 94 | call(Object data, FieldDecoder fieldDecoder, 95 | Map typeCodecs, [Type type]) { 96 | return data; 97 | } 98 | 99 | } 100 | 101 | class _DefaultEncoder { 102 | 103 | const _DefaultEncoder(); 104 | 105 | call(Object obj, FieldEncoder fieldEncoder, Map typeCodecs) { 106 | return obj; 107 | } 108 | 109 | } 110 | 111 | class _MapDecoder { 112 | 113 | final _StaticMapper wrappedMapper; 114 | 115 | _MapDecoder([this.wrappedMapper]); 116 | 117 | call(Object data, FieldDecoder fieldDecoder, 118 | Map typeCodecs, [Type type]) { 119 | if (data is! Map) { 120 | throw new MapperException("Expecting Map, found ${data.runtimeType}"); 121 | } 122 | 123 | var decoded = {}; 124 | (data as Map).forEach((key, value) { 125 | var mapper = wrappedMapper; 126 | if (mapper == null) { 127 | mapper = _getOrCreateMapper(type); 128 | } 129 | decoded[key] = mapper.decoder(value, fieldDecoder, typeCodecs); 130 | }); 131 | return decoded; 132 | } 133 | 134 | } 135 | 136 | class _MapEncoder { 137 | 138 | final _StaticMapper wrappedMapper; 139 | 140 | _MapEncoder([this.wrappedMapper]); 141 | 142 | call(Object data, FieldEncoder fieldEncoder, Map typeCodecs) { 143 | if (data is Map) { 144 | var encoded = {}; 145 | var mapper = wrappedMapper; 146 | if (mapper == null) { 147 | mapper = _getOrCreateMapper(data.runtimeType); 148 | } 149 | data.forEach((key, value) { 150 | encoded[key] = mapper.encoder(value, fieldEncoder, typeCodecs); 151 | }); 152 | return encoded; 153 | } 154 | return data; 155 | } 156 | 157 | } 158 | 159 | class _ListDecoder { 160 | 161 | final _StaticMapper wrappedMapper; 162 | 163 | _ListDecoder([this.wrappedMapper]); 164 | 165 | call(Object data, FieldDecoder fieldDecoder, 166 | Map typeCodecs, [Type type]) { 167 | if (data is! List) { 168 | throw new MapperException("Expecting List, found ${data.runtimeType}"); 169 | } 170 | 171 | var mapper = wrappedMapper; 172 | if (mapper == null) { 173 | mapper = _getOrCreateMapper(type); 174 | } 175 | return new List.from((data as List).map((value) => 176 | mapper.decoder(value, fieldDecoder, typeCodecs))); 177 | } 178 | 179 | } 180 | 181 | class _ListEncoder { 182 | 183 | final _StaticMapper wrappedMapper; 184 | 185 | _ListEncoder([this.wrappedMapper]); 186 | 187 | call(Object data, FieldEncoder fieldEncoder, Map typeCodecs) { 188 | if (data is List) { 189 | return new List.from(data.map((value) { 190 | var mapper = wrappedMapper; 191 | if (mapper == null) { 192 | mapper = _getOrCreateMapper(value.runtimeType); 193 | } 194 | return mapper.encoder(value, fieldEncoder, typeCodecs); 195 | })); 196 | } 197 | return data; 198 | } 199 | 200 | } 201 | 202 | _StaticMapper _getOrCreateMapper(Type type, 203 | {bool encodable: true, 204 | bool isList: false, 205 | bool isMap: false, 206 | _StaticMapper wrap}) { 207 | 208 | if (!encodable) { 209 | return const _StaticMapper.notEncodable(); 210 | } else if (isList) { 211 | return new _StaticMapper.list(wrap); 212 | } else if (isMap) { 213 | return new _StaticMapper.map(wrap); 214 | } else { 215 | var mapper = _cache[type]; 216 | if (mapper == null) { 217 | var typeInfo = _staticTypeInfo[type]; 218 | if (typeInfo != null) { 219 | 220 | var decoder = (data, fieldDecoder, typeCodecs, [fieldType]) { 221 | if (data == null) { 222 | return data; 223 | } 224 | if (data is List) { 225 | return new _StaticMapper.list().decoder(data, fieldDecoder, 226 | typeCodecs, type); 227 | } 228 | 229 | try { 230 | return typeInfo.decoder(data, _getOrCreateMapper, fieldDecoder, typeCodecs); 231 | } catch(e) { 232 | throw new MapperException("Failed to decode: $data \nreason: $e"); 233 | } 234 | }; 235 | 236 | var encoder = (obj, fieldEncoder, typeCodecs) { 237 | if (obj == null) { 238 | return null; 239 | } 240 | try { 241 | return typeInfo.encoder(obj, _getOrCreateMapper, fieldEncoder, typeCodecs); 242 | } catch(e) { 243 | throw new MapperException("Can't encode $obj: $e"); 244 | } 245 | }; 246 | 247 | mapper = new _StaticMapper(decoder, encoder, typeInfo.fields); 248 | } else { 249 | throw new MapperException("UnsupportedType: $type. " 250 | "This type wasn't mapped by redstone_mapper's transformer. See http://goo.gl/YYMou2 for more information."); 251 | } 252 | _cache[type] = mapper; 253 | } 254 | return mapper; 255 | } 256 | } 257 | 258 | class _StaticValidator implements Validator { 259 | 260 | final Type _type; 261 | 262 | Map _fields; 263 | Map> _rules = {}; 264 | 265 | _StaticValidator([Type this._type]) { 266 | if (_type != null) { 267 | _fields = _getOrCreateMapper(_type)._fields; 268 | } 269 | } 270 | 271 | @override 272 | addAll(Validator validator) { 273 | (validator as _StaticValidator)._rules.forEach((fieldName, rules) { 274 | rules.forEach((r) => add(fieldName, r)); 275 | }); 276 | } 277 | 278 | @override 279 | add(String field, ValidationRule rule) { 280 | if (_fields != null && !_fields.containsKey(field)) { 281 | throw new ValidationException("$_type has no field '$field'"); 282 | } 283 | var fieldRules = _rules[field]; 284 | if (fieldRules == null) { 285 | fieldRules = []; 286 | _rules[field] = fieldRules; 287 | } 288 | fieldRules.add(rule); 289 | } 290 | 291 | @override 292 | ValidationError execute(Object obj) { 293 | if (obj is Map) { 294 | return _validateMap(obj); 295 | } else { 296 | if (_type == null) { 297 | throw new ArgumentError("This validator can only validate maps"); 298 | } 299 | return _validateObj(obj); 300 | } 301 | } 302 | 303 | ValidationError _validateObj(obj) { 304 | 305 | var invalidFields = {}; 306 | _rules.forEach((field, rules) { 307 | rules.forEach((rule) { 308 | if (!rule.validate(_fields[field].getter(obj))) { 309 | List errors = invalidFields[field]; 310 | if (errors == null) { 311 | errors = []; 312 | invalidFields[field] = errors; 313 | } 314 | errors.add(rule.type); 315 | } 316 | }); 317 | }); 318 | 319 | if (invalidFields.isNotEmpty) { 320 | return new ValidationError(invalidFields); 321 | } 322 | 323 | return null; 324 | } 325 | 326 | ValidationError _validateMap(Map map) { 327 | var invalidFields = {}; 328 | 329 | _rules.forEach((field, rules) { 330 | rules.forEach((rule) { 331 | if (!rule.validate(map[field])) { 332 | List errors = invalidFields[field]; 333 | if (errors == null) { 334 | errors = []; 335 | invalidFields[field] = errors; 336 | } 337 | errors.add(rule.type); 338 | } 339 | }); 340 | }); 341 | 342 | if (invalidFields.isNotEmpty) { 343 | return new ValidationError(invalidFields); 344 | } 345 | 346 | return null; 347 | } 348 | 349 | } 350 | 351 | _StaticValidator _createValidator([Type type, bool parseAnnotations = false]) { 352 | var validator = new _StaticValidator(type); 353 | if (parseAnnotations) { 354 | validator._fields.forEach((fieldName, fieldData) { 355 | fieldData.metadata.where((m) => m is ValidationRule).forEach((r) => 356 | validator.add(fieldName, r)); 357 | }); 358 | } 359 | return validator; 360 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redstone Mapper 2 | 3 | [![Build Status](https://travis-ci.org/redstone-dart/redstone_mapper.svg)](https://travis-ci.org/redstone-dart/redstone_mapper) 4 | 5 | redstone_mapper is a set of utilities for handling common tasks in web applications, including: 6 | 7 | * Encoding and decoding of objects to JSON. 8 | * Data validation. 9 | * Database connection management. 10 | * Encoding and decoding of objects to the database. 11 | 12 | Encoding and decoding of objects and data validation can also be used on the client side. redstone_mapper provides a pub transformer that prevents dart2js from generating a bloated javascript file. 13 | 14 | Example: Using redstone_mapper with Redstone.dart 15 | 16 | ```dart 17 | 18 | import 'package:redstone/server.dart' as app; 19 | import 'package:redstone_mapper/mapper.dart'; 20 | import 'package:redstone_mapper/plugin.dart'; 21 | 22 | main() { 23 | 24 | //When using redstone_mapper as a Redstone.dart plugin, 25 | //you can use the @Decode and @Encode annotations. 26 | app.addPlugin(getMapperPlugin()); 27 | 28 | app.setupConsoleLog(); 29 | app.start(); 30 | } 31 | 32 | class User { 33 | 34 | //The @Field annotation is used to specify 35 | //the fields that can be serialized. 36 | @Field() 37 | String username; 38 | 39 | @Field() 40 | String password; 41 | 42 | } 43 | 44 | //The @Decode annotation specifies that a parameter value 45 | //must be decoded from the request. By default, it will expect 46 | //that the request has a JSON body. 47 | @app.Route('/services/users/add', methods: const[app.POST]) 48 | addUser(@Decode() User user) { 49 | ... 50 | } 51 | 52 | //The @Encode annotation specifies that a route 53 | //response can be encoded to JSON. 54 | @app.Route('/services/users/list') 55 | @Encode() 56 | List listUsers() { 57 | ... 58 | } 59 | 60 | ``` 61 | 62 | ### The @Field annotation 63 | 64 | To properly encode and decode an object, its class must have every serializable member 65 | annotated with `@Field`. 66 | 67 | ```dart 68 | 69 | class User { 70 | 71 | @Field() 72 | String username; 73 | 74 | @Field() 75 | String password; 76 | 77 | } 78 | 79 | ``` 80 | 81 | It's important to always define the type of the class member, so 82 | it can be properly encoded and decoded. If the field is a List 83 | or a Map, be sure to specify its parameters. Example: 84 | 85 | ```dart 86 | 87 | class User { 88 | 89 | @Field() 90 | String name; 91 | 92 | @Field() 93 | List
adresses; 94 | 95 | } 96 | 97 | class Address { 98 | 99 | @Field() 100 | String description; 101 | 102 | @Field() 103 | int number; 104 | 105 | } 106 | 107 | ``` 108 | 109 | However, it's not recommended to use other classes that have 110 | type parameters, since it's not guaranteed that it will 111 | be properly encoded and decoded. 112 | 113 | It's also possible to annotate getters and setters: 114 | 115 | ```dart 116 | class User { 117 | 118 | String _name; 119 | 120 | @Field() 121 | String get name() => _name; 122 | 123 | @Field() 124 | set name(String value) => _name = value; 125 | 126 | } 127 | 128 | ``` 129 | 130 | When encoding or decoding an object to JSON, you can use the `view` parameter 131 | to map a class member to its corresponding JSON field: 132 | 133 | ```dart 134 | 135 | class User { 136 | 137 | @Field(view: "user_name") 138 | String name; 139 | 140 | @Field() 141 | String pass; 142 | 143 | } 144 | 145 | ``` 146 | 147 | Also, if you are encoding or decoding an object to the database, you can use the `model` 148 | parameter to map a class member to its corresponding database field: 149 | 150 | ```dart 151 | 152 | class User { 153 | 154 | //JSON: 'user_name' DATABASE: 'USERNAME' 155 | @Field(view: "user_name", model: "USERNAME") 156 | String name; 157 | 158 | //JSON: 'pass' DATABASE: 'PASSWORD' 159 | @Field(model: "PASSWORD") 160 | String pass; 161 | } 162 | 163 | ``` 164 | 165 | Besides, every class that can be encoded or decoded must provide 166 | a default constructor, with no required arguments. 167 | 168 | ### Data validation 169 | 170 | The `Validator` class provides a simple and flexible way to build a set of validation rules. 171 | 172 | ```dart 173 | var userValidator = new Validator() 174 | ..add("username", const NotEmpty()) 175 | ..add("password", const Range(min: 6. required: true)); 176 | 177 | ... 178 | Map user = {"username": "user", "password": "pass"}; 179 | ValidationError err = userValidator.execute(user); 180 | if (err != null) { 181 | ... 182 | } 183 | ``` 184 | 185 | To validate objects, you must provide the target class to the constructor. Also, 186 | you must annotate with `@Field` all members that can be validated. 187 | 188 | ```dart 189 | Class User { 190 | 191 | @Field() 192 | String username; 193 | 194 | @Field() 195 | String password; 196 | 197 | } 198 | 199 | var userValidator = new Validator(User) 200 | ..add("username", const NotEmpty()) 201 | ..add("password", const Range(min: 6. required: true)); 202 | 203 | ... 204 | User user = new User() 205 | ..username = "user" 206 | ..password = "pass"; 207 | 208 | ValidationError err = userValidator.execute(user); 209 | if (err != null) { 210 | ... 211 | } 212 | ``` 213 | 214 | Alternatively, you can set the rules directly in the class. 215 | 216 | ```dart 217 | class User { 218 | 219 | @Field() 220 | @NotEmpty() 221 | String username; 222 | 223 | @Field() 224 | @Range(min: 6, required: true) 225 | String password; 226 | 227 | } 228 | 229 | var userValidator = new Validator(User, true); 230 | ``` 231 | 232 | You can also inherit from the `Schema` class, which will provide a `Validator` 233 | for you. 234 | 235 | ```dart 236 | class User extends Schema { 237 | 238 | @Field() 239 | @NotEmpty() 240 | String username; 241 | 242 | @Field() 243 | @Range(min: 6, required: true) 244 | String password; 245 | 246 | } 247 | 248 | ... 249 | var user = new User() 250 | ..username = "user" 251 | ..password = "pass"; 252 | 253 | var err = user.validate(); 254 | if (err != null) { 255 | ... 256 | } 257 | ``` 258 | 259 | redstone_mapper already provides the following basic rules, that you can use 260 | to build a `Validator`: 261 | 262 | * `NotEmpty`: 263 | * If the value is a String, verify if it isn't null, empty, or contains only spaces. 264 | * If the value is an Iterable, verify if isn't null or empty. 265 | * For other values, verify if it isn't null. 266 | * `Range`: 267 | * If the value is numeric, verify if it's within the specified range. 268 | * If the value is a String or an Iterable, verify if its length is within the specified range. 269 | * `Matches`: 270 | * For strings only: verify if the value matches the specified regex. 271 | * `OnlyNumbers`: 272 | * For strings only: verify if the value contains only digit characters. 273 | 274 | You can easily build new rules by just inheriting from the `ValidationRule` class. 275 | 276 | ### Configuration 277 | 278 | To install redstone_mapper as a Redstone.dart plugin, you just have to import `plugin.dart` and 279 | call `getMapperPlugin()`: 280 | 281 | ```dart 282 | 283 | import 'package:redstone/server.dart' as app; 284 | import 'package:redstone_mapper/plugin.dart'; 285 | 286 | import 'package:redstone/server.dart' as app; 287 | import 'package:redstone_mapper/mapper.dart'; 288 | import 'package:redstone_mapper/plugin.dart'; 289 | 290 | main() { 291 | 292 | //When using redstone_mapper as a Redstone.dart plugin, 293 | //you can use the @Decode and @Encode annotations. 294 | app.addPlugin(getMapperPlugin()); 295 | 296 | app.setupConsoleLog(); 297 | app.start(); 298 | } 299 | 300 | ``` 301 | 302 | Also, if `getMapperPlugin()` receives an instance of `DatabaseManager`, then the plugin will manage 303 | the database connections for you. For more information, see one of the redstone_mapper extensions, such as 304 | [redstone_mapper_pg](https://github.com/luizmineo/redstone_mapper_pg) or 305 | [redstone_mapper_mongo](https://github.com/luizmineo/redstone_mapper_mongo). 306 | 307 | To use with other server-side frameworks, or on the client side, you just have to import `mapper_factory.dart` 308 | and call `bootstrapMapper()` from the `main()` function: 309 | 310 | ```dart 311 | 312 | import 'package:redstone/server.dart' as app; 313 | import 'package:redstone_mapper/mapper_factory.dart'; 314 | 315 | main() { 316 | 317 | bootstrapMapper(); 318 | ... 319 | } 320 | 321 | ``` 322 | 323 | To encode and decode objects, you can use the `encodeJson()` and `decodeJson()` top level function from `mapper.dart`: 324 | 325 | ```dart 326 | 327 | import 'package:redstone_mapper/mapper.dart'; 328 | 329 | class User { 330 | 331 | @Field() 332 | String username; 333 | 334 | @Field() 335 | String password; 336 | 337 | } 338 | 339 | var user = new User() 340 | ..username = "user" 341 | ..password = "pass"; 342 | 343 | String userJson = encodeJson(user); 344 | 345 | ``` 346 | 347 | When using on the client side, be sure to set redstone_mapper's transformer to your pubspec.yaml 348 | file, so dart2js won't generate a bloated javascript file: 349 | 350 | ``` 351 | name: my_app 352 | version: 0.1.0 353 | dependencies: 354 | redstone: any 355 | redstone_mapper: any 356 | transformers: 357 | - redstone_mapper 358 | 359 | ``` 360 | 361 | ### Integration with Polymer 362 | 363 | Polymer applications usually doesn't have an entry-point (a dart script with the `main` function), so 364 | you have to provide one. Also, the entry-point has to import all libraries that contains encodable classes, 365 | so the transformer will be able to map them. You can see a working example which uses 366 | polymer and redstone_mapper [here](https://github.com/luizmineo/io_2014_contacts_demo). 367 | 368 | ### Database integration 369 | 370 | redstone_mapper provides integration with database drivers through extensions. Currently, the following extensions are available: 371 | 372 | * [redstone_mapper_mongo](https://github.com/luizmineo/redstone_mapper_mongo): MongoDB extension for redstone_mapper. 373 | * [redstone_mapper_pg](https://github.com/luizmineo/redstone_mapper_pg): PostgreSQL extension for redstone_mapper. 374 | 375 | Note that redstone_mapper doesn't aim to be a full ORM/ODM framework. It just provides some helper functions to easily 376 | encode and decode objects to the database. It won't generate database queries, neither hide the default driver API from you. 377 | That means you can use the redstone_mapper functions only when it's useful for you, and ignore it when it's just an extra overhead. 378 | 379 | #### What about other databases? 380 | 381 | Dart already has support for several databases, including: MongoDb, Redis, CouchDb, MySql, PostgreSql, and so on. I'll try to provide new extensions over 382 | time, but if you are interested, you can help me on this task. 383 | 384 | Building a redstone_mapper extension is really easy, and you can start by taking a look at the source code of [redstone_mapper_pg](https://github.com/luizmineo/redstone_mapper_pg) and [redstone_mapper_mongo](https://github.com/luizmineo/redstone_mapper_mongo). 385 | If you are willing to build a externsion, please let me know :) 386 | -------------------------------------------------------------------------------- /lib/mapper.dart: -------------------------------------------------------------------------------- 1 | library redstone_mapper; 2 | 3 | import 'dart:convert'; 4 | import 'dart:collection'; 5 | 6 | part 'src/mapper_impl.dart'; 7 | part 'src/validation_impl.dart'; 8 | part 'src/metadata.dart'; 9 | part 'src/json_codec.dart'; 10 | 11 | /** 12 | * Decode [data] to one or more objects of type [type], 13 | * using [defaultCodec]. 14 | * 15 | * [data] is expected to be a Map or 16 | * a List, and [type] a class which contains members 17 | * annotated with the [Field] annotation. 18 | * 19 | * If [data] is a Map, then this function will return 20 | * an object of [type]. Otherwise, if [data] is a List, then a 21 | * List<[type]> will be returned. 22 | * 23 | * For more information on how serialization 24 | * and deserialization of objects works, see [Field]. 25 | * 26 | * When using on the client side, be sure to set the redstone_mapper's 27 | * transformer in your pubspec.yaml. 28 | */ 29 | dynamic decode(dynamic data, Type type) { 30 | return defaultCodec.decode(data, type); 31 | } 32 | 33 | /** 34 | * Encode [input] using [defaultCodec]. 35 | * 36 | * [input] can be an object or a List of objects. 37 | * If it's an object, then this function will return 38 | * a Map, otherwise a List will be returned. 39 | * 40 | * For more information on how serialization 41 | * and deserialization of objects works, see [Field]. 42 | * 43 | * When using on the client side, be sure to set the redstone_mapper's 44 | * transformer in your pubspec.yaml. 45 | */ 46 | dynamic encode(dynamic input) { 47 | return defaultCodec.encode(input); 48 | } 49 | 50 | /** 51 | * Decode [json] to one or more objects of type [type]. 52 | * 53 | * [json] is expected to be a JSON object, or a list of 54 | * JSON objects, and [type] a class which contains members 55 | * annotated with the [Field] annotation. 56 | * 57 | * If [json] is a JSON object, then this function will return 58 | * an object of [type]. Otherwise, if [json] is a list, then a 59 | * List<[type]> will be returned. 60 | * 61 | * For more information on how serialization 62 | * and deserialization of objects works, see [Field]. 63 | * 64 | * When using on the client side, be sure to set the redstone_mapper's 65 | * transformer in your pubspec.yaml. 66 | */ 67 | dynamic decodeJson(String json, Type type) { 68 | return jsonCodec.decode(JSON.decode(json), type); 69 | } 70 | 71 | /** 72 | * Encode [input] to JSON. 73 | * 74 | * [input] can be an object or a List of objects. 75 | * If it's an object, then this function will return 76 | * a JSON object, otherwise a list of JSON objects 77 | * will be returned. 78 | * 79 | * For more information on how serialization 80 | * and deserialization of objects works, see [Field]. 81 | * 82 | * When using on the client side, be sure to set the redstone_mapper's 83 | * transformer in your pubspec.yaml. 84 | */ 85 | String encodeJson(dynamic input) { 86 | return JSON.encode(jsonCodec.encode(input)); 87 | } 88 | 89 | ///The codec used by the [decode] and [encode] top level functions. 90 | GenericTypeCodec defaultCodec = jsonCodec; 91 | 92 | /** 93 | * Configure the mapper system. 94 | * 95 | * Usually, you don't need to call this method directly, since it will 96 | * be called by the [bootstrapMapper] method. 97 | */ 98 | void configure(MapperFactory mapperFactory, ValidatorFactory validatorFactory) { 99 | _mapperFactory = mapperFactory; 100 | _validatorFactory = validatorFactory; 101 | } 102 | 103 | /** 104 | * A set of rules to validate maps and objects. 105 | * 106 | * This class provides a simple and flexible way to 107 | * build a set of validation rules. 108 | * 109 | * Usage: 110 | * 111 | * var userValidator = new Validator() 112 | * ..add("username", const NotEmpty()) 113 | * ..add("password", const Range(min: 6. required: true)); 114 | * 115 | * ... 116 | * Map user = {"username": "user", "password": "pass"}; 117 | * ValidationError err = userValidator.execute(user); 118 | * if (err != null) { 119 | * ... 120 | * } 121 | * 122 | * To validate objects, you must provide the target class to the constructor. Also, 123 | * you must annotate with [Field] the members that will be validated. 124 | * 125 | * Class User { 126 | * 127 | * @Field() 128 | * String username; 129 | * 130 | * @Field() 131 | * String password; 132 | * 133 | * } 134 | * 135 | * var userValidator = new Validator(User) 136 | * ..add("username", const NotEmpty()) 137 | * ..add("password", const Range(min: 6. required: true)); 138 | * 139 | * ... 140 | * User user = new User() 141 | * ..username = "user" 142 | * ..password = "pass"; 143 | * ValidationError err = userValidator.execute(user); 144 | * if (err != null) { 145 | * ... 146 | * } 147 | * 148 | * Alternatively, you can set the rules directly in the class. 149 | * 150 | * Class User { 151 | * 152 | * @Field() 153 | * @NotEmpty() 154 | * String username; 155 | * 156 | * @Field() 157 | * @Range(min: 6, required: true) 158 | * String password; 159 | * 160 | * } 161 | * 162 | * var userValidator = new Validator(User, true); 163 | * 164 | * You can also inherit from [Schema], which will provide a Validator 165 | * for you. 166 | * 167 | * Class User extends Schema { 168 | * 169 | * @Field() 170 | * @NotEmpty() 171 | * String username; 172 | * 173 | * @Field() 174 | * @Range(min: 6, required: true) 175 | * String password; 176 | * 177 | * } 178 | * 179 | * ... 180 | * User user = new User() 181 | * ..username = "user" 182 | * ..password = "pass"; 183 | * var err = user.validate(); 184 | * if (err != null) { 185 | * ... 186 | * } 187 | * 188 | * When using on the client side, be sure to set the redstone_mapper's 189 | * transformer in your pubspec.yaml. 190 | * 191 | */ 192 | abstract class Validator { 193 | 194 | /** 195 | * Construct a new Validator. 196 | * 197 | * If [type] is provided, then the validator will be able 198 | * to validate objects of type [type]. If [parseAnnotations] is 199 | * true, then the validator will load all rules that were specified 200 | * directly in the class. 201 | */ 202 | factory Validator([Type type, bool parseAnnotations = false]) => 203 | _validatorFactory(type, parseAnnotations); 204 | 205 | /** 206 | * Add a new [rule] to this Validator 207 | * 208 | * If this Validator is tied to a class, [field] must 209 | * be a class member. [rule] can be a instance of [NotEmpty], 210 | * [Range], [Matches] or [OnlyNumbers]. If you want to build 211 | * a custom rule, see [ValidationRule]. 212 | * 213 | */ 214 | add(String field, ValidationRule rule); 215 | 216 | /** 217 | * Adds all rules from [validator] to this Validator. 218 | */ 219 | addAll(Validator validator); 220 | 221 | /** 222 | * Validate [obj]. 223 | * 224 | * If an error is found, returns a [ValidationError], 225 | * otherwise returns null. 226 | */ 227 | ValidationError execute(Object obj); 228 | 229 | } 230 | 231 | ///An error produced by a [Validator]. 232 | class ValidationError { 233 | 234 | /** 235 | * A Map of fields that failed the validation test. 236 | * 237 | * For each field, this map provides the List of rules 238 | * that the field couldn't match. 239 | */ 240 | @Field() 241 | Map> invalidFields; 242 | 243 | ValidationError([this.invalidFields]) { 244 | if (invalidFields == null) { 245 | invalidFields = {}; 246 | } 247 | } 248 | 249 | String toString() => "invalidFields: $invalidFields"; 250 | 251 | /** 252 | * Add all invalid fields from [error] to this 253 | * [ValidationError]. 254 | */ 255 | join(ValidationError error) { 256 | if (error != null) { 257 | error.invalidFields.forEach((key, value) { 258 | var fieldErrors = invalidFields[key]; 259 | if (fieldErrors == null) { 260 | fieldErrors = []; 261 | invalidFields[key] = fieldErrors; 262 | } 263 | fieldErrors.addAll(value); 264 | }); 265 | } 266 | } 267 | } 268 | 269 | ///A codec to convert objects of an specific type. 270 | class TypeCodec extends Codec { 271 | 272 | final Type type; 273 | 274 | _TypeDecoder _decoder; 275 | _TypeEncoder _encoder; 276 | 277 | TypeCodec(this.type, {FieldDecoder fieldDecoder, 278 | FieldEncoder fieldEncoder, 279 | Map typeCodecs: const {}}) { 280 | fieldDecoder = fieldDecoder != null ? 281 | fieldDecoder : _defaultFieldDecoder; 282 | fieldEncoder = fieldEncoder != null ? 283 | fieldEncoder : _defaultFieldEncoder; 284 | 285 | _decoder = new _TypeDecoder(fieldDecoder, 286 | type: type, typeCodecs: typeCodecs); 287 | _encoder = new _TypeEncoder(fieldEncoder, 288 | type: type, typeCodecs: typeCodecs); 289 | } 290 | 291 | @override 292 | Converter get decoder => _decoder; 293 | 294 | @override 295 | Converter get encoder => _encoder; 296 | 297 | } 298 | 299 | ///A codec that can convert objects of any type. 300 | class GenericTypeCodec { 301 | 302 | _TypeDecoder _decoder; 303 | _TypeEncoder _encoder; 304 | 305 | GenericTypeCodec({FieldDecoder fieldDecoder, FieldEncoder fieldEncoder, 306 | Map typeCodecs: const {}}) { 307 | fieldDecoder = fieldDecoder != null ? 308 | fieldDecoder : _defaultFieldDecoder; 309 | fieldEncoder = fieldEncoder != null ? 310 | fieldEncoder : _defaultFieldEncoder; 311 | 312 | _decoder = new _TypeDecoder(fieldDecoder, typeCodecs: typeCodecs); 313 | _encoder = new _TypeEncoder(fieldEncoder, typeCodecs: typeCodecs); 314 | } 315 | 316 | dynamic encode(dynamic input, [Type type]) { 317 | return _encoder.convert(input, type); 318 | } 319 | 320 | dynamic decode(dynamic data, Type type) { 321 | return _decoder.convert(data, type); 322 | } 323 | 324 | } 325 | 326 | class IgnoreValue { 327 | const IgnoreValue(); 328 | } 329 | 330 | const ignoreValue = const IgnoreValue(); 331 | 332 | /** 333 | * A [FieldDecoder] is a function which can extract field 334 | * values from an encoded data. 335 | */ 336 | typedef Object FieldDecoder(Object encodedData, String fieldName, 337 | Field fieldInfo, List metadata); 338 | 339 | /** 340 | * A [FieldEncoder] is a function which can add fields to 341 | * an encoded data. 342 | */ 343 | typedef void FieldEncoder(Map encodedData, String fieldName, 344 | Field fieldInfo, List metadata, Object value); 345 | 346 | 347 | /** 348 | * The main mapper class, used by codecs to transform 349 | * objects. 350 | * 351 | * Currently, there are two implementations of Mapper. 352 | * The first one uses the mirrors API, and is used when 353 | * the application runs on the dartvm. The second one uses 354 | * static data generated by the redstone_mapper's transformer, 355 | * and is used when the application is compiled to javascript. 356 | * 357 | */ 358 | abstract class Mapper { 359 | 360 | MapperDecoder get decoder; 361 | 362 | MapperEncoder get encoder; 363 | 364 | } 365 | 366 | ///decode [data] to one or more objects of type [type], using [fieldDecoder] 367 | ///and [typeCodecs] to extract field values. 368 | typedef dynamic MapperDecoder(Object data, FieldDecoder fieldDecoder, 369 | Map typeCodecs, [Type type]); 370 | 371 | ///encode [obj] using [fieldEncoder] and [typeCodecs] to encode field values. 372 | typedef Map MapperEncoder(Object obj, FieldEncoder fieldEncoder, 373 | Map typeCodecs); 374 | 375 | /** 376 | * An exception generated when an object can't be encoded 377 | * or decoded. 378 | */ 379 | class MapperException implements Exception { 380 | 381 | String message; 382 | 383 | Queue _stack = new Queue(); 384 | 385 | MapperException(String this.message); 386 | 387 | void append(StackElement element) { 388 | _stack.addFirst(element); 389 | } 390 | 391 | String _printStack() { 392 | if (_stack.isEmpty) { 393 | return ""; 394 | } 395 | var stack = new StringBuffer(_stack.first.name); 396 | _stack.skip(1).forEach((e) { 397 | if (e.isType) { 398 | stack.write("(${e.name})"); 399 | } else { 400 | stack.write("#${e.name}"); 401 | } 402 | }); 403 | stack.write(":"); 404 | return stack.toString(); 405 | } 406 | 407 | String toString() => "MapperException: ${_printStack()} $message"; 408 | 409 | } 410 | 411 | 412 | class StackElement { 413 | 414 | final bool isType; 415 | final String name; 416 | 417 | StackElement(this.isType, this.name); 418 | 419 | } 420 | 421 | typedef Mapper MapperFactory(Type type); 422 | typedef Validator ValidatorFactory([Type type, bool parseAnnotations]); 423 | 424 | MapperFactory _mapperFactory = (Type type) => 425 | throw new UnsupportedError( 426 | "redstone_mapper is not properly configured. Did you call bootstrapMapper()?"); 427 | ValidatorFactory _validatorFactory = ([Type type, bool parseAnnotations]) => 428 | throw new UnsupportedError( 429 | "redstone_mapper is not properly configured. Did you call bootstrapMapper()?"); 430 | -------------------------------------------------------------------------------- /lib/mapper_factory.dart: -------------------------------------------------------------------------------- 1 | library redstone_mapper_factory; 2 | 3 | import 'dart:convert'; 4 | import 'dart:mirrors'; 5 | 6 | import 'package:redstone_mapper/mapper.dart'; 7 | 8 | /** 9 | * Initialize the mapper system. 10 | * 11 | * It's necessary to call this method before encoding or decoding 12 | * any object. When using redstone_mapper on the client side, it's 13 | * necessary to call this method from the `main()` function, so during 14 | * `pub build`, it can be replaced by `staticBootstrapMapper()`, which uses 15 | * static data generated by the redstone_mapper's tranformer to encode and 16 | * decode objects. 17 | * 18 | * main() { 19 | * ... 20 | * bootstrapMapper(); 21 | * ... 22 | * } 23 | * 24 | * Also, be sure to set the redstone_mapper's transformer in your 25 | * `pubspec.yaml` file. 26 | * 27 | * On the server side, if you are using redstone_mapper as a 28 | * Redstone.dart plugin, then this function will be called for 29 | * you. 30 | */ 31 | void bootstrapMapper() { 32 | configure(_getOrCreateMapper, _createValidator); 33 | } 34 | 35 | 36 | class _DynamicMapper implements Mapper { 37 | 38 | final MapperDecoder decoder; 39 | final MapperEncoder encoder; 40 | 41 | final Map _fields; 42 | 43 | final bool isCollection; 44 | 45 | bool get isEncodable => _fields.isNotEmpty; 46 | 47 | _DynamicMapper(this.decoder, this.encoder, this._fields) : 48 | isCollection = false; 49 | 50 | const _DynamicMapper.notEncodable() : 51 | decoder = const _DefaultDecoder(), 52 | encoder = const _DefaultEncoder(), 53 | _fields = const {}, 54 | isCollection = false; 55 | 56 | const _DynamicMapper.map() : 57 | decoder = const _MapDecoder(), 58 | encoder = const _MapEncoder(), 59 | _fields = const {}, 60 | isCollection = true; 61 | 62 | const _DynamicMapper.list() : 63 | decoder = const _ListDecoder(), 64 | encoder = const _ListEncoder(), 65 | _fields = const {}, 66 | isCollection = true; 67 | } 68 | 69 | class _FieldData { 70 | 71 | final Symbol symbol; 72 | final List metadata; 73 | 74 | _FieldData(this.symbol, this.metadata); 75 | 76 | } 77 | 78 | class _DynamicValidator implements Validator { 79 | 80 | final Type _type; 81 | 82 | Map _fields; 83 | Map> _rules = {}; 84 | 85 | _DynamicValidator([Type this._type]) { 86 | if (_type != null) { 87 | _fields = _getOrCreateMapper(_type)._fields; 88 | } 89 | } 90 | 91 | @override 92 | addAll(Validator validator) { 93 | (validator as _DynamicValidator)._rules.forEach((fieldName, rules) { 94 | rules.forEach((r) => add(fieldName, r)); 95 | }); 96 | } 97 | 98 | @override 99 | add(String field, ValidationRule rule) { 100 | if (_fields != null && !_fields.containsKey(field)) { 101 | throw new ValidationException("$_type has no field '$field'"); 102 | } 103 | var fieldRules = _rules[field]; 104 | if (fieldRules == null) { 105 | fieldRules = []; 106 | _rules[field] = fieldRules; 107 | } 108 | fieldRules.add(rule); 109 | } 110 | 111 | @override 112 | ValidationError execute(Object obj) { 113 | if (obj is Map) { 114 | return _validateMap(obj); 115 | } else { 116 | if (_type == null) { 117 | throw new ArgumentError("This validator can only validate maps"); 118 | } 119 | return _validateObj(obj); 120 | } 121 | } 122 | 123 | ValidationError _validateObj(obj) { 124 | var mirror = reflect(obj); 125 | 126 | var invalidFields = {}; 127 | _rules.forEach((field, rules) { 128 | rules.forEach((rule) { 129 | if (!rule.validate(mirror.getField(_fields[field].symbol).reflectee)) { 130 | List errors = invalidFields[field]; 131 | if (errors == null) { 132 | errors = []; 133 | invalidFields[field] = errors; 134 | } 135 | errors.add(rule.type); 136 | } 137 | }); 138 | }); 139 | 140 | if (invalidFields.isNotEmpty) { 141 | return new ValidationError(invalidFields); 142 | } 143 | 144 | return null; 145 | } 146 | 147 | ValidationError _validateMap(Map map) { 148 | var invalidFields = {}; 149 | 150 | _rules.forEach((field, rules) { 151 | rules.forEach((rule) { 152 | if (!rule.validate(map[field])) { 153 | List errors = invalidFields[field]; 154 | if (errors == null) { 155 | errors = []; 156 | invalidFields[field] = errors; 157 | } 158 | errors.add(rule.type); 159 | } 160 | }); 161 | }); 162 | 163 | if (invalidFields.isNotEmpty) { 164 | return new ValidationError(invalidFields); 165 | } 166 | 167 | return null; 168 | } 169 | 170 | } 171 | 172 | final Map _cache = { 173 | String: const _DynamicMapper.notEncodable(), 174 | int: const _DynamicMapper.notEncodable(), 175 | double: const _DynamicMapper.notEncodable(), 176 | num: const _DynamicMapper.notEncodable(), 177 | bool: const _DynamicMapper.notEncodable(), 178 | Object: const _DynamicMapper.notEncodable(), 179 | Null: const _DynamicMapper.notEncodable() 180 | }; 181 | 182 | class _DefaultDecoder { 183 | 184 | const _DefaultDecoder(); 185 | 186 | call(Object data, FieldDecoder fieldDecoder, 187 | Map typeCodecs, [Type type]) { 188 | return data; 189 | } 190 | 191 | } 192 | 193 | class _DefaultEncoder { 194 | 195 | const _DefaultEncoder(); 196 | 197 | call(Object obj, FieldEncoder fieldEncoder, 198 | Map typeCodecs) { 199 | return obj; 200 | } 201 | 202 | } 203 | 204 | class _MapDecoder { 205 | 206 | const _MapDecoder(); 207 | 208 | call(Object data, FieldDecoder fieldDecoder, 209 | Map typeCodecs, [Type type]) { 210 | if (data is! Map) { 211 | throw new MapperException("Expecting ${type}, found ${data.runtimeType}"); 212 | } 213 | 214 | TypeMirror clazz = reflectType(type); 215 | if (clazz.isOriginalDeclaration) { 216 | return data; 217 | } 218 | 219 | var decoded = {}; 220 | var valueType = clazz.typeArguments[1].reflectedType; 221 | var mapper = _getOrCreateMapper(valueType); 222 | (data as Map).forEach((key, value) { 223 | decoded[key] = mapper.decoder(value, fieldDecoder, 224 | typeCodecs, valueType); 225 | }); 226 | return decoded; 227 | } 228 | 229 | } 230 | 231 | class _MapEncoder { 232 | 233 | const _MapEncoder(); 234 | 235 | call(Object data, FieldEncoder fieldEncoder, 236 | Map typeCodecs) { 237 | if (data is Map) { 238 | var encoded = {}; 239 | data.forEach((key, value) { 240 | var mapper = _getOrCreateMapper(value.runtimeType); 241 | encoded[key] = mapper.encoder(value, fieldEncoder, typeCodecs); 242 | }); 243 | return encoded; 244 | } 245 | return data; 246 | } 247 | 248 | } 249 | 250 | class _ListDecoder { 251 | 252 | const _ListDecoder(); 253 | 254 | call(Object data, FieldDecoder fieldDecoder, 255 | Map typeCodecs, [Type type]) { 256 | TypeMirror clazz = reflectType(type); 257 | if (data is! List) { 258 | if (clazz.isOriginalDeclaration) { 259 | throw new MapperException("Expecting List<${type}>, found ${data.runtimeType}"); 260 | } else { 261 | throw new MapperException("Expecting ${type}, found ${data.runtimeType}"); 262 | } 263 | } 264 | 265 | var valueType; 266 | if (!clazz.isOriginalDeclaration) { 267 | valueType = clazz.typeArguments[0].reflectedType; 268 | } else { 269 | if (clazz.isSubtypeOf(_listMirror)) { 270 | return const _DynamicMapper.notEncodable().decoder(data, 271 | fieldDecoder, typeCodecs); 272 | } 273 | valueType = type; 274 | } 275 | 276 | var mapper = _getOrCreateMapper(valueType); 277 | return new List.from((data as List).map((value) => 278 | mapper.decoder(value, fieldDecoder, typeCodecs, valueType))); 279 | } 280 | 281 | } 282 | 283 | class _ListEncoder { 284 | 285 | const _ListEncoder(); 286 | 287 | call(Object data, FieldEncoder fieldEncoder, Map typeCodecs) { 288 | if (data is List) { 289 | return new List.from(data.map((value) { 290 | var mapper = _getOrCreateMapper(value.runtimeType); 291 | return mapper.encoder(value, fieldEncoder, typeCodecs); 292 | })); 293 | } 294 | return data; 295 | } 296 | 297 | } 298 | 299 | final _mapMirror = reflectClass(Map); 300 | final _listMirror = reflectClass(List); 301 | 302 | _DynamicMapper _getOrCreateMapper(Type type) { 303 | var mapper = _cache[type]; 304 | if (mapper == null) { 305 | var clazz = reflectClass(type); 306 | bool isMap = clazz == _mapMirror || clazz.isSubclassOf(_mapMirror); 307 | bool isList = clazz == _listMirror || clazz.isSubclassOf(_listMirror); 308 | if (!isMap && !isList) { 309 | clazz.superinterfaces.forEach((i) { 310 | var d = i.originalDeclaration; 311 | if (d == _mapMirror) { 312 | isMap = true; 313 | } else if (d == _listMirror) { 314 | isList = true; 315 | } 316 | }); 317 | } 318 | 319 | 320 | if (isMap) { 321 | mapper = const _DynamicMapper.map(); 322 | _cache[type] = mapper; 323 | return mapper; 324 | } else if(isList) { 325 | mapper = const _DynamicMapper.list(); 326 | _cache[type] = mapper; 327 | return mapper; 328 | } 329 | 330 | var decodeChain = {}; 331 | var encodeChain = {}; 332 | var fields = {}; 333 | 334 | _buildChain(clazz, decodeChain, encodeChain, fields); 335 | 336 | if (fields.isEmpty) { 337 | 338 | mapper = const _DynamicMapper.notEncodable(); 339 | 340 | } else { 341 | 342 | var decoder = (data, fieldDecoder, typeCodecs, [fieldType]) { 343 | if (data == null) { 344 | return null; 345 | } 346 | if (data is List) { 347 | return const _DynamicMapper.list().decoder(data, fieldDecoder, 348 | typeCodecs, type); 349 | } 350 | var mirror; 351 | try { 352 | mirror = clazz.newInstance(const Symbol(""), const []); 353 | } catch(e) { 354 | throw new MapperException( 355 | "Can't create an instance of $type. Does $type have a default constructor? Cause: $e") 356 | ..append(new StackElement(true, type.toString())); 357 | } 358 | 359 | try { 360 | decodeChain.values.forEach((f) => f(data, mirror, fieldDecoder, typeCodecs)); 361 | } on MapperException catch(e) { 362 | throw e..append(new StackElement(true, type.toString())); 363 | } catch(e) { 364 | throw new MapperException("Can't decode $type: $e") 365 | ..append(new StackElement(true, type.toString())); 366 | } 367 | return mirror.reflectee; 368 | }; 369 | var encoder = (obj, fieldEncoder, typeCodecs) { 370 | if (obj == null) { 371 | return null; 372 | } 373 | var data = {}; 374 | try { 375 | var mirror = reflect(obj); 376 | encodeChain.values.forEach((f) => f(data, mirror, fieldEncoder, typeCodecs)); 377 | } on MapperException catch(e) { 378 | throw e..append(new StackElement(true, type.toString())); 379 | } catch(e) { 380 | throw new MapperException("Can't encode $obj: $e") 381 | ..append(new StackElement(true, type.toString())); 382 | } 383 | return data; 384 | }; 385 | 386 | mapper = new _DynamicMapper(decoder, encoder, fields); 387 | 388 | } 389 | _cache[type] = mapper; 390 | } 391 | return mapper; 392 | } 393 | 394 | void _buildChain(ClassMirror clazz, Map decodeChain, Map encodeChain, 395 | Map fields) { 396 | 397 | if(clazz.superclass != null && clazz.superclass.reflectedType != Object) { 398 | _buildChain(clazz.superclass, decodeChain, encodeChain, fields); 399 | } 400 | 401 | clazz.superinterfaces.forEach((interface) { 402 | _buildChain(interface, decodeChain, encodeChain, fields); 403 | }); 404 | 405 | clazz.declarations.forEach((name, mirror) { 406 | if (mirror is VariableMirror && !mirror.isStatic && !mirror.isPrivate) { 407 | var metadata = mirror.metadata.map((m) => m.reflectee).toList(growable: false); 408 | var fieldInfo = metadata. 409 | firstWhere((o) => o is Field, orElse: () => null) as Field; 410 | 411 | if (fieldInfo != null) { 412 | var fieldName = MirrorSystem.getName(mirror.simpleName); 413 | fields[fieldName] = new _FieldData(mirror.simpleName, metadata); 414 | 415 | _encodeField(fieldName, fieldInfo, metadata, 416 | encodeChain, mirror, mirror.type); 417 | 418 | if (!mirror.isFinal) { 419 | _decodeField(fieldName, fieldInfo, metadata, 420 | decodeChain, mirror, mirror.type); 421 | } 422 | } 423 | } else if (mirror is MethodMirror && 424 | !mirror.isStatic && !mirror.isPrivate) { 425 | 426 | var metadata = mirror.metadata.map((m) => m.reflectee).toList(growable: false); 427 | var fieldInfo = metadata. 428 | firstWhere((o) => o is Field, orElse: () => null) as Field; 429 | 430 | if (fieldInfo != null) { 431 | var fieldName = MirrorSystem.getName(mirror.simpleName); 432 | if (mirror.isGetter) { 433 | fields[fieldName] = new _FieldData(mirror.simpleName, metadata); 434 | 435 | TypeMirror fieldType = mirror.returnType; 436 | _encodeField(fieldName, fieldInfo, metadata, 437 | encodeChain, mirror, fieldType); 438 | 439 | } else if (mirror.isSetter) { 440 | fieldName = fieldName.substring(0, fieldName.length - 1); 441 | 442 | TypeMirror fieldType = mirror.parameters[0].type; 443 | _decodeField(fieldName, fieldInfo, metadata, 444 | decodeChain, mirror, fieldType); 445 | } 446 | } 447 | } 448 | }); 449 | } 450 | 451 | void _decodeField(String fieldName, Field fieldInfo, List metadata, 452 | Map decodeChain, DeclarationMirror mirror, TypeMirror fieldType) { 453 | 454 | var type = fieldType.reflectedType; 455 | var name = new Symbol(fieldName); 456 | 457 | decodeChain[fieldName] = ((data, InstanceMirror obj, FieldDecoder fieldDecoder, 458 | Map typeCodecs) { 459 | _DynamicMapper mapper = _getOrCreateMapper(type); 460 | try { 461 | var value = fieldDecoder(data, fieldName, fieldInfo, metadata); 462 | if (value != null && value is! IgnoreValue) { 463 | var typeCodec = typeCodecs[type]; 464 | if (typeCodec != null) { 465 | value = typeCodec.decode(value); 466 | } 467 | value = mapper.decoder(value, fieldDecoder, typeCodecs, type); 468 | obj.setField(name, value); 469 | } 470 | } on MapperException catch(e) { 471 | throw e..append(new StackElement(false, fieldName)); 472 | } catch(e) { 473 | throw new MapperException("$e")..append(new StackElement(false, fieldName)); 474 | } 475 | }); 476 | 477 | } 478 | 479 | void _encodeField(String fieldName, Field fieldInfo, List metadata, 480 | Map encodeChain, DeclarationMirror mirror, TypeMirror fieldType) { 481 | 482 | var type = fieldType.reflectedType; 483 | encodeChain[fieldName] = ((Map data, InstanceMirror obj, FieldEncoder fieldEncoder, 484 | Map typeCodecs) { 485 | var value = obj.getField(mirror.simpleName).reflectee; 486 | if (value != null) { 487 | var mapper = _getOrCreateMapper(type); 488 | value = mapper.encoder(value, fieldEncoder, typeCodecs); 489 | 490 | var typeCodec = typeCodecs[type]; 491 | value = typeCodec != null ? typeCodec.encode(value) : value; 492 | } 493 | 494 | try { 495 | fieldEncoder(data, fieldName, fieldInfo, metadata, value); 496 | } on MapperException catch(e) { 497 | throw e..append(new StackElement(false, fieldName)); 498 | } catch(e) { 499 | throw new MapperException("$e")..append(new StackElement(false, fieldName)); 500 | } 501 | }); 502 | 503 | } 504 | 505 | _DynamicValidator _createValidator([Type type, bool parseAnnotations = false]) { 506 | var validator = new _DynamicValidator(type); 507 | if (parseAnnotations) { 508 | validator._fields.forEach((fieldName, fieldData) { 509 | fieldData.metadata.where((m) => m is ValidationRule).forEach((r) => 510 | validator.add(fieldName, r)); 511 | }); 512 | } 513 | return validator; 514 | } 515 | 516 | -------------------------------------------------------------------------------- /lib/transformer.dart: -------------------------------------------------------------------------------- 1 | library redstone_mapper_transformer; 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:analyzer/src/generated/ast.dart'; 6 | import 'package:analyzer/src/generated/element.dart'; 7 | import 'package:code_transformers/resolver.dart'; 8 | import 'package:barback/barback.dart'; 9 | import 'package:source_maps/refactor.dart' show TextEditTransaction; 10 | import 'package:path/path.dart' as path; 11 | 12 | /** 13 | * The redstone_mapper transformer, which replaces the default 14 | * mapper implementation, that relies on the mirrors API, by a 15 | * static implementation, that uses data extracted at compile 16 | * time. 17 | * 18 | */ 19 | class StaticMapperGenerator extends Transformer with ResolverTransformer { 20 | ClassElement objectType; 21 | 22 | _CollectionType collectionType; 23 | ClassElement fieldAnnotationClass; 24 | 25 | final _UsedLibs usedLibs = new _UsedLibs(); 26 | final Map types = {}; 27 | 28 | String _mapperLibPrefix; 29 | 30 | StaticMapperGenerator.asPlugin(BarbackSettings settings) { 31 | var sdkDir = settings.configuration["dart_sdk"]; 32 | if (sdkDir == null) { 33 | // Assume the Pub executable is always coming from the SDK. 34 | sdkDir = path.dirname(path.dirname(Platform.executable)); 35 | } 36 | resolvers = new Resolvers(sdkDir); 37 | } 38 | 39 | @override 40 | applyResolver(Transform transform, Resolver resolver) { 41 | fieldAnnotationClass = resolver.getType("redstone_mapper.Field"); 42 | 43 | if (fieldAnnotationClass == null) { 44 | //mapper is not being used 45 | transform.addOutput(transform.primaryInput); 46 | return; 47 | } 48 | 49 | var dynamicApp = 50 | resolver.getLibraryFunction('redstone_mapper_factory.bootstrapMapper'); 51 | if (dynamicApp == null) { 52 | // No dynamic mapper imports, exit. 53 | transform.addOutput(transform.primaryInput); 54 | return; 55 | } 56 | 57 | objectType = resolver.getType("dart.core.Object"); 58 | collectionType = new _CollectionType(resolver); 59 | _mapperLibPrefix = 60 | usedLibs.resolveLib(resolver.getLibraryByName("redstone_mapper")); 61 | 62 | resolver.libraries 63 | .expand((lib) => lib.units) 64 | .expand((unit) => unit.types) 65 | .forEach((ClassElement clazz) => _scannClass(clazz)); 66 | 67 | var id = transform.primaryInput.id; 68 | var outputFilename = "${path.url.basenameWithoutExtension(id.path)}" 69 | "_static_mapper.dart"; 70 | var outputPath = path.url.join(path.url.dirname(id.path), outputFilename); 71 | var generatedAssetId = new AssetId(id.package, outputPath); 72 | 73 | String typesSource = types.toString(); 74 | 75 | StringBuffer source = new StringBuffer(); 76 | _writeHeader(transform.primaryInput.id, source); 77 | usedLibs.libs.forEach((lib) { 78 | if (lib.isDartCore) return; 79 | var uri = resolver.getImportUri(lib, from: generatedAssetId); 80 | source.write("import '$uri' as ${usedLibs.prefixes[lib]};\n"); 81 | }); 82 | _writePreamble(source); 83 | source.write(typesSource); 84 | _writeFooter(source); 85 | 86 | transform 87 | .addOutput(new Asset.fromString(generatedAssetId, source.toString())); 88 | 89 | var lib = resolver.getLibrary(id); 90 | var transaction = resolver.createTextEditTransaction(lib); 91 | var unit = lib.definingCompilationUnit.computeNode(); 92 | 93 | for (var directive in unit.directives) { 94 | if (directive is ImportDirective && 95 | directive.uri.stringValue == 96 | 'package:redstone_mapper/mapper_factory.dart') { 97 | var uri = directive.uri; 98 | transaction.edit(uri.beginToken.offset, uri.end, 99 | '\'package:redstone_mapper/mapper_factory_static.dart\''); 100 | } 101 | } 102 | 103 | var dynamicToStatic = 104 | new _MapperDynamicToStaticVisitor(dynamicApp, transaction); 105 | unit.accept(dynamicToStatic); 106 | 107 | _addImport(transaction, unit, outputFilename, 'generated_static_mapper'); 108 | 109 | var printer = transaction.commit(); 110 | var url = id.path.startsWith('lib/') 111 | ? 'package:${id.package}/${id.path.substring(4)}' 112 | : id.path; 113 | printer.build(url); 114 | transform.addOutput(new Asset.fromString(id, printer.text)); 115 | } 116 | 117 | void _writeHeader(AssetId id, StringBuffer source) { 118 | var libName = path.withoutExtension(id.path).replaceAll('/', '.'); 119 | libName = libName.replaceAll('-', '_'); 120 | source.write("library ${id.package}.$libName.generated_static_mapper;\n"); 121 | source.write("import 'package:redstone_mapper/mapper.dart';\n"); 122 | source.write( 123 | "import 'package:redstone_mapper/mapper_factory_static.dart';\n\n"); 124 | } 125 | 126 | void _writePreamble(StringBuffer source) { 127 | var defaultField = "const $_mapperLibPrefix.Field()"; 128 | 129 | source.write( 130 | "_encodeField(data, fieldName, mapper, value, fieldEncoder, typeCodecs, type, \n"); 131 | source.write( 132 | " [fieldInfo = $defaultField, metadata = const [$defaultField]]) {\n"); 133 | source.write(" if (value != null) {\n"); 134 | source.write( 135 | " value = mapper.encoder(value, fieldEncoder, typeCodecs);\n"); 136 | source.write(" var typeCodec = typeCodecs[type];\n"); 137 | source.write( 138 | " value = typeCodec != null ? typeCodec.encode(value) : value;\n"); 139 | source.write(" }\n"); 140 | source.write(" fieldEncoder(data, fieldName, fieldInfo, metadata,\n"); 141 | source.write(" value);\n"); 142 | source.write("}\n\n"); 143 | 144 | source.write( 145 | "_decodeField(data, fieldName, mapper, fieldDecoder, typeCodecs, type, \n"); 146 | source.write( 147 | " [fieldInfo = $defaultField, metadata = const [$defaultField]]) {\n"); 148 | source.write( 149 | " var value = fieldDecoder(data, fieldName, fieldInfo, metadata);\n"); 150 | source.write(" if (value != null) {\n"); 151 | source.write(" var typeCodec = typeCodecs[type];\n"); 152 | source.write( 153 | " value = typeCodec != null ? typeCodec.decode(value) : value;\n"); 154 | source.write(" return mapper.decoder(value, fieldDecoder, typeCodecs);"); 155 | source.write(" }\n"); 156 | source.write(" return null;\n"); 157 | source.write("}\n\n"); 158 | 159 | source.write("final Map types = "); 160 | } 161 | 162 | void _writeFooter(StringBuffer source) { 163 | source.write(";"); 164 | } 165 | 166 | /// Injects an import into the list of imports in the file. 167 | void _addImport(TextEditTransaction transaction, CompilationUnit unit, 168 | String uri, String prefix) { 169 | var last = unit.directives.where((d) => d is ImportDirective).last; 170 | transaction.edit(last.end, last.end, '\nimport \'$uri\' as $prefix;'); 171 | } 172 | 173 | dynamic _scannClass(ClassElement clazz, 174 | [List<_FieldInfo> fields, 175 | Set cache, 176 | Map fieldIdxs, 177 | Map accessorIdxs]) { 178 | bool rootType = false; 179 | if (fields == null) { 180 | rootType = true; 181 | fields = []; 182 | } 183 | if (cache == null) { 184 | cache = new Set(); 185 | } 186 | if (fieldIdxs == null) { 187 | fieldIdxs = {}; 188 | } 189 | if (accessorIdxs == null) { 190 | accessorIdxs = {}; 191 | } 192 | 193 | cache.add(clazz); 194 | 195 | if (clazz.supertype != null && 196 | clazz.supertype.element != objectType && 197 | !cache.contains(clazz.supertype.element)) { 198 | _scannClass( 199 | clazz.supertype.element, fields, cache, fieldIdxs, accessorIdxs); 200 | } 201 | 202 | clazz.interfaces.where((i) => !cache.contains(i.element)).forEach((i) { 203 | _scannClass(i.element, fields, cache, fieldIdxs, accessorIdxs); 204 | }); 205 | 206 | clazz.fields 207 | .where((f) => !f.isStatic && !f.isPrivate) 208 | .forEach((f) => _scannField(fields, f, fieldIdxs)); 209 | 210 | clazz.accessors 211 | .where((p) => !p.isStatic && !p.isPrivate) 212 | .forEach((p) => _scannAccessor(fields, p, accessorIdxs)); 213 | 214 | if (rootType) { 215 | if (fields.isNotEmpty) { 216 | var key = usedLibs.resolveLib(clazz.library); 217 | if (key.isNotEmpty) { 218 | key = "$key.${clazz.displayName}"; 219 | } else { 220 | key = "${clazz.displayName}"; 221 | } 222 | types[key] = 223 | new _TypeCodecGenerator(collectionType, usedLibs, key, fields); 224 | } 225 | return null; 226 | } 227 | return fields; 228 | } 229 | 230 | List _extractArgs(String source, String name) { 231 | source = source.substring(0, source.lastIndexOf(new RegExp("\\s$name"))); 232 | 233 | var idx = source.lastIndexOf(new RegExp("[@\)]")); 234 | if (idx == -1) { 235 | return []; 236 | } 237 | 238 | var char = source[idx]; 239 | if (char == ")") { 240 | source = source.substring(0, idx + 1); 241 | } else { 242 | idx = source.indexOf("\s", idx); 243 | source = source.substring(0, idx); 244 | } 245 | 246 | return source.split(new RegExp(r"\s@")).map((m) { 247 | if (m[m.length - 1] == ")") { 248 | return m.substring(m.indexOf("(")); 249 | } 250 | return m; 251 | }).toList(growable: false); 252 | } 253 | 254 | bool _isFieldConstructor(ElementAnnotation m) => 255 | m.element is ConstructorElement && 256 | (m.element.enclosingElement == fieldAnnotationClass || 257 | (m.element.enclosingElement as ClassElement) 258 | .allSupertypes 259 | .map((i) => i.element) 260 | .contains(fieldAnnotationClass)); 261 | 262 | _FieldMetadata _buildMetadata(Element element) { 263 | String source; 264 | if (element is FieldElement) { 265 | source = element.computeNode().parent.parent.toSource(); 266 | } else { 267 | source = element.computeNode().toSource(); 268 | } 269 | 270 | List args = _extractArgs(source, element.displayName); 271 | 272 | //For fields with default configuration, don't generate metadata code 273 | if (args.length == 1 && args[0] == "()" && element.metadata.length == 1) { 274 | return null; 275 | } 276 | 277 | String fieldExp; 278 | List exps = []; 279 | 280 | int idx = 0; 281 | for (ElementAnnotation m in element.metadata) { 282 | var prefix = usedLibs.resolveLib(m.element.library); 283 | if (prefix.isNotEmpty) { 284 | prefix += "."; 285 | } 286 | 287 | if (m.element is ConstructorElement) { 288 | var className = m.element.enclosingElement.displayName; 289 | var constructor = m.element.displayName; 290 | if (constructor.isNotEmpty) { 291 | constructor = ".$constructor"; 292 | } 293 | var exp = "const $prefix$className$constructor${args[idx]}"; 294 | exps.add(exp); 295 | if (fieldExp == null && _isFieldConstructor(m)) { 296 | fieldExp = exp; 297 | } 298 | } else { 299 | exps.add("$prefix${args[idx]}"); 300 | } 301 | 302 | idx++; 303 | } 304 | 305 | return new _FieldMetadata(fieldExp, exps); 306 | } 307 | 308 | void _scannField(List<_FieldInfo> fields, FieldElement element, 309 | Map fieldIdxs) { 310 | var field = element.metadata 311 | .firstWhere((f) => _isFieldConstructor(f), orElse: () => null); 312 | if (field != null) { 313 | var idx = fieldIdxs[element.displayName]; 314 | if (idx != null) { 315 | fields.removeAt(idx); 316 | } 317 | 318 | var metadata = _buildMetadata(element); 319 | fields.add(new _FieldInfo(element.displayName, element.type, metadata, 320 | canDecode: !element.isFinal)); 321 | 322 | fieldIdxs[element.displayName] = fields.length - 1; 323 | } 324 | } 325 | 326 | void _scannAccessor(List<_FieldInfo> fields, PropertyAccessorElement element, 327 | Map accessorIdxs) { 328 | var field = element.metadata 329 | .firstWhere((f) => _isFieldConstructor(f), orElse: () => null); 330 | if (field != null) { 331 | var metadata = _buildMetadata(element); 332 | var name = element.displayName; 333 | var type; 334 | var idx; 335 | if (element.isSetter) { 336 | name = name.substring(0, name.length - 1); 337 | 338 | idx = accessorIdxs[name]; 339 | if (idx != null) { 340 | fields.removeAt(idx); 341 | } 342 | 343 | type = element.type.normalParameterTypes[0]; 344 | } else { 345 | idx = accessorIdxs[name]; 346 | if (idx != null) { 347 | fields.removeAt(idx); 348 | } 349 | 350 | type = element.returnType; 351 | } 352 | fields.add(new _FieldInfo(element.displayName, type, metadata, 353 | canDecode: element.isSetter, canEncode: element.isGetter)); 354 | 355 | accessorIdxs[name] = fields.length - 1; 356 | } 357 | } 358 | } 359 | 360 | class _TypeCodecGenerator { 361 | final _UsedLibs usedLibs; 362 | final _CollectionType collectionType; 363 | 364 | final String className; 365 | final List<_FieldInfo> fields; 366 | 367 | _TypeCodecGenerator( 368 | this.collectionType, this.usedLibs, this.className, this.fields); 369 | 370 | String toString() { 371 | var source = new StringBuffer("new TypeInfo("); 372 | 373 | _buildEncoder(source); 374 | source.write(", "); 375 | _buildDecoder(source); 376 | source.write(", "); 377 | _buildFields(source); 378 | source.write(")"); 379 | 380 | return source.toString(); 381 | } 382 | 383 | void _buildEncoder(StringBuffer source) { 384 | source.write("(obj, factory, fieldEncoder, typeCodecs) {\n"); 385 | source.write(" var data = {};\n"); 386 | 387 | fields.where((f) => f.canEncode).forEach((f) { 388 | var typeName = _getTypeName(f.type); 389 | 390 | source.write(" _encodeField(data, '${f.name}', "); 391 | _buildMapper(source, f.type, typeName); 392 | source.write(", obj.${f.name}, fieldEncoder, typeCodecs, $typeName"); 393 | 394 | if (f.metadata != null) { 395 | var fieldExp = f.metadata.fieldExp; 396 | var exps = f.metadata.exps; 397 | source.write(", $fieldExp, $exps"); 398 | } 399 | 400 | source.write(");\n"); 401 | }); 402 | 403 | source.write(" return data;\n }"); 404 | } 405 | 406 | void _buildDecoder(StringBuffer source) { 407 | source.write("(data, factory, fieldDecoder, typeCodecs) {\n"); 408 | source.write(" var obj = new ${className}();\n"); 409 | source.write(" var value;\n"); 410 | 411 | fields.where((f) => f.canDecode).forEach((f) { 412 | var typeName = _getTypeName(f.type); 413 | 414 | source.write(" value = _decodeField(data, '${f.name}',"); 415 | _buildMapper(source, f.type, typeName); 416 | source.write(", fieldDecoder, typeCodecs, $typeName"); 417 | 418 | if (f.metadata != null) { 419 | var fieldExp = f.metadata.fieldExp; 420 | var exps = f.metadata.exps; 421 | source.write(", $fieldExp, $exps"); 422 | } 423 | 424 | source.write(");\n"); 425 | source.write(" if (value != null) {\n"); 426 | source.write(" obj.${f.name} = value;\n"); 427 | source.write(" }\n"); 428 | }); 429 | 430 | source.write(" return obj;\n }"); 431 | } 432 | 433 | void _buildFields(StringBuffer source) { 434 | source.write("{"); 435 | fields.where((f) => f.canEncode).forEach((f) { 436 | source.write("'${f.name}': new FieldWrapper((obj) => obj.${f.name}"); 437 | if (f.metadata != null) { 438 | source.write(", ${f.metadata.exps}"); 439 | } 440 | source.write("),"); 441 | }); 442 | 443 | source.write("}"); 444 | } 445 | 446 | String _getTypeName(DartType type) { 447 | String typePrefix = ""; 448 | String typeName; 449 | 450 | if (type.element != null && !type.isDynamic) { 451 | typePrefix = usedLibs.resolveLib(type.element.library); 452 | } 453 | if (typePrefix.isNotEmpty) { 454 | typeName = "$typePrefix.${type.name}"; 455 | } else { 456 | typeName = "${type.name}"; 457 | } 458 | 459 | return typeName; 460 | } 461 | 462 | void _buildMapper(StringBuffer source, DartType type, String typeName) { 463 | if (type.isDynamic) { 464 | source.write("factory(null, encodable: false)"); 465 | } else if (collectionType.isList(type)) { 466 | if (type is ParameterizedType) { 467 | var pType = type as ParameterizedType; 468 | if (pType.typeArguments.isNotEmpty) { 469 | var paramType = pType.typeArguments[0]; 470 | var paramTypeName = _getTypeName(paramType); 471 | source.write("factory(null, isList: true, wrap: "); 472 | _buildMapper(source, paramType, paramTypeName); 473 | source.write(")"); 474 | } else { 475 | source.write("factory(null, isList: true, wrap: factory(Object))"); 476 | } 477 | } else { 478 | source.write("factory(null, isList: true, wrap: factory(Object))"); 479 | } 480 | } else if (collectionType.isMap(type)) { 481 | if (type is ParameterizedType) { 482 | var pType = type; 483 | if (pType.typeArguments.isNotEmpty) { 484 | var paramType = pType.typeArguments[1]; 485 | var paramTypeName = _getTypeName(paramType); 486 | source.write("factory($typeName, isMap: true, wrap: "); 487 | _buildMapper(source, paramType, paramTypeName); 488 | source.write(")"); 489 | } else { 490 | source.write("factory(null, isMap: true, wrap: factory(Object))"); 491 | } 492 | } else { 493 | source.write("factory(null, isMap: true, wrap: factory(Object))"); 494 | } 495 | } else { 496 | if (type.element.library.isDartCore) { 497 | source.write("factory(null, encodable: false)"); 498 | } else { 499 | source.write("factory($typeName)"); 500 | } 501 | } 502 | } 503 | } 504 | 505 | class _UsedLibs { 506 | final Set libs = new Set(); 507 | final Map prefixes = {}; 508 | 509 | String resolveLib(LibraryElement lib) { 510 | libs.add(lib); 511 | var prefix = prefixes[lib]; 512 | if (prefix == null) { 513 | prefix = lib.isDartCore ? "" : "import_${prefixes.length}"; 514 | prefixes[lib] = prefix; 515 | } 516 | return prefix; 517 | } 518 | } 519 | 520 | class _CollectionType { 521 | ClassElement listType; 522 | ClassElement mapType; 523 | 524 | _CollectionType(Resolver resolver) { 525 | listType = resolver.getType("dart.core.List"); 526 | mapType = resolver.getType("dart.core.Map"); 527 | } 528 | 529 | bool isList(DartType type) => 530 | type.element is ClassElement && 531 | (type.element == listType || 532 | (type.element as ClassElement) 533 | .allSupertypes 534 | .map((i) => i.element) 535 | .contains(listType)); 536 | 537 | bool isMap(DartType type) => 538 | type.element is ClassElement && 539 | (type.element == mapType || 540 | (type.element as ClassElement) 541 | .allSupertypes 542 | .map((i) => i.element) 543 | .contains(mapType)); 544 | } 545 | 546 | class _FieldMetadata { 547 | final String fieldExp; 548 | final List exps; 549 | 550 | _FieldMetadata(this.fieldExp, this.exps); 551 | } 552 | 553 | class _FieldInfo { 554 | final String name; 555 | final _FieldMetadata metadata; 556 | final DartType type; 557 | final bool canEncode; 558 | final bool canDecode; 559 | 560 | _FieldInfo(this.name, this.type, this.metadata, 561 | {this.canDecode: true, this.canEncode: true}); 562 | } 563 | 564 | class _MapperDynamicToStaticVisitor extends GeneralizingAstVisitor { 565 | final Element mapperDynamicFn; 566 | final TextEditTransaction transaction; 567 | _MapperDynamicToStaticVisitor(this.mapperDynamicFn, this.transaction); 568 | 569 | visitMethodInvocation(MethodInvocation m) { 570 | if (m.methodName.bestElement == mapperDynamicFn) { 571 | transaction.edit(m.methodName.beginToken.offset, 572 | m.methodName.endToken.end, 'staticBootstrapMapper'); 573 | 574 | var args = m.argumentList; 575 | transaction.edit(args.beginToken.offset + 1, args.end - 1, 576 | 'generated_static_mapper.types'); 577 | } 578 | super.visitMethodInvocation(m); 579 | } 580 | } 581 | --------------------------------------------------------------------------------