// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /** * This provides a general-purpose serialization facility for Dart objects. A * [Serialization] is defined in terms of [SerializationRule]s and supports * reading and writing to different formats. * * Setup * ===== * A simple example of usage is * * var address = new Address(); * address.street = 'N 34th'; * address.city = 'Seattle'; * var serialization = new Serialization() * ..addRuleFor(address); * String output = serialization.write(address); * * This creates a new serialization and adds a rule for address objects. Right * now it has to be passed an address instance because of limitations using * Address as a literal. Then we ask the Serialization to write the address * and we get back a Map which is a [json]able representation of the state of * the address and related objects. * * The version above used reflection to automatically identify the public * fields of the address object. We can also specify those fields explicitly. * * var serialization = new Serialization() * ..addRuleFor(address, * constructor: "create", * constructorFields: ["number", "street"], * fields: ["city"]); * * This rule still uses reflection to access the fields, but does not try to * identify which fields to use, but instead uses only the "number" and "street" * fields that we specified. We may also want to tell it to identify the * fields, but to specifically omit certain fields that we don't want * serialized. * * var serialization = new Serialization() * ..addRuleFor(address, * constructor: "", * excludeFields: ["other", "stuff"]); * * Writing Rules * ============= * We can also use a completely non-reflective rule to serialize and * de-serialize objects. This can be more work, but it does work in * dart2js, where mirrors are not yet implemented. We can specify this in two * ways. First, we can write our own SerializationRule class that has methods * for our Address class. * * class AddressRule extends CustomRule { * bool appliesTo(instance, Writer w) => instance.runtimeType == Address; * getState(instance) => [instance.street, instance.city]; * create(state) => new Address(); * setState(Address a, List state) { * a.street = state[0]; * a.city = state[1]; * } * } * * The class needs four different methods. The [appliesTo] method tells us if * the rule should be used to write an object. In this example we use a test * based on runtimeType. We could also use an "is Address" test, but if Address * has subclasses that would find those as well, and we want a separate rule * for each. The [getState] method should * return all the state of the object that we want to recreate, * and should be either a Map or a List. If you want to write to human-readable * formats where it's useful to be able to look at the data as a map from * field names to values, then it's better to return it as a map. Otherwise it's * more efficient to return it as a list. You just need to be sure that the * [create] and [setState] methods interpret the same way as [getState] does. * * The [create] method will create the new object and return it. While it's * possible to create the object and set all its state in this one method, that * increases the likelihood of problems with cycles. So it's better to use the * minimum necessary information in [create] and do more of the work in * [setState]. * * The other way to do this is not creating a subclass, but by using a * [ClosureRule] and giving it functions for how to create * the address. * * addressToMap(a) => {"number" : a.number, "street" : a.street, * "city" : a.city}; * createAddress(Map m) => new Address.create(m["number"], m["street"]); * fillInAddress(Address a, Map m) => a.city = m["city"]; * var serialization = new Serialization() * ..addRule( * new ClosureRule(anAddress.runtimeType, * addressToMap, createAddress, fillInAddress); * * In this case we have created standalone functions rather than * methods in a subclass and we pass them to the constructor of * [ClosureRule]. In this case we've also had them use maps rather than * lists for the state, but either would work as long as the rule is * consistent with the representation it uses. We pass it the runtimeType * of the object, and functions equivalent to the methods on [CustomRule] * * Constant Values * =============== * There are cases where the constructor needs values that we can't easily get * from the serialized object. For example, we may just want to pass null, or a * constant value. To support this, we can specify as constructor fields * values that aren't field names. If any value isn't a String, or is a string * that doesn't correspond to a field name, it will be * treated as a constant and passed unaltered to the constructor. * * In some cases a non-constructor field should not be set using field * access or a setter, but should be done by calling a method. For example, it * may not be possible to set a List field "foo", and you need to call an * addFoo() method for each entry in the list. In these cases, if you are using * a BasicRule for the object you can call the setFieldWith() method. * * s..addRuleFor(fooHolderInstance).setFieldWith("foo", * (parent, value) => for (var each in value) parent.addFoo(value)); * * Writing * ======= * To write objects, we use the write() method. * * var output = serialization.write(someObject); * * By default this uses a representation in which objects are represented as * maps keyed by field name, but in which references between objects have been * converted into Reference objects. This is then typically encoded as * a [json] string, but can also be used in other ways, e.g. sent to another * isolate. * * We can write objects in different formats by passing a [Format] object to * the [write] method or by getting a [Writer] object. The available formats * include the default, a simple "flat" format that doesn't include field names, * and a simple JSON format that produces output more suitable for talking to * services that expect JSON in a predefined format. Examples of these are * * Map output = serialization.write(address, new SimpleMapFormat()); * List output = serialization.write(address, new SimpleFlatFormat()); * var output = serialization.write(address, new SimpleJsonFormat()); * Or, using a [Writer] explicitly * var writer = serialization.newWriter(new SimpleFlatFormat()); * List output = writer.write(address); * * These representations are not yet considered stable. * * Reading * ======= * To read objects, the corresponding [read] method can be used. * * Address input = serialization.read(input); * * When reading, the serialization instance doing the reading must be configured * with compatible rules to the one doing the writing. It's possible for the * rules to be different, but they need to be able to read the same * representation. For most practical purposes right now they should be the * same. The simplest way to achieve this is by having the serialization * variable [selfDescribing] be true. In that case the rules themselves are also * stored along with the serialized data, and can be read back on the receiving * end. Note that this may not work for all rules or all formats. The * [selfDescribing] variable is true by default, but the [SimpleJsonFormat] does * not support it, since the point is to provide a representation in a form * other services might expect. Using CustomRule or ClosureRule also does not * yet work with the [selfDescribing] variable. * * Named Objects * ============= * When reading, some object references should not be serialized, but should be * connected up to other instances on the receiving side. A notable example of * this is when serialization rules have been stored. Instances of BasicRule * take a [ClassMirror] in their constructor, and we cannot serialize those. So * when we read the rules, we must provide a Map which maps from * the simple name of classes we are interested in to a [ClassMirror]. This can * be provided either in the [namedObjects] variable of the Serialization, * or as an additional parameter to the reading and writing methods on the * [Reader] or [Writer] respectively. * * new Serialization() * ..addRuleFor(new Person(), constructorFields: ["name"]) * ..namedObjects['Person'] = reflect(new Person()).type; */ library serialization; import 'src/mirrors_helpers.dart'; import 'src/serialization_helpers.dart'; import 'dart:async'; import 'dart:json' as json; import 'dart:collection'; part 'src/reader_writer.dart'; part 'src/serialization_rule.dart'; part 'src/basic_rule.dart'; part 'src/format.dart'; /** * This class defines a particular serialization scheme, in terms of * [SerializationRule] instances, and supports reading and writing them. * See library comment for examples of usage. */ class Serialization { /** * The serialization is controlled by the list of Serialization rules. These * are most commonly added via [addRuleFor]. */ List _rules = []; /** * The serialization is controlled by the list of Serialization rules. These * are most commonly added via [addRuleFor]. */ List get rules => _rules; /** * When reading, we may need to resolve references to existing objects in * the system. The right action may not be to create a new instance of * something, but rather to find an existing instance and connect to it. * For example, if we have are serializing an Email message and it has a * link to the owning account, it may not be appropriate to try and serialize * the account. Instead we should just connect the de-serialized message * object to the account object that already exists there. */ Map namedObjects = {}; /** * When we write out data using this serialization, should we also write * out a description of the rules. This is on by default unless using * CustomRule subclasses, in which case it requires additional setup and * is off by default. */ bool _selfDescribing; /** * When we write out data using this serialization, should we also write * out a description of the rules. This is on by default unless using * CustomRule subclasses, in which case it requires additional setup and * is off by default. */ bool get selfDescribing { // TODO(alanknight): Should this be moved to the format? // TODO(alanknight): Allow self-describing in the presence of CustomRule. if (_selfDescribing != null) return _selfDescribing; return !_rules.any((x) => x is CustomRule); } /** * When we write out data using this serialization, should we also write * out a description of the rules. This is on by default unless using * CustomRule subclasses, in which case it requires additional setup and * is off by default. */ set selfDescribing(x) => _selfDescribing = x; /** * Creates a new serialization with a default set of rules for primitives * and lists. */ Serialization() { addDefaultRules(); } /** * Creates a new serialization with no default rules at all. The most common * use for this is if we are reading self-describing serialized data and * will populate the rules from that data. */ Serialization.blank() { } /** * Create a [BasicRule] rule for the type of * [instanceOfType]. Optionally * allows specifying a [constructor] name, the list of [constructorFields], * and the list of [fields] not used in the constructor. Returns the new * rule. Note that [BasicRule] uses reflection, and so will not work with the * current state of dartj2s. If you need to run there, consider using * [CustomRule] instead. * * If the optional parameters aren't specified, the default constructor will * be used, and the list of fields will be computed. Alternatively, you can * omit [fields] and provide [excludeFields], which will then compute the * list of fields specifically excluding those listed. * * The fields can be actual public fields, but can also be getter/setter * pairs or getters whose value is provided in the constructor. For the * [constructorFields] they can also be arbitrary objects. Anything that is * not a String will be treated as a constant value to be used in any * construction of these objects. * * If the list of fields is computed, fields from the superclass will be * included. However, each subclass needs its own rule, since the constructors * are not inherited, and so may need to be specified separately for each * subclass. */ // TODO(alanknight): Take a type rather than an instance. Issue 6282 and 6433. BasicRule addRuleFor( instanceOfType, {String constructor, List constructorFields, List fields, List excludeFields}) { var rule; rule = new BasicRule( turnInstanceIntoSomethingWeCanUse( instanceOfType), constructor, constructorFields, fields, excludeFields); addRule(rule); return rule; } /** Set up the default rules, for lists and primitives. */ void addDefaultRules() { addRule(new PrimitiveRule()); addRule(new ListRule()); // Both these rules apply to lists, so unless otherwise indicated, // it will always find the first one. addRule(new ListRuleEssential()); addRule(new MapRule()); } /** * Add a new SerializationRule [rule]. The addRuleFor method will probably * handle most simple cases, but for adding an arbitrary rule, including * a SerializationRule subclass which you have created, you can use this * method. */ void addRule(SerializationRule rule) { rule.number = _rules.length; _rules.add(rule); } /** * This writes out an object graph rooted at [object] and returns the result. * The [format] parameter determines the form of the result. The default * format returns a String in [json] format. */ write(Object object, [Format format]) { return newWriter(format).write(object); } /** * Return a new [Writer] object for this serialization. This is useful if you * want to do something more complex with the writer than just returning * the final result. */ Writer newWriter([Format format]) => new Writer(this, format); /** * Read the serialized data from [input] and return the root object * from the result. The [input] can be of any type that the [Format] * reads/writes, but normally will be a [List], [Map], or a simple type. * If there are objects that need to be resolved * in the current context, they should be provided in [externals] as a * Map from names to values. In particular, in the current implementation * any class mirrors needed should be provided in [externals] using the * class name as a key. In addition to the [externals] map provided here, * values will be looked up in the [namedObjects] map. */ read(input, [Map externals = const {}]) { return newReader().read(input, externals); } /** * Return a new [Reader] object for this serialization. This is useful if * you want to do something more complex with the reader than just returning * the final result. */ Reader newReader([Format format]) => new Reader(this, format); /** * Return the list of SerializationRule that apply to [object]. For * internal use, but public because it's used in testing. */ Iterable rulesFor(object, Writer w) { // This has a couple of edge cases. // 1) The owning object may have indicated we should use a different // rule than the default. // 2) We may not have a rule, in which case we lazily create a BasicRule. // 3) Rules are allowed to say mustBePrimary, meaning that they can be used // iff no other rule was chosen first. // TODO(alanknight): Can the mustBePrimary mechanism be removed or changed. // It adds an order dependency to the rules, and is messy. Reconsider in the // light of a more general mechanism for multiple rules per object. // TODO(alanknight): Finding which rules apply seems likely to be a // bottleneck, particularly with the current reflective implementation. // Consider how to improve it. e.g. cache the list of rules by class. But // be careful of issues like rules which have arbitrary predicates. Or // consider having the arbitrary predicates be secondary to an initial // class-based lookup mechanism. var target, candidateRules; if (object is DesignatedRuleForObject) { target = object.target; candidateRules = object.possibleRules(_rules); } else { target = object; candidateRules = _rules; } Iterable applicable = candidateRules.where( (each) => each.appliesTo(target, w)); if (applicable.isEmpty) { return [addRuleFor(target)]; } if (applicable.length == 1) return applicable; var first = applicable.first; var finalRules = applicable.where( (x) => !x.mustBePrimary || (x == first)); if (finalRules.isEmpty) throw new SerializationException( 'No valid rule found for object $object'); return finalRules; } /** * Create a Serialization for serializing SerializationRules. This is used * to save the rules in a self-describing format along with the data. * If there are new rule classes created, they will need to be described * here. */ Serialization ruleSerialization() { // TODO(alanknight): There's an extensibility issue here with new rules. // TODO(alanknight): How to handle rules with closures? They have to // exist on the other side, but we might be able to hook them up by name, // or we might just be able to validate that they're correctly set up // on the other side. // Make some bogus rule instances so we have something to feed rule creation // and get their types. If only we had class literals implemented... var basicRule = new BasicRule(reflect(null).type, '', [], [], []); var meta = new Serialization() ..selfDescribing = false ..addRuleFor(new ListRule()) ..addRuleFor(new MapRule()) ..addRuleFor(new PrimitiveRule()) ..addRuleFor(new ListRuleEssential()) ..addRuleFor(basicRule, constructorFields: ['type', 'constructorName', 'constructorFields', 'regularFields', []], fields: []) ..addRule(new NamedObjectRule()) ..addRule(new MirrorRule()); meta.namedObjects = namedObjects; return meta; } /** Return true if our [namedObjects] collection has an entry for [object].*/ bool _hasNameFor(object) { var sentinel = const _Sentinel(); return _nameFor(object, () => sentinel) != sentinel; } /** * Return the name we have for [object] in our [namedObjects] collection or * the result of evaluating [ifAbsent] if there is no entry. */ _nameFor(object, [ifAbsent]) { for (var key in namedObjects.keys) { if (identical(namedObjects[key], object)) return key; } return ifAbsent == null ? null : ifAbsent(); } } /** * An exception class for errors during serialization. */ class SerializationException implements Exception { final String message; const SerializationException([this.message]); toString() => "SerializationException($message)"; }