Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(252)

Side by Side Diff: pkg/serialization/lib/serialization.dart

Issue 11820032: Make input/output formats pluggable, adapt to new libraries (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 /** 5 /**
6 * This provides a general-purpose serialization facility for Dart objects. A 6 * This provides a general-purpose serialization facility for Dart objects. A
7 * [Serialization] is defined in terms of [SerializationRule]s and supports 7 * [Serialization] is defined in terms of [SerializationRule]s and supports
8 * reading and writing to different formats. 8 * reading and writing to different formats.
9 * 9 *
10 * Setup 10 * Setup
11 * ===== 11 * =====
12 * A simple example of usage is 12 * A simple example of usage is
13 * 13 *
14 * var address = new Address(); 14 * var address = new Address();
15 * address.street = 'N 34th'; 15 * address.street = 'N 34th';
16 * address.city = 'Seattle'; 16 * address.city = 'Seattle';
17 * var serialization = new Serialization() 17 * var serialization = new Serialization()
18 * ..addRuleFor(address); 18 * ..addRuleFor(address);
19 * String output = serialization.write(address); 19 * String output = serialization.write(address);
20 * 20 *
21 * This creates a new serialization and adds a rule for address objects. Right 21 * This creates a new serialization and adds a rule for address objects. Right
22 * now it has to be passed an address instance because we can't write Address 22 * now it has to be passed an address instance because of limitations using
23 * as a literal. Then we ask the Serialization to write the address and we get 23 * Address as a literal. Then we ask the Serialization to write the address
24 * back a String which is a [JSON] representation of the state of it and related 24 * and we get back a String which is a [JSON] representation of the state of
25 * objects. 25 * it and related objects.
26 * 26 *
27 * The version above used reflection to automatically identify the public 27 * The version above used reflection to automatically identify the public
28 * fields of the address object. We can also specify those fields explicitly. 28 * fields of the address object. We can also specify those fields explicitly.
29 * 29 *
30 * var serialization = new Serialization() 30 * var serialization = new Serialization()
31 * ..addRuleFor(address, 31 * ..addRuleFor(address,
32 * constructor: "create", 32 * constructor: "create",
33 * constructorFields: ["number", "street"], 33 * constructorFields: ["number", "street"],
34 * fields: ["city"]); 34 * fields: ["city"]);
35 * 35 *
36 * This rule still uses reflection to access the fields, but does not try to 36 * This rule still uses reflection to access the fields, but does not try to
37 * identify which fields to use, but instead uses only the "number" and "street" 37 * identify which fields to use, but instead uses only the "number" and "street"
38 * fields that we specified. We may also want to tell it to identify the 38 * fields that we specified. We may also want to tell it to identify the
39 * fields, but to specifically omit certain fields that we don't want 39 * fields, but to specifically omit certain fields that we don't want
40 * serialized. 40 * serialized.
41 * 41 *
42 * var serialization = new Serialization() 42 * var serialization = new Serialization()
43 * ..addRuleFor(address, 43 * ..addRuleFor(address,
44 * constructor: "", 44 * constructor: "",
45 * excludeFields: ["other", "stuff"]); 45 * excludeFields: ["other", "stuff"]);
46 * 46 *
47 * Writing Rules
48 * =============
47 * We can also use a completely non-reflective rule to serialize and 49 * We can also use a completely non-reflective rule to serialize and
48 * de-serialize objects. This can be more cumbersome, but it does work in 50 * de-serialize objects. This can be more work, but it does work in
49 * dart2js, where mirrors are not yet implemented. 51 * dart2js, where mirrors are not yet implemented. We can specify this in two
52 * ways. First, we can write our own SerializationRule class that has methods
53 * for our Address class.
54 *
55 * class AddressRule extends CustomRule {
56 * bool appliesTo(instance, Writer w) => instance.runtimeType == Address;
Jennifer Messerly 2013/01/11 02:21:53 "instance is Address"?
Alan Knight 2013/01/11 19:18:11 I actually did that on purpose, because if we have
Jennifer Messerly 2013/01/11 21:54:25 Yeah, comment would be nice. That makes sense. Do
Alan Knight 2013/01/11 22:35:00 null will get caught by the PrimitiveRule that co
57 * getState(instance) => [instance.street, instance.city];
58 * create(state) => new Address();
59 * setState(Address a, List state) {
60 * a.street = state[0];
61 * a.city = state[1];
62 * }
63 * }
64 *
65 * The class needs four different methods. The [appliesTo] method tells us if
66 * the rule should be used to write an object. The [getState] method should
67 * return all the state of the object that we want to recreate,
68 * and should be either a Map or a List. If you want to write to human-readable
69 * formats where it's useful to be able to look at the data as a map from
70 * field names to values, then it's better to return it as a map. Otherwise it's
71 * more efficient to return it as a list. You just need to be sure that the
72 * [create] and [setState] methods interpret the same way as [getState] does.
73 *
74 * The [create] method will create the new object and return it. While it's
75 * possible to create the object and set all its state in this one method, that
76 * increases the likelihood of problems with cycles. So it's better to use the
77 * minimum necessary information in [create] and do more of the work in
78 * [setState].
79 *
80 * The other way to do this is not creating a subclass, but by using a
81 * [ClosureRule] and giving it functions for how to create
82 * the address.
50 * 83 *
51 * addressToMap(a) => {"number" : a.number, "street" : a.street, 84 * addressToMap(a) => {"number" : a.number, "street" : a.street,
52 * "city" : a.city}; 85 * "city" : a.city};
53 * createAddress(Map m) => new Address.create(m["number"], m["street"]); 86 * createAddress(Map m) => new Address.create(m["number"], m["street"]);
54 * fillInAddress(Map m, Address a) => a.city = m["city"]; 87 * fillInAddress(Map m, Address a) => a.city = m["city"];
55 * var serialization = new Serialization() 88 * var serialization = new Serialization()
56 * ..addRule( 89 * ..addRule(
57 * new ClosureToMapRule(anAddress.runtimeType, 90 * new ClosureRule(anAddress.runtimeType,
58 * addressToMap, createAddress, fillInAddress); 91 * addressToMap, createAddress, fillInAddress);
59 * 92 *
60 * Note that there are three different functions provided. The addressToMap 93 * In this case we have created standalone functions rather than
61 * function takes the fields we want serialized from the Address and puts them 94 * methods in a subclass and we pass them to the constructor of
62 * into a map. The createAddress function creates a new address using a map like 95 * [ClosureRule]. In this case we've also had them use maps rather than
63 * the one returned by the first function. And the fillInAddress function fills 96 * lists for the state, but either would work as long as the rule is
64 * in any remaining state in the created object. 97 * consistent with the representation it uses. We pass it the runtimeType
98 * of the object, and functions equivalent to the methods on [CustomRule]
65 * 99 *
66 * At the moment, however, using this rule increases the probability of problems 100 * Constant Values
67 * with cycles. The problem is that before passing values to the user-supplied 101 * ===============
68 * functions it has to inflate any references to be the real objects. Since it
69 * doesn't know which ones the creation function uses it has to inflate all of
70 * them. For example, consider a Node class with parent, leftChild, and
71 * rightChild, and the parent field was final and set in the constructor. When
72 * we inflate all of the values we will end up with a cycle and can't
73 * de-serialize. If we know which fields are used by the constructor we can
74 * inflate only those, which is what BasicRule does. We expect to make a richer
75 * API for rules not using reflection, but there's a tension between providing
76 * the serialization process with enough information and making it more work
77 * to specify.
78 *
79 * There are cases where the constructor needs values that we can't easily get 102 * There are cases where the constructor needs values that we can't easily get
80 * the serialized object. For example, we may just want to pass null, or a 103 * from the serialized object. For example, we may just want to pass null, or a
81 * constant value. To support this, we can specify as constructor fields 104 * constant value. To support this, we can specify as constructor fields
82 * values that aren't field names. If any value isn't a String, it will be 105 * values that aren't field names. If any value isn't a String, it will be
83 * treated as a constant and passed unaltered to the constructor. 106 * treated as a constant and passed unaltered to the constructor.
84 * 107 *
85 * In some cases a non-constructor field should not be set using field 108 * In some cases a non-constructor field should not be set using field
86 * access or a setter, but should be done by calling a method. For example, it 109 * access or a setter, but should be done by calling a method. For example, it
87 * may not be possible to set a List field "foo", and you need to call an 110 * may not be possible to set a List field "foo", and you need to call an
88 * addFoo() method for each entry in the list. In these cases, if you are using 111 * addFoo() method for each entry in the list. In these cases, if you are using
89 * a BasicRule for the object you can call the setFieldWith() method. 112 * a BasicRule for the object you can call the setFieldWith() method.
90 * 113 *
91 * s..addRuleFor(fooHolderInstance).setFieldWith("foo", 114 * s..addRuleFor(fooHolderInstance).setFieldWith("foo",
92 * (parent, value) => for (var each in value) parent.addFoo(value)); 115 * (parent, value) => for (var each in value) parent.addFoo(value));
93 * 116 *
94 * Writing 117 * Writing
95 * ======= 118 * =======
96 * To write objects, we use the write() methods. There are two variations. 119 * To write objects, we use the write() method.
97 * 120 *
98 * String output = serialization.write(someObject); 121 * var output = serialization.write(someObject);
99 * List output = serialization.writeFlat(someObject);
100 * 122 *
101 * The first uses a representation in which objects are represented as maps 123 * By default this uses a representation in which objects are represented as
102 * keyed by field name, but in which references between objects have been 124 * maps keyed by field name, but in which references between objects have been
103 * converted into Reference objects. This is then encoded as a [JSON] string. 125 * converted into Reference objects. This is then encoded as a [json] string.
104 * 126 *
105 * The second representation holds all the objects as a List of simple types. 127 * We can write objects in different formats by passing a [Format] object to
106 * For practical use you may want to convert that to a [JSON] or other encoded 128 * the [write] method or by getting a [Writer] object. The available formats
107 * representation as well. 129 * include the default, a simple "flat" format that doesn't include field names,
130 * and a simple JSON format that produces output more suitable for talking to
131 * services that expect JSON in a predefined format. Examples of these are
108 * 132 *
109 * Both representations are primarily intended as proofs of concept for 133 * String output = serialization.write(address, new SimpleMapFormat());
110 * different types of representation, and we expect to generalize that to a 134 * List output = serialization.write(address, new SimpleFlatFormat());
111 * pluggable mechanism for different representations. 135 * Map output = serialization.write(address, new SimpleJSONFormat());
136 * Or, using a [Writer] explicitly
137 * var writer = serialization.newWriter(new SimpleFlatFormat());
138 * var output = writer.write(address);
139 *
140 * These representations are not yet considered stable.
112 * 141 *
113 * Reading 142 * Reading
114 * ======= 143 * =======
115 * To read objects, the corresponding methods are [read] and [readFlat]. 144 * To read objects, the corresponding [read] method can be used.
116 * 145 *
117 * List input = serialization.read(aString); 146 * Address input = serialization.read(aString);
118 * List input = serialization.readFlat(aList);
119 *
120 * There is also a convenience method for the case of reading a single object.
121 *
122 * Object result = serialization.readOne(aString);
123 * Object result = serialization.readOneFlat(aString);
124 * 147 *
125 * When reading, the serialization instance doing the reading must be configured 148 * When reading, the serialization instance doing the reading must be configured
126 * with compatible rules to the one doing the writing. It's possible for the 149 * with compatible rules to the one doing the writing. It's possible for the
127 * rules to be different, but they need to be able to read the same 150 * rules to be different, but they need to be able to read the same
128 * representation. For most practical purposes right now they should be the 151 * representation. For most practical purposes right now they should be the
129 * same. The simplest way to achieve this is by having the serialization 152 * same. The simplest way to achieve this is by having the serialization
130 * variable [selfDescribing] be true. In that case the rules themselves are also 153 * variable [selfDescribing] be true. In that case the rules themselves are also
131 * stored along with the serialized data, and can be read back on the receiving 154 * stored along with the serialized data, and can be read back on the receiving
132 * end. Note that this does not yet work for [ClosureToMapRule]. The 155 * end. Note that this may not work for all rules or all formats. The
133 * [selfDescribing] variable is true by default. 156 * [selfDescribing] variable is true by default, but the SimpleJSONFormat does
Jennifer Messerly 2013/01/11 02:21:53 SimpleJsonFormat? I think we do camel case (e.g.
Alan Knight 2013/01/11 19:18:11 Done.
157 * not support it, since the point is to provide a representation in a form
158 * other services might expect. Using CustomRule or ClosureRule also does not
159 * yet work with the [selfDescribing] variable.
134 * 160 *
161 * Named Objects
Jennifer Messerly 2013/01/11 02:21:53 like these new sections!
162 * =============
135 * When reading, some object references should not be serialized, but should be 163 * When reading, some object references should not be serialized, but should be
136 * connected up to other instances on the receiving side. A notable example of 164 * connected up to other instances on the receiving side. A notable example of
137 * this is when serialization rules have been stored. Instances of BasicRule 165 * this is when serialization rules have been stored. Instances of BasicRule
138 * take a [ClassMirror] in their constructor, and we cannot serialize those. So 166 * take a [ClassMirror] in their constructor, and we cannot serialize those. So
139 * when we read the rules, we must provide a Map<String, Object> which maps from 167 * when we read the rules, we must provide a Map<String, Object> which maps from
140 * the simple name of classes we are interested in to a [ClassMirror]. This can 168 * the simple name of classes we are interested in to a [ClassMirror]. This can
141 * be provided either in the [externalObjects] variable of the Serialization, 169 * be provided either in the [namedObjects] variable of the Serialization,
142 * or as an additional parameter to the reading methods. 170 * or as an additional parameter to the reading and writing methods on the
171 * [Reader] or [Writer] respectively.
143 * 172 *
144 * new Serialization() 173 * new Serialization()
145 * ..addRuleFor(new Person(), constructorFields: ["name"]) 174 * ..addRuleFor(new Person(), constructorFields: ["name"])
146 * ..externalObjects['Person'] = reflect(new Person()).type; 175 * ..namedObjects['Person'] = reflect(new Person()).type;
147 */ 176 */
148 library serialization; 177 library serialization;
149 178
150 import 'src/mirrors_helpers.dart'; 179 import 'src/mirrors_helpers.dart';
151 import 'src/serialization_helpers.dart'; 180 import 'src/serialization_helpers.dart';
152 import 'dart:async'; 181 import 'dart:async';
153 import 'dart:json' as json; 182 import 'dart:json' as json;
154 183
155 part 'src/reader_writer.dart'; 184 part 'src/reader_writer.dart';
156 part 'src/serialization_rule.dart'; 185 part 'src/serialization_rule.dart';
157 part 'src/basic_rule.dart'; 186 part 'src/basic_rule.dart';
187 part 'src/format.dart';
158 188
159 /** 189 /**
160 * This class defines a particular serialization scheme, in terms of 190 * This class defines a particular serialization scheme, in terms of
161 * [SerializationRule] instances, and supports reading and writing them. 191 * [SerializationRule] instances, and supports reading and writing them.
162 * See library comment for examples of usage. 192 * See library comment for examples of usage.
163 */ 193 */
164 class Serialization { 194 class Serialization {
165 195
166 /** 196 /**
167 * The serialization is controlled by the list of Serialization rules. These 197 * The serialization is controlled by the list of Serialization rules. These
(...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after
284 * handle most simple cases, but for adding an arbitrary rule, including 314 * handle most simple cases, but for adding an arbitrary rule, including
285 * a SerializationRule subclass which you have created, you can use this 315 * a SerializationRule subclass which you have created, you can use this
286 * method. 316 * method.
287 */ 317 */
288 void addRule(SerializationRule rule) { 318 void addRule(SerializationRule rule) {
289 rule.number = _rules.length; 319 rule.number = _rules.length;
290 _rules.add(rule); 320 _rules.add(rule);
291 } 321 }
292 322
293 /** 323 /**
294 * This is the basic method to write out an object graph rooted at 324 * This writes out an object graph rooted at [object] and returns the result.
295 * [object] and return the result. Right now this is hard-coded to return 325 * The [format] parameter determines the form of the result. The default
296 * a String from a custom [JSON] format, but that is likely to change to be 326 * format returns a String in [json] format.
297 * more pluggable in the near future.
298 */ 327 */
299 String write(Object object) { 328 write(Object object, [Format format]) {
300 return newWriter().write(object); 329 return newWriter(format).write(object);
301 } 330 }
302 331
303 /** 332 /**
304 * Return a new [Writer] object for this serialization. This is useful if you 333 * Return a new [Writer] object for this serialization. This is useful if you
305 * want to do something more complex with the writer than just returning 334 * want to do something more complex with the writer than just returning
306 * the final result. 335 * the final result.
307 */ 336 */
308 Writer newWriter() => new Writer(this); 337 Writer newWriter([Format format]) =>
309 338 new Writer(this, format);
310 /**
311 * Write out the tree in a custom flat format, returning a list containing
312 * only "simple" types: num, String, and bool.
313 */
314 List writeFlat(Object object) {
315 return newWriter().writeFlat(object);
316 }
317 339
318 /** 340 /**
319 * Read the serialized data from [input] and return the root object 341 * Read the serialized data from [input] and return the root object
320 * from the result. If there are objects that need to be resolved 342 * from the result. If there are objects that need to be resolved
321 * in the current context, they should be provided in [externals] as a 343 * in the current context, they should be provided in [externals] as a
322 * Map from names to values. In particular, in the current implementation 344 * Map from names to values. In particular, in the current implementation
323 * any class mirrors needed should be provided in [externals] using the 345 * any class mirrors needed should be provided in [externals] using the
324 * class name as a key. In addition to the [externals] map provided here, 346 * class name as a key. In addition to the [externals] map provided here,
325 * values will be looked up in the [externalObjects] map. 347 * values will be looked up in the [externalObjects] map.
326 */ 348 */
327 read(String input, [Map externals = const {}]) { 349 read(String input, [Map externals = const {}]) {
328 return newReader().read(input, externals); 350 return newReader().read(input, externals);
329 } 351 }
330 352
331 /** 353 /**
332 * Return a new [Reader] object for this serialization. This is useful if 354 * Return a new [Reader] object for this serialization. This is useful if
333 * you want to do something more complex with the reader than just returning 355 * you want to do something more complex with the reader than just returning
334 * the final result. 356 * the final result.
335 */ 357 */
336 Reader newReader() => new Reader(this); 358 Reader newReader([Format format]) => new Reader(this, format);
337 359
338 /** 360 /**
339 * Return the list of SerializationRule that apply to [object]. For 361 * Return the list of SerializationRule that apply to [object]. For
340 * internal use, but public because it's used in testing. 362 * internal use, but public because it's used in testing.
341 */ 363 */
342 List<SerializationRule> rulesFor(object, Writer w) { 364 Iterable<SerializationRule> rulesFor(object, Writer w) {
343 // This has a couple of edge cases. 365 // This has a couple of edge cases.
344 // 1) The owning object may have indicated we should use a different 366 // 1) The owning object may have indicated we should use a different
345 // rule than the default. 367 // rule than the default.
346 // 2) We may not have a rule, in which case we lazily create a BasicRule. 368 // 2) We may not have a rule, in which case we lazily create a BasicRule.
347 // 3) Rules are allowed to say mustBePrimary, meaning that they can be used 369 // 3) Rules are allowed to say mustBePrimary, meaning that they can be used
348 // iff no other rule was chosen first. 370 // iff no other rule was chosen first.
349 // TODO(alanknight): Can the mustBePrimary mechanism be removed or changed. 371 // TODO(alanknight): Can the mustBePrimary mechanism be removed or changed.
350 // It adds an order dependency to the rules, and is messy. Reconsider in the 372 // It adds an order dependency to the rules, and is messy. Reconsider in the
351 // light of a more general mechanism for multiple rules per object. 373 // light of a more general mechanism for multiple rules per object.
352 // TODO(alanknight): Finding which rules apply seems likely to be a 374 // TODO(alanknight): Finding which rules apply seems likely to be a
353 // bottleneck, particularly with the current reflective implementation. 375 // bottleneck, particularly with the current reflective implementation.
354 // Consider how to improve it. e.g. cache the list of rules by class. But 376 // Consider how to improve it. e.g. cache the list of rules by class. But
355 // be careful of issues like rules which have arbitrary predicates. Or 377 // be careful of issues like rules which have arbitrary predicates. Or
356 // consider having the arbitrary predicates be secondary to an initial 378 // consider having the arbitrary predicates be secondary to an initial
357 // class-based lookup mechanism. 379 // class-based lookup mechanism.
358 var target, candidateRules; 380 var target, candidateRules;
359 if (object is DesignatedRuleForObject) { 381 if (object is DesignatedRuleForObject) {
360 target = object.target; 382 target = object.target;
361 candidateRules = object.possibleRules(_rules); 383 candidateRules = object.possibleRules(_rules);
362 } else { 384 } else {
363 target = object; 385 target = object;
364 candidateRules = _rules; 386 candidateRules = _rules;
365 } 387 }
366 List applicable = 388 Iterable applicable = candidateRules.where(
367 candidateRules.where((each) => each.appliesTo(target, w)).toList(); 389 (each) => each.appliesTo(target, w));
368 390
369 if (applicable.isEmpty) { 391 if (applicable.isEmpty) {
370 return [addRuleFor(target)]; 392 return [addRuleFor(target)];
371 } 393 }
372 394
373 if (applicable.length == 1) return applicable; 395 if (applicable.length == 1) return applicable;
374 var first = applicable[0]; 396 var first = applicable.first;
375 var finalRules = applicable.where( 397 var finalRules = applicable.where(
376 (x) => !x.mustBePrimary || (x == first)).toList(); 398 (x) => !x.mustBePrimary || (x == first));
377 399
378 if (finalRules.isEmpty) throw new SerializationException( 400 if (finalRules.isEmpty) throw new SerializationException(
379 'No valid rule found for object $object'); 401 'No valid rule found for object $object');
380 return finalRules; 402 return finalRules;
381 } 403 }
382 404
383 /** 405 /**
384 * Create a Serialization for serializing SerializationRules. This is used 406 * Create a Serialization for serializing SerializationRules. This is used
385 * to save the rules in a self-describing format along with the data. 407 * to save the rules in a self-describing format along with the data.
386 * If there are new rule classes created, they will need to be described 408 * If there are new rule classes created, they will need to be described
387 * here. 409 * here.
388 */ 410 */
389 Serialization _ruleSerialization() { 411 Serialization ruleSerialization() {
390 // TODO(alanknight): There's an extensibility issue here with new rules. 412 // TODO(alanknight): There's an extensibility issue here with new rules.
391 // TODO(alanknight): How to handle rules with closures? They have to 413 // TODO(alanknight): How to handle rules with closures? They have to
392 // exist on the other side, but we might be able to hook them up by name, 414 // exist on the other side, but we might be able to hook them up by name,
393 // or we might just be able to validate that they're correctly set up 415 // or we might just be able to validate that they're correctly set up
394 // on the other side. 416 // on the other side.
395 417
396 // Make some bogus rule instances so we have something to feed rule creation 418 // Make some bogus rule instances so we have something to feed rule creation
397 // and get their types. If only we had class literals implemented... 419 // and get their types. If only we had class literals implemented...
398 var basicRule = new BasicRule(reflect(null).type, '', [], [], []); 420 var basicRule = new BasicRule(reflect(null).type, '', [], [], []);
399 421
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
431 } 453 }
432 } 454 }
433 455
434 /** 456 /**
435 * An exception class for errors during serialization. 457 * An exception class for errors during serialization.
436 */ 458 */
437 class SerializationException implements Exception { 459 class SerializationException implements Exception {
438 final String message; 460 final String message;
439 const SerializationException([this.message]); 461 const SerializationException([this.message]);
440 } 462 }
OLDNEW
« no previous file with comments | « no previous file | pkg/serialization/lib/src/basic_rule.dart » ('j') | pkg/serialization/lib/src/format.dart » ('J')

Powered by Google App Engine
This is Rietveld 408576698