library mirrors.src.decode; | |
import 'dart:mirrors' as mirrors; | |
import 'dart:convert'; | |
/// Create an object of the given type [t] from the given JSON string. | |
decode(String json, Type t) { | |
// Get deserialization descriptor for the given type. | |
// This descriptor describes how to handle the type and all its fields. | |
final TypeDesc desc = getDesc(t); | |
return parseJsonWithListener(json, new HydratingListener(desc)); | |
} | |
final Map<Type, TypeDesc> typeCache = new Map<Type, TypeDesc>(); | |
TypeDesc getDesc(Type type) { | |
return typeCache[type] ??= buildTypeDesc(type); | |
} | |
/// Object construction closure. | |
typedef dynamic Constructor(); | |
/// Field setter closure. | |
typedef void Setter(InstanceMirror obj, dynamic value); | |
/// Deserialization descriptor built from a [Type] or a [mirrors.TypeMirror]. | |
/// Describes how to instantiate an object and how to fill it with properties. | |
class TypeDesc { | |
/// Tag describing what kind of object this is: expected to be | |
/// either [tagArray] or [tagObject]. Other tags ([tagString], [tagNumber], | |
/// [tagBoolean]) are not used for [TypeDesc]s. | |
final int tag; | |
/// Constructor closure for this object. | |
final Constructor ctor; | |
/// Either map from property names to property descriptors for objects or | |
/// element [TypeDesc] for lists. | |
final /* Map<String, Property> | TypeDesc */ properties; | |
/// Mode determining whether this [TypeDesc] is trying to adapt to | |
/// a particular order of properties in the incoming JSON: | |
/// the expectation here is that if JSON contains several serialized objects | |
/// of the same type they will all have the same order of properties inside. | |
int mode = modeAdapt; | |
/// A sequence of triplets (property name, hydrate callback, assign callback) | |
/// recorded while trying to adapt to the property order in the incoming JSON. | |
/// If [mode] is set to [modeFollow] then [HydratingListener] will attempt | |
/// to follow the trail. | |
List<dynamic> propertyTrail = []; | |
TypeDesc(this.tag, this.ctor, this.properties); | |
static const int tagNone = 0; | |
static const int tagObject = 1; | |
static const int tagArray = 2; | |
static const int tagString = 3; | |
static const int tagNumber = 4; | |
static const int tagBool = 5; | |
static const int modeAdapt = 0; | |
static const int modeFollow = 1; | |
static const int modeNone = 2; | |
} | |
/// Deserialization descriptor built from a [mirrors.VariableMirror]. | |
/// Describes what kind of value is expected for this property and how | |
/// how to store it in the object. | |
class Property { | |
/// Either [TypeDesc] if the property is a [List] or an object or | |
/// [tagString], [tagBool], [tagNumber] if the property has primitive type. | |
final /* int | TypeDesc */ desc; | |
/// Setter callback. | |
final Setter assign; | |
Property(this.desc, this.assign); | |
} | |
/// --------------------------------------------------------------------------- | |
/// Helpers to build [TypeDesc] out of [Type]. | |
/* int | TypeDesc */ buildTypeDesc(Type type) { | |
return buildTypeDescFromMirror(mirrors.reflectType(type)); | |
} | |
/* int | TypeDesc */ buildTypeDescFromMirror(mirrors.TypeMirror typeMirror) { | |
if (typeMirror.hasReflectedType) { | |
switch (typeMirror.reflectedType) { | |
case String: | |
return TypeDesc.tagString; | |
case num: | |
return TypeDesc.tagNumber; | |
case bool: | |
return TypeDesc.tagBool; | |
} | |
} | |
if (typeMirror.originalDeclaration == listMirror) { | |
final ctor = () => []; | |
final elementDesc = buildTypeDescFromMirror( | |
typeMirror.typeArguments.first); | |
return new TypeDesc(TypeDesc.tagArray, ctor, elementDesc); | |
} else if (typeMirror.hasReflectedType) { | |
return buildTypeDescFromType(typeMirror.reflectedType); | |
} else { | |
throw "unsupported type ${typeMirror}"; | |
} | |
} | |
TypeDesc buildTypeDescFromType(Type type) { | |
TypeDesc act = typeCache[type]; | |
if (act != null) return act; | |
final klass = mirrors.reflectClass(type); | |
typeCache[type] = act = new TypeDesc(TypeDesc.tagObject, | |
() => klass.newInstance(const Symbol(""), const []), | |
new Map<String, Property>()); | |
final props = act.properties; | |
klass.declarations.forEach((sym, decl) { | |
if (decl is mirrors.VariableMirror && !decl.isStatic) { | |
final fieldNameSym = decl.simpleName; | |
final fieldNameStr = name(fieldNameSym); | |
final setField = (obj, value) { obj.setField(fieldNameSym, value); }; | |
// final setField = mirrors.$evaluate('(obj, value) { obj.reflectee.${fieldNameStr} = value; }'); | |
props[fieldNameStr] = new Property(buildTypeDescFromMirror(decl.type), setField); | |
} | |
}); | |
return act; | |
} | |
name(sym) => mirrors.MirrorSystem.getName(sym); | |
final listMirror = mirrors.reflectClass(List); | |
/// --------------------------------------------------------------------------- | |
/// [JsonListener] implementation which uses [TypeDesc] deserialization | |
/// descriptors to build objects directly from JSON. | |
class HydratingListener extends JsonListener { | |
/// Currently active [TypeDesc]. | |
TypeDesc desc; | |
/// Expected type for a property value. | |
int expected = TypeDesc.tagNone; | |
/// Current object that will be filled with properties. | |
var object; | |
/// Either current property of index into [desc.propertyTrail] array. | |
var /* int | Property */ prop; | |
/// Parsing stack, when we start parsing a nested object we push a triple of | |
/// (prop, desc, object) to the stack. | |
final List stack = []; | |
/// Last parsed value. | |
var value; | |
get result => value; | |
HydratingListener(TypeDesc this.desc) { expect(desc); } | |
@override | |
void handleString(String value) { | |
this.value = value; | |
} | |
@override | |
void handleNumber(num value) { | |
this.value = value; | |
} | |
@override | |
void handleBool(bool value) { | |
this.value = value; | |
} | |
@override | |
void handleNull() { | |
if (expected == TypeDesc.tagObject || expected == TypeDesc.tagArray) { | |
// We were expecting an object or a list, but got null - which means | |
// there is no need to create a new instance and we have no properties | |
// to deserialize. | |
leaveObject(); | |
expected = TypeDesc.tagNone; | |
} | |
this.value = null; | |
} | |
@override | |
void beginObject() { | |
checkExpected(TypeDesc.tagObject); | |
if (desc.mode == TypeDesc.modeFollow) { | |
prop = 0; | |
} | |
object = (desc.ctor)(); | |
} | |
@override | |
void propertyName() { | |
if (desc.mode == TypeDesc.modeNone || desc.mode == TypeDesc.modeAdapt) { | |
// This is either the first time we encountered an object with such | |
// [TypeDesc], which means we are currently recording the [propertyTrail] | |
// or we have already failed to follow the [propertyTrail] and have fallen | |
// back to simple dictionary based property lookups. | |
final p = desc.properties[value]; | |
if (p == null) { | |
throw "Unexpected property ${name}, only expect: ${desc.properties.keys | |
.join(', ')}"; | |
} | |
if (desc.mode == TypeDesc.modeAdapt) { | |
desc.propertyTrail.add(value); | |
desc.propertyTrail.add(p.desc); | |
desc.propertyTrail.add(p.assign); | |
} | |
prop = p; | |
expect(p.desc); | |
} else { | |
// We are trying to follow the trail. | |
final name = desc.propertyTrail[prop++]; | |
if (name != value) { | |
// We failed to follow the trail. Fall back to the simple dictionary | |
// based lookup. | |
desc.mode = TypeDesc.modeNone; | |
desc.propertyTrail = null; | |
return propertyName(); | |
} | |
// We are still on the trail. | |
final propDesc = desc.propertyTrail[prop++]; | |
expect(propDesc); | |
} | |
} | |
@override | |
void propertyValue() { | |
// First check if [value] matches what we expect. | |
if (expected != TypeDesc.tagNone && !isOk(expected, value)) { | |
throw "unexpected value: got ${value} expected ${tag2string(expected)}"; | |
} | |
// If we are on the trail - then get setter callback from the | |
// [propertyTrail]. | |
if (prop is int) { | |
final assign = desc.propertyTrail[prop++]; | |
assign(this.object, value); | |
} else { | |
// Otherwise [prop] is [Property]. | |
prop.assign(this.object, value); | |
} | |
} | |
@override | |
void endObject() { | |
value = object.reflectee; | |
// If we finished reading the first object with such [TypeDesc]. | |
// Switch it from recording the [propertyTrail] to following it. | |
if (desc.mode == TypeDesc.modeAdapt) { | |
desc.mode = TypeDesc.modeFollow; | |
desc.propertyTrail = desc.propertyTrail.toList(growable: false); | |
} | |
// Done with this object. | |
leaveObject(); | |
} | |
@override | |
void beginArray() { | |
checkExpected(TypeDesc.tagArray); | |
object = (desc.ctor)(); | |
expect(desc.properties); | |
} | |
@override | |
void arrayElement() { | |
object.add(value); | |
expect(desc.properties); | |
} | |
@override | |
void endArray() { | |
// We always expect one more element to follow. Discard it here. | |
if (object == null) leaveObject(); | |
value = object; | |
expected = TypeDesc.tagNone; | |
leaveObject(); | |
} | |
/// Expect the next value (either property value or element value) to | |
/// match the given tag or [TypeDesc]. | |
void expect(/* int | TypeDesc */ td) { | |
if (td is int) { | |
// Primitive value expected. | |
expected = td; | |
return; | |
} | |
// Expecting nested object. Save current state. | |
stack.add(prop); | |
stack.add(desc); | |
stack.add(object); | |
// Enter nested object. | |
expected = td.tag; | |
desc = td; | |
object = null; | |
} | |
/// Leave nested object. | |
void leaveObject() { | |
final tos = stack.length; | |
if (tos >= 2) { | |
object = stack[tos - 1]; | |
desc = stack[tos - 2]; | |
prop = stack[tos - 3]; | |
stack.length -= 3; | |
} | |
} | |
void checkExpected(int tag) { | |
if (expected != tag) | |
throw "unexpected ${tag2string(tag)}, expecting ${tag2string(expected)}"; | |
expected = TypeDesc.tagNone; | |
} | |
bool isOk(tag, value) { | |
if (tag == TypeDesc.tagString) { | |
return value is String; | |
} else if (tag == TypeDesc.tagNumber) { | |
return value is num; | |
} else if (tag == TypeDesc.tagBool) { | |
return value is bool; | |
} else if (tag == TypeDesc.tagObject || tag == TypeDesc.tagArray) { | |
return false; | |
} else { | |
throw "unexpected tag: ${tag2string(tag)}"; | |
} | |
} | |
static String tag2string(int tag) { | |
switch (tag) { | |
case TypeDesc.tagNone: | |
return 'none'; | |
case TypeDesc.tagObject: | |
return 'object'; | |
case TypeDesc.tagArray: | |
return 'array'; | |
case TypeDesc.tagString: | |
return 'string'; | |
case TypeDesc.tagNumber: | |
return 'number'; | |
case TypeDesc.tagBool: | |
return 'bool'; | |
default: | |
return 'unknown'; | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
@mraleph, Could you please license this gist? I gonna use it.