├── CHANGELOG.md ├── lib ├── src │ ├── codable.dart │ ├── resolver.dart │ ├── coding.dart │ ├── list.dart │ └── keyed_archive.dart ├── codable.dart └── cast.dart ├── .travis.yml ├── .gitignore ├── analysis_options.yaml ├── pubspec.yaml ├── LICENSE ├── README.md └── test ├── encode_test.dart └── decode_test.dart /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version 4 | -------------------------------------------------------------------------------- /lib/src/codable.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable/src/resolver.dart'; 2 | 3 | abstract class Referencable { 4 | void resolveOrThrow(ReferenceResolver resolver); 5 | } -------------------------------------------------------------------------------- /lib/codable.dart: -------------------------------------------------------------------------------- 1 | /// Support for doing something awesome. 2 | /// 3 | /// More dartdocs go here. 4 | library codable; 5 | 6 | export 'src/coding.dart'; 7 | export 'src/list.dart'; 8 | export 'src/keyed_archive.dart'; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dart: 3 | - stable 4 | 5 | jobs: 6 | include: 7 | - stage: test 8 | script: pub get && pub run test 9 | 10 | stages: 11 | - test 12 | 13 | branches: 14 | only: 15 | - master 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | .pub/ 5 | build/ 6 | # Remove the following pattern if you wish to check in your lock file 7 | pubspec.lock 8 | 9 | # Directory created by dartdoc 10 | doc/api/ 11 | .idea/ 12 | *.iml -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 3 | linter: 4 | rules: 5 | - cancel_subscriptions 6 | - hash_and_equals 7 | - iterable_contains_unrelated_type 8 | - list_remove_unrelated_type 9 | - test_types_in_equals 10 | - unrelated_type_equality_checks 11 | - valid_regexps 12 | -------------------------------------------------------------------------------- /lib/src/resolver.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable/src/keyed_archive.dart'; 2 | 3 | class ReferenceResolver { 4 | ReferenceResolver(this.document); 5 | 6 | final KeyedArchive document; 7 | 8 | KeyedArchive resolve(Uri ref) { 9 | return ref.pathSegments.fold(document, (objectPtr, pathSegment) { 10 | return objectPtr[pathSegment] as Map; 11 | }) as KeyedArchive; 12 | } 13 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: codable 2 | description: A serialization library for converting dynamic, structured data (JSON, YAML) into Dart types. 3 | version: 1.0.0 4 | homepage: https://github.com/stablekernel/dart-codable 5 | author: stable|kernel 6 | 7 | environment: 8 | sdk: '>=2.0.0 <3.0.0' 9 | 10 | dependencies: 11 | meta: ^1.1.5 12 | 13 | dev_dependencies: 14 | test: ^1.3.0 15 | -------------------------------------------------------------------------------- /lib/src/coding.dart: -------------------------------------------------------------------------------- 1 | import 'package:codable/src/keyed_archive.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:codable/cast.dart' as cast; 4 | 5 | /// A base class for encodable and decodable objects. 6 | /// 7 | /// Types that can read or write their values to a document should extend this abstract class. 8 | /// By overriding [decode] and [encode], an instance of this type will read or write its values 9 | /// into a data container that can be transferred into formats like JSON or YAML. 10 | abstract class Coding { 11 | Uri referenceURI; 12 | Map> get castMap => null; 13 | 14 | @mustCallSuper 15 | void decode(KeyedArchive object) { 16 | referenceURI = object.referenceURI; 17 | object.castValues(castMap); 18 | } 19 | 20 | // would prefer to write referenceURI to object here, but see note in KeyedArchive._encodedObject 21 | void encode(KeyedArchive object); 22 | 23 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018, stable/kernel 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /lib/src/list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:codable/src/codable.dart'; 4 | import 'package:codable/src/coding.dart'; 5 | import 'package:codable/src/keyed_archive.dart'; 6 | import 'package:codable/src/resolver.dart'; 7 | 8 | /// A list of values in a [KeyedArchive]. 9 | /// 10 | /// This object is a [List] that has additional behavior for encoding and decoding [Coding] objects. 11 | class ListArchive extends Object with ListMixin implements Referencable { 12 | final List _inner; 13 | 14 | ListArchive() : _inner = []; 15 | 16 | /// Replaces all instances of [Map] and [List] in this object with [KeyedArchive] and [ListArchive]s. 17 | ListArchive.from(List raw) 18 | : _inner = raw.map((e) { 19 | if (e is Map) { 20 | return KeyedArchive(e); 21 | } else if (e is List) { 22 | return ListArchive.from(e); 23 | } 24 | return e; 25 | }).toList(); 26 | 27 | @override 28 | operator [](int index) => _inner[index]; 29 | 30 | @override 31 | int get length => _inner.length; 32 | 33 | @override 34 | set length(int length) { 35 | _inner.length = length; 36 | } 37 | 38 | @override 39 | void operator []=(int index, dynamic val) { 40 | _inner[index] = val; 41 | } 42 | 43 | @override 44 | void add(dynamic element) { 45 | _inner.add(element); 46 | } 47 | 48 | @override 49 | void addAll(Iterable iterable) { 50 | _inner.addAll(iterable); 51 | } 52 | 53 | List toPrimitive() { 54 | final out = []; 55 | _inner.forEach((val) { 56 | if (val is KeyedArchive) { 57 | out.add(val.toPrimitive()); 58 | } else if (val is ListArchive) { 59 | out.add(val.toPrimitive()); 60 | } else { 61 | out.add(val); 62 | } 63 | }); 64 | return out; 65 | } 66 | 67 | @override 68 | void resolveOrThrow(ReferenceResolver coder) { 69 | _inner.forEach((i) { 70 | if (i is KeyedArchive) { 71 | i.resolveOrThrow(coder); 72 | } else if (i is ListArchive) { 73 | i.resolveOrThrow(coder); 74 | } 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codable 2 | 3 | [![Build Status](https://travis-ci.org/stablekernel/dart-codable.svg?branch=master)](https://travis-ci.org/stablekernel/dart-codable) 4 | 5 | A library for encoding and decoding dynamic data into Dart objects. 6 | 7 | ## Basic Usage 8 | 9 | Data objects extend `Coding`: 10 | 11 | ```dart 12 | class Person extends Coding { 13 | String name; 14 | 15 | @override 16 | void decode(KeyedArchive object) { 17 | // must call super 18 | super.decode(object); 19 | 20 | name = object.decode("name"); 21 | } 22 | 23 | @override 24 | void encode(KeyedArchive object) { 25 | object.encode("name", name); 26 | } 27 | } 28 | ``` 29 | 30 | An object that extends `Coding` can be read from JSON: 31 | 32 | ```dart 33 | final json = json.decode(...); 34 | final archive = KeyedArchive.unarchive(json); 35 | final person = Person()..decode(archive); 36 | ``` 37 | 38 | Objects that extend `Coding` may also be written to JSON: 39 | 40 | ```dart 41 | final person = Person()..name = "Bob"; 42 | final archive = KeyedArchive.archive(person); 43 | final json = json.encode(archive); 44 | ``` 45 | 46 | `Coding` objects can encode or decode other `Coding` objects, including lists of `Coding` objects and maps where `Coding` objects are values. You must provide a closure that instantiates the `Coding` object being decoded. 47 | 48 | ```dart 49 | class Team extends Coding { 50 | 51 | List members; 52 | Person manager; 53 | 54 | @override 55 | void decode(KeyedArchive object) { 56 | super.decode(object); // must call super 57 | 58 | members = object.decodeObjects("members", () => Person()); 59 | manager = object.decodeObject("manager", () => Person()); 60 | } 61 | 62 | @override 63 | void encode(KeyedArchive object) { 64 | object.encodeObject("manager", manager); 65 | object.encodeObjects("members", members); 66 | } 67 | } 68 | ``` 69 | 70 | ## Dynamic Type Casting 71 | 72 | Types with primitive type arguments (e.g., `List` or `Map`) are a particular pain point when decoding. Override `castMap` in `Coding` to perform type coercion. 73 | You must import `package:codable/cast.dart as cast` and prefix type names with `cast`. 74 | 75 | ```dart 76 | import 'package:codable/cast.dart' as cast; 77 | class Container extends Coding { 78 | List things; 79 | 80 | @override 81 | Map> get castMap => { 82 | "things": cast.List(cast.String) 83 | }; 84 | 85 | 86 | @override 87 | void decode(KeyedArchive object) { 88 | super.decode(object); 89 | 90 | things = object.decode("things"); 91 | } 92 | 93 | @override 94 | void encode(KeyedArchive object) { 95 | object.encode("things", things); 96 | } 97 | } 98 | 99 | ``` 100 | 101 | 102 | ## Document References 103 | 104 | `Coding` objects may be referred to multiple times in a document without duplicating their structure. An object is referenced with the `$key` key. 105 | For example, consider the following JSON: 106 | 107 | ```json 108 | { 109 | "components": { 110 | "thing": { 111 | "name": "The Thing" 112 | } 113 | }, 114 | "data": { 115 | "$ref": "#/components/thing" 116 | } 117 | } 118 | ``` 119 | 120 | In the above, the decoded value of `data` inherits all properties from `/components/thing`: 121 | 122 | ```json 123 | { 124 | "$ref": "#/components/thing", 125 | "name": "The Thing" 126 | } 127 | ``` 128 | 129 | You may create references in your in-memory data structures through the `Coding.referenceURI`. 130 | 131 | ```dart 132 | final person = Person()..referenceURI = Uri(path: "/teams/engineering/manager"); 133 | ``` 134 | 135 | The above person is encoded as: 136 | 137 | ```json 138 | { 139 | "$ref": "#/teams/engineering/manager" 140 | } 141 | ``` 142 | 143 | You may have cyclical references. 144 | 145 | See the specification for [JSON Schema](http://json-schema.org) and the `$ref` keyword for more details. 146 | 147 | -------------------------------------------------------------------------------- /lib/cast.dart: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018, the Dart project authors. All rights reserved. 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | */ 29 | 30 | import 'dart:async' as async; 31 | import 'dart:core' as core; 32 | import 'dart:core' hide Map, String, int; 33 | 34 | class FailedCast implements core.Exception { 35 | dynamic context; 36 | dynamic key; 37 | core.String message; 38 | FailedCast(this.context, this.key, this.message); 39 | toString() { 40 | if (key == null) { 41 | return "Failed cast at $context: $message"; 42 | } 43 | return "Failed cast at $context $key: $message"; 44 | } 45 | } 46 | 47 | abstract class Cast { 48 | const Cast(); 49 | T _cast(dynamic from, core.String context, dynamic key); 50 | T cast(dynamic from) => _cast(from, "toplevel", null); 51 | } 52 | 53 | class AnyCast extends Cast { 54 | const AnyCast(); 55 | dynamic _cast(dynamic from, core.String context, dynamic key) => from; 56 | } 57 | 58 | class IntCast extends Cast { 59 | const IntCast(); 60 | core.int _cast(dynamic from, core.String context, dynamic key) => 61 | from is core.int 62 | ? from 63 | : throw new FailedCast(context, key, "$from is not an int"); 64 | } 65 | 66 | class DoubleCast extends Cast { 67 | const DoubleCast(); 68 | core.double _cast(dynamic from, core.String context, dynamic key) => 69 | from is core.double 70 | ? from 71 | : throw new FailedCast(context, key, "$from is not an double"); 72 | } 73 | 74 | class StringCast extends Cast { 75 | const StringCast(); 76 | core.String _cast(dynamic from, core.String context, dynamic key) => 77 | from is core.String 78 | ? from 79 | : throw new FailedCast(context, key, "$from is not a String"); 80 | } 81 | 82 | class BoolCast extends Cast { 83 | const BoolCast(); 84 | core.bool _cast(dynamic from, core.String context, dynamic key) => 85 | from is core.bool 86 | ? from 87 | : throw new FailedCast(context, key, "$from is not a bool"); 88 | } 89 | 90 | class Map extends Cast> { 91 | final Cast _key; 92 | final Cast _value; 93 | const Map(Cast key, Cast value) 94 | : _key = key, 95 | _value = value; 96 | core.Map _cast(dynamic from, core.String context, dynamic key) { 97 | if (from is core.Map) { 98 | var result = {}; 99 | for (var key in from.keys) { 100 | var newKey = _key._cast(key, "map entry", key); 101 | result[newKey] = _value._cast(from[key], "map entry", key); 102 | } 103 | return result; 104 | } 105 | return throw new FailedCast(context, key, "not a map"); 106 | } 107 | } 108 | 109 | class StringMap extends Cast> { 110 | final Cast _value; 111 | const StringMap(Cast value) : _value = value; 112 | core.Map _cast( 113 | dynamic from, core.String context, dynamic key) { 114 | if (from is core.Map) { 115 | var result = {}; 116 | for (core.String key in from.keys) { 117 | result[key] = _value._cast(from[key], "map entry", key); 118 | } 119 | return result; 120 | } 121 | return throw new FailedCast(context, key, "not a map"); 122 | } 123 | } 124 | 125 | class List extends Cast> { 126 | final Cast _entry; 127 | const List(Cast entry) : _entry = entry; 128 | core.List _cast(dynamic from, core.String context, dynamic key) { 129 | if (from is core.List) { 130 | var length = from.length; 131 | var result = core.List(length); 132 | for (core.int i = 0; i < length; ++i) { 133 | if (from[i] != null) { 134 | result[i] = _entry._cast(from[i], "list entry", i); 135 | } else { 136 | result[i] = null; 137 | } 138 | } 139 | return result; 140 | } 141 | return throw new FailedCast(context, key, "not a list"); 142 | } 143 | } 144 | 145 | class Keyed extends Cast> { 146 | Iterable get keys => _map.keys; 147 | final core.Map> _map; 148 | const Keyed(core.Map> map) 149 | : _map = map; 150 | core.Map _cast(dynamic from, core.String context, dynamic key) { 151 | core.Map result = {}; 152 | if (from is core.Map) { 153 | for (K key in from.keys) { 154 | if (_map.containsKey(key)) { 155 | result[key] = _map[key]._cast(from[key], "map entry", key); 156 | } else { 157 | result[key] = from[key]; 158 | } 159 | } 160 | return result; 161 | } 162 | return throw new FailedCast(context, key, "not a map"); 163 | } 164 | } 165 | 166 | class OneOf extends Cast { 167 | final Cast _left; 168 | final Cast _right; 169 | const OneOf(Cast left, Cast right) 170 | : _left = left, 171 | _right = right; 172 | dynamic _cast(dynamic from, core.String context, dynamic key) { 173 | try { 174 | return _left._cast(from, context, key); 175 | } on FailedCast { 176 | return _right._cast(from, context, key); 177 | } 178 | } 179 | } 180 | 181 | class Apply extends Cast { 182 | final Cast _first; 183 | final T Function(S) _transform; 184 | const Apply(T Function(S) transform, Cast first) 185 | : _transform = transform, 186 | _first = first; 187 | T _cast(dynamic from, core.String context, dynamic key) => 188 | _transform(_first._cast(from, context, key)); 189 | } 190 | 191 | class Future extends Cast> { 192 | final Cast _value; 193 | const Future(Cast value) : _value = value; 194 | async.Future _cast(dynamic from, core.String context, dynamic key) { 195 | if (from is async.Future) { 196 | return from.then(_value.cast); 197 | } 198 | return throw new FailedCast(context, key, "not a Future"); 199 | } 200 | } 201 | 202 | const any = AnyCast(); 203 | const bool = BoolCast(); 204 | const int = IntCast(); 205 | const double = DoubleCast(); 206 | const String = StringCast(); 207 | -------------------------------------------------------------------------------- /test/encode_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable/codable.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group("Primitive encode", () { 8 | test("Can encode primitive type", () { 9 | final out = encode((obj) { 10 | obj.encode("int", 1); 11 | obj.encode("string", "1"); 12 | }); 13 | 14 | expect(out, {"int": 1, "string": "1"}); 15 | }); 16 | 17 | test("Can encode List type", () { 18 | final out = encode((obj) { 19 | obj.encode("key", [1, "2"]); 20 | }); 21 | 22 | expect(out, { 23 | "key": [1, "2"] 24 | }); 25 | }); 26 | 27 | test("Can encode Map", () { 28 | final out = encode((obj) { 29 | obj.encode("key", {"1": 1, "2": "2"}); 30 | }); 31 | 32 | expect(out, { 33 | "key": {"1": 1, "2": "2"} 34 | }); 35 | }); 36 | 37 | test("Can encode URI", () { 38 | final out = encode((obj) { 39 | obj.encode("key", Uri.parse("https://host.com")); 40 | }); 41 | 42 | expect(out, {"key": "https://host.com"}); 43 | }); 44 | 45 | test("Can encode DateTime", () { 46 | final out = encode((obj) { 47 | obj.encode("key", new DateTime(2000)); 48 | }); 49 | 50 | expect(out, {"key": new DateTime(2000).toIso8601String()}); 51 | }); 52 | 53 | test("If value is null, do not include key", () { 54 | final out = encode((obj) { 55 | obj.encode("key", null); 56 | }); 57 | 58 | expect(out, {}); 59 | }); 60 | }); 61 | 62 | group("Coding objects", () { 63 | test("Can encode Coding object", () { 64 | final out = encode((object) { 65 | object.encodeObject("key", Parent("Bob")); 66 | }); 67 | 68 | expect(out, { 69 | "key": {"name": "Bob"} 70 | }); 71 | }); 72 | 73 | test("Can encode list of Coding objects", () { 74 | final out = encode((object) { 75 | object.encodeObject("key", Parent("Bob", children: [Child("Fred"), null, Child("Sally")])); 76 | }); 77 | 78 | expect(out, { 79 | "key": { 80 | "name": "Bob", 81 | "children": [ 82 | {"name": "Fred"}, 83 | null, 84 | {"name": "Sally"} 85 | ] 86 | } 87 | }); 88 | }); 89 | 90 | test("Can encode map of Coding objects", () { 91 | final out = encode((object) { 92 | object.encodeObject( 93 | "key", Parent("Bob", childMap: {"fred": Child("Fred"), "null": null, "sally": Child("Sally")})); 94 | }); 95 | 96 | expect(out, { 97 | "key": { 98 | "name": "Bob", 99 | "childMap": { 100 | "fred": {"name": "Fred"}, 101 | "null": null, 102 | "sally": {"name": "Sally"} 103 | } 104 | } 105 | }); 106 | }); 107 | }); 108 | 109 | group("Coding object references", () { 110 | test("Parent can contain reference to child in single object encode", () { 111 | final container = Container( 112 | Parent("Bob", child: Child._()..referenceURI = Uri(path: "/definitions/child")), {"child": Child("Sally")}); 113 | 114 | final out = KeyedArchive.archive(container, allowReferences: true); 115 | expect(out, { 116 | "definitions": { 117 | "child": {"name": "Sally"} 118 | }, 119 | "root": { 120 | "name": "Bob", 121 | "child": {"\$ref": "#/definitions/child"} 122 | } 123 | }); 124 | }); 125 | 126 | test("If reference doesn't exist, an error is thrown when creating document", () { 127 | final container = Container(Parent("Bob", child: Child._()..referenceURI = Uri(path: "/definitions/child")), {}); 128 | 129 | try { 130 | KeyedArchive.archive(container, allowReferences: true); 131 | fail('unreachable'); 132 | } on ArgumentError catch (e) { 133 | expect(e.toString(), contains("#/definitions/child")); 134 | } 135 | }); 136 | 137 | test("If reference doesn't exist in objectMap, an error is thrown when creating document", () { 138 | final container = 139 | Container(Parent("Bob", childMap: {"c": Child._()..referenceURI = Uri(path: "/definitions/child")}), {}); 140 | 141 | try { 142 | KeyedArchive.archive(container, allowReferences: true); 143 | fail('unreachable'); 144 | } on ArgumentError catch (e) { 145 | expect(e.toString(), contains("#/definitions/child")); 146 | } 147 | }); 148 | 149 | test("If reference doesn't exist in objectList, an error is thrown when creating document", () { 150 | final container = 151 | Container(Parent("Bob", children: [Child._()..referenceURI = Uri(path: "/definitions/child")]), {}); 152 | 153 | try { 154 | KeyedArchive.archive(container, allowReferences: true); 155 | fail('unreachable'); 156 | } on ArgumentError catch (e) { 157 | expect(e.toString(), contains("#/definitions/child")); 158 | } 159 | }); 160 | 161 | test("Parent can contain reference to child in a list of objects", () { 162 | final container = Container( 163 | Parent("Bob", children: [Child("Sally"), Child._()..referenceURI = Uri(path: "/definitions/child")]), 164 | {"child": Child("Fred")}); 165 | 166 | final out = KeyedArchive.archive(container, allowReferences: true); 167 | expect(out, { 168 | "definitions": { 169 | "child": {"name": "Fred"} 170 | }, 171 | "root": { 172 | "name": "Bob", 173 | "children": [ 174 | {"name": "Sally"}, 175 | {"\$ref": "#/definitions/child"} 176 | ] 177 | } 178 | }); 179 | }); 180 | 181 | test("Parent can contain reference to child in a map of objects", () { 182 | final container = Container( 183 | Parent("Bob", 184 | childMap: {"sally": Child("Sally"), "ref": Child._()..referenceURI = Uri(path: "/definitions/child")}), 185 | {"child": Child("Fred")}); 186 | 187 | final out = KeyedArchive.archive(container, allowReferences: true); 188 | expect(out, { 189 | "definitions": { 190 | "child": {"name": "Fred"} 191 | }, 192 | "root": { 193 | "name": "Bob", 194 | "childMap": { 195 | "sally": {"name": "Sally"}, 196 | "ref": {"\$ref": "#/definitions/child"} 197 | } 198 | } 199 | }); 200 | }); 201 | 202 | test("Cyclical references are resolved", () { 203 | final container = Container( 204 | Parent("Bob", children: [Child("Sally"), Child._()..referenceURI = Uri(path: "/definitions/child")]), 205 | {"child": Child("Fred", parent: Parent._()..referenceURI = Uri(path: "/root"))}); 206 | 207 | final out = KeyedArchive.archive(container, allowReferences: true); 208 | final expected = { 209 | "definitions": { 210 | "child": { 211 | "name": "Fred", 212 | "parent": {"\$ref": "#/root"} 213 | } 214 | }, 215 | "root": { 216 | "name": "Bob", 217 | "children": [ 218 | {"name": "Sally"}, 219 | {"\$ref": "#/definitions/child"}, 220 | ] 221 | } 222 | }; 223 | expect(out, expected); 224 | 225 | // we'll also ensure that writing it out and reading it back in 226 | // works, to complete the lifecycle of a document. we are ensuring 227 | // that no state is accumulated in decoding that impacts encoding 228 | // and ensure that our data is valid json 229 | final washedData = json.decode(json.encode(out)); 230 | final doc = KeyedArchive.unarchive(washedData); 231 | final decodedContainer = new Container._()..decode(doc); 232 | final reencodedArchive = KeyedArchive.archive(decodedContainer); 233 | expect(reencodedArchive, expected); 234 | }); 235 | }); 236 | 237 | test("toPrimitive does not include keyed archives or lists", () { 238 | final archive = KeyedArchive.unarchive({ 239 | "value": "v", 240 | "archive": {"key": "value"}, 241 | "list": [ 242 | "value", 243 | {"key": "value"}, 244 | ["value"] 245 | ] 246 | }); 247 | 248 | final encoded = archive.toPrimitive(); 249 | expect(encoded["value"], "v"); 250 | expect(encoded["archive"] is Map, true); 251 | expect(encoded["archive"] is KeyedArchive, false); 252 | expect(encoded["list"] is List, true); 253 | expect(encoded["list"] is ListArchive, false); 254 | expect(encoded["list"][0], "value"); 255 | expect(encoded["list"][1] is Map, true); 256 | expect(encoded["list"][1] is KeyedArchive, false); 257 | expect(encoded["list"][2] is List, true); 258 | expect(encoded["list"][2] is ListArchive, false); 259 | }); 260 | } 261 | 262 | Map encode(void encoder(KeyedArchive object)) { 263 | final archive = new KeyedArchive({}); 264 | encoder(archive); 265 | return json.decode(json.encode(archive)); 266 | } 267 | 268 | class Container extends Coding { 269 | Container._(); 270 | 271 | Container(this.root, this.definitions); 272 | 273 | Parent root; 274 | Map definitions; 275 | 276 | @override 277 | void decode(KeyedArchive object) { 278 | super.decode(object); 279 | 280 | root = object.decodeObject("root", () => Parent._()); 281 | definitions = object.decodeObjectMap("definitions", () => Child._()); 282 | } 283 | 284 | @override 285 | void encode(KeyedArchive object) { 286 | object.encodeObject("root", root); 287 | object.encodeObjectMap("definitions", definitions); 288 | } 289 | } 290 | 291 | class Parent extends Coding { 292 | Parent._(); 293 | 294 | Parent(this.name, {this.child, this.children, this.childMap, this.things}); 295 | 296 | String name; 297 | Child child; 298 | List children; 299 | Map childMap; 300 | List things; 301 | 302 | @override 303 | void decode(KeyedArchive object) { 304 | super.decode(object); 305 | 306 | name = object.decode("name"); 307 | child = object.decodeObject("child", () => Child._()); 308 | children = object.decodeObjects("children", () => Child._()); 309 | childMap = object.decodeObjectMap("childMap", () => Child._()); 310 | } 311 | 312 | @override 313 | void encode(KeyedArchive object) { 314 | object.encode("name", name); 315 | object.encodeObject("child", child); 316 | object.encodeObjects("children", children); 317 | object.encodeObjectMap("childMap", childMap); 318 | object.encode("things", things); 319 | } 320 | } 321 | 322 | class Child extends Coding { 323 | Child._(); 324 | 325 | Child(this.name, {this.parent}); 326 | 327 | String name; 328 | Parent parent; 329 | 330 | @override 331 | void decode(KeyedArchive object) { 332 | super.decode(object); 333 | 334 | name = object.decode("name"); 335 | parent = object.decodeObject("parent", () => Parent._()); 336 | } 337 | 338 | @override 339 | void encode(KeyedArchive object) { 340 | object.encode("name", name); 341 | object.encodeObject("parent", parent); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /test/decode_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:codable/codable.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:codable/cast.dart' as cast; 6 | 7 | void main() { 8 | group("Primitive decode", () { 9 | test("Can decode primitive type", () { 10 | final archive = getJSONArchive({"key": 2}); 11 | int val = archive.decode("key"); 12 | expect(val, 2); 13 | }); 14 | 15 | test("Can decode List type", () { 16 | final archive = getJSONArchive({ 17 | "key": [1, "2"] 18 | }); 19 | List l = archive.decode("key"); 20 | expect(l, [1, "2"]); 21 | }); 22 | 23 | test("Can decode Map", () { 24 | final archive = getJSONArchive({ 25 | "key": {"key": "val"} 26 | }); 27 | Map d = archive.decode("key"); 28 | expect(d, {"key": "val"}); 29 | }); 30 | 31 | test("Can decode URI", () { 32 | final archive = getJSONArchive({"key": "https://host.com"}); 33 | Uri d = archive.decode("key"); 34 | expect(d.host, "host.com"); 35 | }); 36 | 37 | test("Can decode DateTime", () { 38 | final date = new DateTime.now(); 39 | final archive = getJSONArchive({"key": date.toIso8601String()}); 40 | DateTime d = archive.decode("key"); 41 | expect(d.isAtSameMomentAs(date), true); 42 | }); 43 | 44 | test("If value is null, return null from decode", () { 45 | final archive = getJSONArchive({"key": null}); 46 | int val = archive.decode("key"); 47 | expect(val, isNull); 48 | }); 49 | 50 | test("If archive does not contain key, return null from decode", () { 51 | final archive = getJSONArchive({}); 52 | int val = archive.decode("key"); 53 | expect(val, isNull); 54 | }); 55 | }); 56 | 57 | group("Primitive map decode", () { 58 | test("Can decode Map from Map", () { 59 | final archive = getJSONArchive({ 60 | "key": {"key": "val"} 61 | }); 62 | archive.castValues({"key": cast.Map(cast.String, cast.String)}); 63 | Map d = archive.decode("key"); 64 | expect(d, {"key": "val"}); 65 | }); 66 | 67 | test("Can decode Map>", () { 68 | final archive = getJSONArchive({ 69 | "key": { 70 | "key": ["val"] 71 | } 72 | }); 73 | archive.castValues({"key": cast.Map(cast.String, cast.List(cast.String))}); 74 | Map> d = archive.decode("key"); 75 | expect(d, { 76 | "key": ["val"] 77 | }); 78 | }); 79 | 80 | test("Can decode Map> where elements are null", () { 81 | final archive = getJSONArchive({ 82 | "key": { 83 | "key": [null, null] 84 | } 85 | }); 86 | archive.castValues({"key": cast.Map(cast.String, cast.List(cast.String))}); 87 | Map> d = archive.decode("key"); 88 | expect(d, { 89 | "key": [null, null] 90 | }); 91 | }); 92 | 93 | test("Can decode Map>>", () { 94 | final archive = getJSONArchive({ 95 | "key": { 96 | "key": { 97 | "key": ["val", null] 98 | } 99 | } 100 | }); 101 | archive.castValues({"key": cast.Map(cast.String, cast.Map(cast.String, cast.List(cast.String)))}); 102 | Map>> d = archive.decode("key"); 103 | expect(d, { 104 | "key": { 105 | "key": ["val", null] 106 | } 107 | }); 108 | }); 109 | }); 110 | 111 | group("Primitive list decode", () { 112 | test("Can decode List from List", () { 113 | final archive = getJSONArchive({ 114 | "key": ["val", null] 115 | }); 116 | archive.castValues({"key": cast.List(cast.String)}); 117 | List d = archive.decode("key"); 118 | expect(d, ["val", null]); 119 | }); 120 | 121 | test("Can decode List>>", () { 122 | final archive = getJSONArchive({ 123 | "key": [ 124 | { 125 | "key": ["val", null] 126 | }, 127 | null 128 | ] 129 | }); 130 | archive.castValues({"key": cast.List(cast.Map(cast.String, cast.List(cast.String)))}); 131 | List>> d = archive.decode("key"); 132 | expect(d, [ 133 | { 134 | "key": ["val", null] 135 | }, 136 | null 137 | ]); 138 | }); 139 | }); 140 | 141 | group("Coding objects", () { 142 | test("Can decode Coding object", () { 143 | final archive = getJSONArchive({ 144 | "key": {"name": "Bob"} 145 | }); 146 | Parent p = archive.decodeObject("key", () => Parent()); 147 | expect(p.name, "Bob"); 148 | expect(p.child, isNull); 149 | expect(p.children, isNull); 150 | expect(p.childMap, isNull); 151 | }); 152 | 153 | test("If coding object is paired with non-Map, an exception is thrown", () { 154 | final archive = getJSONArchive({ 155 | "key": [ 156 | {"name": "Bob"} 157 | ] 158 | }); 159 | try { 160 | archive.decodeObject("key", () => Parent()); 161 | fail('unreachable'); 162 | } on ArgumentError {} 163 | }); 164 | 165 | test("Can decode list of Coding objects", () { 166 | final archive = getJSONArchive({ 167 | "key": [ 168 | {"name": "Bob"}, 169 | null, 170 | {"name": "Sally"} 171 | ] 172 | }); 173 | List p = archive.decodeObjects("key", () => Parent()); 174 | expect(p[0].name, "Bob"); 175 | expect(p[1], isNull); 176 | expect(p[2].name, "Sally"); 177 | }); 178 | 179 | test("If coding object list is paired with non-List, an exception is thrown", () { 180 | final archive = getJSONArchive({ 181 | "key": {"name": "Bob"} 182 | }); 183 | try { 184 | archive.decodeObjects("key", () => Parent()); 185 | fail('unreachable'); 186 | } on ArgumentError {} 187 | }); 188 | 189 | test("If any element of coding list is not a coding object, an exception is thrown", () { 190 | final archive = getJSONArchive({ 191 | "key": [ 192 | {"name": "Bob"}, 193 | 'foo' 194 | ] 195 | }); 196 | try { 197 | archive.decodeObjects("key", () => Parent()); 198 | fail('unreachable'); 199 | } on TypeError {} 200 | }); 201 | 202 | test("Can decode map of Coding objects", () { 203 | final archive = getJSONArchive({ 204 | "key": { 205 | "1": {"name": "Bob"}, 206 | "2": null 207 | } 208 | }); 209 | 210 | final map = archive.decodeObjectMap("key", () => Parent()); 211 | expect(map.length, 2); 212 | expect(map["1"].name, "Bob"); 213 | expect(map["2"], isNull); 214 | }); 215 | 216 | test("If coding object map is paired with non-Map, an exception is thrown", () { 217 | final archive = getJSONArchive({"key": []}); 218 | try { 219 | archive.decodeObjectMap("key", () => Parent()); 220 | fail('unreachable'); 221 | } on ArgumentError {} 222 | }); 223 | 224 | test("If any element of coding map is not a coding object, an exception is thrown", () { 225 | final archive = getJSONArchive({ 226 | "key": {"1": "2"} 227 | }); 228 | try { 229 | archive.decodeObjectMap("key", () => Parent()); 230 | fail('unreachable'); 231 | } on TypeError {} 232 | }); 233 | }); 234 | 235 | group("Deep Coding objects", () { 236 | test("Can decode single nested object", () { 237 | final archive = getJSONArchive({ 238 | "key": { 239 | "name": "Bob", 240 | "child": {"name": "Sally"} 241 | } 242 | }); 243 | 244 | final o = archive.decodeObject("key", () => Parent()); 245 | expect(o.name, "Bob"); 246 | expect(o.child.name, "Sally"); 247 | expect(o.childMap, isNull); 248 | expect(o.children, isNull); 249 | }); 250 | 251 | test("Can decode list of nested objects", () { 252 | final archive = getJSONArchive({ 253 | "key": { 254 | "name": "Bob", 255 | "children": [ 256 | {"name": "Sally"} 257 | ] 258 | } 259 | }); 260 | 261 | final o = archive.decodeObject("key", () => Parent()); 262 | expect(o.name, "Bob"); 263 | expect(o.child, isNull); 264 | expect(o.childMap, isNull); 265 | expect(o.children.length, 1); 266 | expect(o.children.first.name, "Sally"); 267 | }); 268 | 269 | test("Can decode map of nested objects", () { 270 | final archive = getJSONArchive({ 271 | "key": { 272 | "name": "Bob", 273 | "childMap": { 274 | "sally": {"name": "Sally"} 275 | } 276 | } 277 | }); 278 | 279 | final o = archive.decodeObject("key", () => Parent()); 280 | expect(o.name, "Bob"); 281 | expect(o.children, isNull); 282 | expect(o.child, isNull); 283 | expect(o.childMap.length, 1); 284 | expect(o.childMap["sally"].name, "Sally"); 285 | }); 286 | }); 287 | 288 | group("Coding object references", () { 289 | test("Parent can contain reference to child in single object decode", () { 290 | final archive = getJSONArchive({ 291 | "child": {"name": "Sally"}, 292 | "parent": { 293 | "name": "Bob", 294 | "child": {"\$ref": "#/child"} 295 | } 296 | }, allowReferences: true); 297 | 298 | final p = archive.decodeObject("parent", () => Parent()); 299 | expect(p.name, "Bob"); 300 | expect(p.child.name, "Sally"); 301 | expect(p.child.parent, isNull); 302 | }); 303 | 304 | test("If reference doesn't exist, an error is thrown when creating document", () { 305 | try { 306 | getJSONArchive({ 307 | "parent": { 308 | "name": "Bob", 309 | "child": {"\$ref": "#/child"} 310 | } 311 | }, allowReferences: true); 312 | fail("unreachable"); 313 | } on ArgumentError catch (e) { 314 | expect(e.toString(), contains("/child")); 315 | } 316 | }); 317 | 318 | test("Parent can contain reference to child in a list of objects", () { 319 | final archive = getJSONArchive({ 320 | "child": {"name": "Sally"}, 321 | "parent": { 322 | "name": "Bob", 323 | "children": [{"\$ref": "#/child"}, {"name": "fred"}] 324 | } 325 | }, allowReferences: true); 326 | 327 | final p = archive.decodeObject("parent", () => Parent()); 328 | expect(p.name, "Bob"); 329 | expect(p.children.first.name, "Sally"); 330 | expect(p.children.last.name, "fred"); 331 | }); 332 | 333 | test("Cyclical references are resolved", () { 334 | final archive = getJSONArchive({ 335 | "child": {"name": "Sally", "parent": {"\$ref": "#/parent"}}, 336 | "parent": { 337 | "name": "Bob", 338 | "children": [{"\$ref": "#/child"}, {"name": "fred"}] 339 | } 340 | }, allowReferences: true); 341 | 342 | final p = archive.decodeObject("parent", () => Parent()); 343 | expect(p.name, "Bob"); 344 | expect(p.children.first.name, "Sally"); 345 | expect(p.children.first.parent.name, "Bob"); 346 | expect(p.children.last.name, "fred"); 347 | 348 | expect(p.hashCode, isNot(p.children.first.parent.hashCode)); 349 | }); 350 | 351 | test("Can override castMap to coerce values", () { 352 | final archive = getJSONArchive({ 353 | "key": { 354 | "name": "Bob", 355 | "things": ["value"] 356 | } 357 | }); 358 | final p = archive.decodeObject("key", () => Parent()); 359 | expect(p.things, ["value"]); 360 | }); 361 | }); 362 | 363 | } 364 | 365 | /// Strips type info from data 366 | KeyedArchive getJSONArchive(dynamic data, {bool allowReferences: false}) { 367 | return KeyedArchive.unarchive(json.decode(json.encode(data)), allowReferences: allowReferences); 368 | } 369 | 370 | class Parent extends Coding { 371 | String name; 372 | Child child; 373 | List children; 374 | Map childMap; 375 | List things; 376 | 377 | @override 378 | Map> get castMap { 379 | return { 380 | "things": cast.List(cast.String) 381 | }; 382 | } 383 | 384 | @override 385 | void decode(KeyedArchive object) { 386 | super.decode(object); 387 | 388 | name = object.decode("name"); 389 | child = object.decodeObject("child", () => Child()); 390 | children = object.decodeObjects("children", () => Child()); 391 | childMap = object.decodeObjectMap("childMap", () => Child()); 392 | things = object.decode("things"); 393 | } 394 | 395 | @override 396 | void encode(KeyedArchive object) {} 397 | } 398 | 399 | class Child extends Coding { 400 | String name; 401 | 402 | Parent parent; 403 | 404 | @override 405 | void decode(KeyedArchive object) { 406 | super.decode(object); 407 | 408 | name = object.decode("name"); 409 | parent = object.decodeObject("parent", () => Parent()); 410 | } 411 | 412 | @override 413 | void encode(KeyedArchive object) {} 414 | } 415 | -------------------------------------------------------------------------------- /lib/src/keyed_archive.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | import 'package:codable/src/codable.dart'; 3 | import 'package:codable/src/coding.dart'; 4 | import 'package:codable/cast.dart' as cast; 5 | import 'package:codable/src/list.dart'; 6 | import 'package:codable/src/resolver.dart'; 7 | 8 | /// A container for a dynamic data object that can be decoded into [Coding] objects. 9 | /// 10 | /// A [KeyedArchive] is a [Map], but it provides additional behavior for decoding [Coding] objects 11 | /// and managing JSON Schema references ($ref) through methods like [decode], [decodeObject], etc. 12 | /// 13 | /// You create a [KeyedArchive] by invoking [KeyedArchive.unarchive] and passing data decoded from a 14 | /// serialization format like JSON and YAML. A [KeyedArchive] is then provided as an argument to 15 | /// a [Coding] subclass' [Coding.decode] method. 16 | /// 17 | /// final json = json.decode(...); 18 | /// final archive = KeyedArchive.unarchive(json); 19 | /// final person = Person()..decode(archive); 20 | /// 21 | /// You may also create [KeyedArchive]s from [Coding] objects so that they can be serialized. 22 | /// 23 | /// final person = Person()..name = "Bob"; 24 | /// final archive = KeyedArchive.archive(person); 25 | /// final json = json.encode(archive); 26 | /// 27 | class KeyedArchive extends Object with MapMixin implements Referencable { 28 | /// Unarchives [data] into a [KeyedArchive] that can be used by [Coding.decode] to deserialize objects. 29 | /// 30 | /// Each [Map] in [data] (including [data] itself) is converted to a [KeyedArchive]. 31 | /// Each [List] in [data] is converted to a [ListArchive]. These conversions occur for deeply nested maps 32 | /// and lists. 33 | /// 34 | /// If [allowReferences] is true, JSON Schema references will be traversed and decoded objects 35 | /// will contain values from the referenced object. This flag defaults to false. 36 | static KeyedArchive unarchive(Map data, {bool allowReferences: false}) { 37 | final archive = new KeyedArchive(data); 38 | if (allowReferences) { 39 | archive.resolveOrThrow(new ReferenceResolver(archive)); 40 | } 41 | return archive; 42 | } 43 | 44 | /// Archives a [Coding] object into a [Map] that can be serialized into format like JSON or YAML. 45 | /// 46 | /// Note that the return value of this method, as well as all other [Map] and [List] objects 47 | /// embedded in the return value, are instances of [KeyedArchive] and [ListArchive]. These types 48 | /// implement [Map] and [List], respectively. 49 | /// 50 | /// If [allowReferences] is true, JSON Schema references in the emitted document will be validated. 51 | /// Defaults to false. 52 | static Map archive(Coding root, {bool allowReferences: false}) { 53 | final archive = new KeyedArchive({}); 54 | root.encode(archive); 55 | if (allowReferences) { 56 | archive.resolveOrThrow(new ReferenceResolver(archive)); 57 | } 58 | return archive.toPrimitive(); 59 | } 60 | 61 | KeyedArchive._empty(); 62 | 63 | /// Use [unarchive] instead. 64 | KeyedArchive(this._map) { 65 | _recode(); 66 | } 67 | 68 | /// A reference to another object in the same document. 69 | /// 70 | /// This value is a path-only [Uri]. Each path segment is a key, starting 71 | /// at the document root this object belongs to. For example, the path '/components/all' 72 | /// would reference the object as returned by `document['components']['all']`. 73 | /// 74 | /// Assign values to this property using the default [Uri] constructor and its path argument. 75 | /// This property is serialized as a [Uri] fragment, e.g. `#/components/all`. 76 | /// 77 | /// Example: 78 | /// 79 | /// final object = new MyObject() 80 | /// ..referenceURI = Uri(path: "/other/object"); 81 | /// archive.encodeObject("object", object); 82 | /// 83 | Uri referenceURI; 84 | 85 | Map _map; 86 | Coding _inflated; 87 | KeyedArchive _objectReference; 88 | 89 | /// Typecast the values in this archive. 90 | /// 91 | /// Prefer to override [Coding.castMap] instead of using this method directly. 92 | /// 93 | /// This method will recursively type values in this archive to the desired type 94 | /// for a given key. Use this method (or [Coding.castMap]) for decoding `List` and `Map` 95 | /// types, where the values are not `Coding` objects. 96 | /// 97 | /// You must `import 'package:codable/cast.dart' as cast;`. 98 | /// 99 | /// Usage: 100 | /// 101 | /// final dynamicObject = { 102 | /// "key": ["foo", "bar"] 103 | /// }; 104 | /// final archive = KeyedArchive.unarchive(dynamicObject); 105 | /// archive.castValues({ 106 | /// "key": cast.List(cast.String) 107 | /// }); 108 | /// 109 | /// // This now becomes a valid assignment 110 | /// List key = archive.decode("key"); 111 | /// 112 | void castValues(Map schema) { 113 | if (schema == null) { 114 | return; 115 | } 116 | 117 | final caster = new cast.Keyed(schema); 118 | _map = caster.cast(_map); 119 | 120 | if (_objectReference != null) { 121 | // todo: can optimize this by only running it once 122 | _objectReference._map = caster.cast(_objectReference._map); 123 | } 124 | } 125 | 126 | operator []=(String key, dynamic value) { 127 | _map[key] = value; 128 | } 129 | 130 | dynamic operator [](Object key) => _getValue(key); 131 | 132 | Iterable get keys => _map.keys; 133 | 134 | void clear() => _map.clear(); 135 | 136 | dynamic remove(Object key) => _map.remove(key); 137 | 138 | Map toPrimitive() { 139 | final out = {}; 140 | _map.forEach((key, val) { 141 | if (val is KeyedArchive) { 142 | out[key] = val.toPrimitive(); 143 | } else if (val is ListArchive) { 144 | out[key] = val.toPrimitive(); 145 | } else { 146 | out[key] = val; 147 | } 148 | }); 149 | return out; 150 | } 151 | 152 | dynamic _getValue(String key) { 153 | if (_map.containsKey(key)) { 154 | return _map[key]; 155 | } 156 | 157 | return _objectReference?._getValue(key); 158 | } 159 | 160 | void _recode() { 161 | const caster = cast.Map(cast.String, cast.any); 162 | final keys = _map.keys.toList(); 163 | keys.forEach((key) { 164 | final val = _map[key]; 165 | if (val is Map) { 166 | _map[key] = new KeyedArchive(caster.cast(val)); 167 | } else if (val is List) { 168 | _map[key] = new ListArchive.from(val); 169 | } else if (key == r"$ref") { 170 | if (val is Map) { 171 | _objectReference = val; 172 | } else { 173 | referenceURI = Uri.parse(Uri.parse(val).fragment); 174 | } 175 | } 176 | }); 177 | } 178 | 179 | /// Validates [referenceURI]s for this object and any objects it contains. 180 | /// 181 | /// This method is automatically invoked by both [KeyedArchive.unarchive] and [KeyedArchive.archive]. 182 | @override 183 | void resolveOrThrow(ReferenceResolver coder) { 184 | if (referenceURI != null) { 185 | _objectReference = coder.resolve(referenceURI); 186 | if (_objectReference == null) { 187 | throw new ArgumentError("Invalid document. Reference '#${referenceURI.path}' does not exist in document."); 188 | } 189 | } 190 | 191 | _map.forEach((key, val) { 192 | if (val is KeyedArchive) { 193 | val.resolveOrThrow(coder); 194 | } else if (val is ListArchive) { 195 | val.resolveOrThrow(coder); 196 | } 197 | }); 198 | } 199 | 200 | /* decode */ 201 | 202 | T _decodedObject(KeyedArchive raw, T inflate()) { 203 | if (raw == null) { 204 | return null; 205 | } 206 | 207 | if (raw._inflated == null) { 208 | raw._inflated = inflate(); 209 | raw._inflated.decode(raw); 210 | } 211 | 212 | return raw._inflated; 213 | } 214 | 215 | /// Returns the object associated by [key]. 216 | /// 217 | /// If [T] is inferred to be a [Uri] or [DateTime], 218 | /// the associated object is assumed to be a [String] and an appropriate value is parsed 219 | /// from that string. 220 | /// 221 | /// If this object is a reference to another object (via [referenceURI]), this object's key-value 222 | /// pairs will be searched first. If [key] is not found, the referenced object's key-values pairs are searched. 223 | /// If no match is found, null is returned. 224 | T decode(String key) { 225 | var v = _getValue(key); 226 | if (v == null) { 227 | return null; 228 | } 229 | 230 | if (T == Uri) { 231 | return Uri.parse(v) as T; 232 | } else if (T == DateTime) { 233 | return DateTime.parse(v) as T; 234 | } 235 | 236 | return v; 237 | } 238 | 239 | /// Returns the instance of [T] associated with [key]. 240 | /// 241 | /// [inflate] must create an empty instance of [T]. The value associated with [key] 242 | /// must be a [KeyedArchive] (a [Map]). The values of the associated object are read into 243 | /// the empty instance of [T]. 244 | T decodeObject(String key, T inflate()) { 245 | final val = _getValue(key); 246 | if (val == null) { 247 | return null; 248 | } 249 | 250 | if (val is! KeyedArchive) { 251 | throw new ArgumentError( 252 | "Cannot decode key '$key' into '$T', because the value is not a Map. Actual value: '$val'."); 253 | } 254 | 255 | return _decodedObject(val, inflate); 256 | } 257 | 258 | /// Returns a list of [T]s associated with [key]. 259 | /// 260 | /// [inflate] must create an empty instance of [T]. The value associated with [key] 261 | /// must be a [ListArchive] (a [List] of [Map]). For each element of the archived list, 262 | /// [inflate] is invoked and each object in the archived list is decoded into 263 | /// the instance of [T]. 264 | List decodeObjects(String key, T inflate()) { 265 | var val = _getValue(key); 266 | if (val == null) { 267 | return null; 268 | } 269 | if (val is! List) { 270 | throw new ArgumentError( 271 | "Cannot decode key '$key' as 'List<$T>', because value is not a List. Actual value: '$val'."); 272 | } 273 | 274 | return (val as List).map((v) => _decodedObject(v, inflate)).toList().cast(); 275 | } 276 | 277 | /// Returns a map of [T]s associated with [key]. 278 | /// 279 | /// [inflate] must create an empty instance of [T]. The value associated with [key] 280 | /// must be a [KeyedArchive] (a [Map]), where each value is a [T]. 281 | /// For each key-value pair of the archived map, [inflate] is invoked and 282 | /// each value is decoded into the instance of [T]. 283 | Map decodeObjectMap(String key, T inflate()) { 284 | var v = _getValue(key); 285 | if (v == null) { 286 | return null; 287 | } 288 | 289 | if (v is! Map) { 290 | throw new ArgumentError("Cannot decode key '$key' as 'Map', because value is not a Map. Actual value: '$v'."); 291 | } 292 | 293 | return new Map.fromIterable(v.keys, key: (k) => k, value: (k) => _decodedObject(v[k], inflate)); 294 | } 295 | 296 | /* encode */ 297 | 298 | Map _encodedObject(Coding object) { 299 | if (object == null) { 300 | return null; 301 | } 302 | 303 | // todo: an object can override the values it inherits from its 304 | // reference object. These values are siblings to the $ref key. 305 | // they are currently not being emitted. the solution is probably tricky. 306 | // letting encode run as normal would stack overflow when there is a cyclic 307 | // reference between this object and another. 308 | var json = new KeyedArchive._empty().._map = {}..referenceURI = object.referenceURI; 309 | if (json.referenceURI != null) { 310 | json._map[r"$ref"] = Uri(fragment: json.referenceURI.path).toString(); 311 | } else { 312 | object.encode(json); 313 | } 314 | return json; 315 | } 316 | 317 | /// Encodes [value] into this object for [key]. 318 | /// 319 | /// If [value] is a [DateTime], it is first encoded as an ISO 8601 string. 320 | /// If [value] is a [Uri], it is first encoded to a string. 321 | /// 322 | /// If [value] is null, no value is encoded and the [key] will not be present 323 | /// in the resulting archive. 324 | void encode(String key, dynamic value) { 325 | if (value == null) { 326 | return; 327 | } 328 | 329 | if (value is DateTime) { 330 | _map[key] = value.toIso8601String(); 331 | } else if (value is Uri) { 332 | _map[key] = value.toString(); 333 | } else { 334 | _map[key] = value; 335 | } 336 | } 337 | 338 | /// Encodes a [Coding] object into this object for [key]. 339 | /// 340 | /// This invokes [Coding.encode] on [value] and adds the object 341 | /// to this archive for the key [key]. 342 | void encodeObject(String key, Coding value) { 343 | if (value == null) { 344 | return; 345 | } 346 | 347 | _map[key] = _encodedObject(value); 348 | } 349 | 350 | /// Encodes list of [Coding] objects into this object for [key]. 351 | /// 352 | /// This invokes [Coding.encode] on each object in [value] and adds the list of objects 353 | /// to this archive for the key [key]. 354 | void encodeObjects(String key, List value) { 355 | if (value == null) { 356 | return; 357 | } 358 | 359 | _map[key] = new ListArchive.from(value.map((v) => _encodedObject(v)).toList()); 360 | } 361 | 362 | /// Encodes map of [Coding] objects into this object for [key]. 363 | /// 364 | /// This invokes [Coding.encode] on each value in [value] and adds the map of objects 365 | /// to this archive for the key [key]. 366 | void encodeObjectMap(String key, Map value) { 367 | if (value == null) { 368 | return; 369 | } 370 | 371 | final object = KeyedArchive({}); 372 | value.forEach((k, v) { 373 | object[k] = _encodedObject(v); 374 | }); 375 | 376 | _map[key] = object; 377 | } 378 | } 379 | 380 | --------------------------------------------------------------------------------