Last active
April 1, 2022 06:32
-
-
Save mraleph/7dc5b77b6a77d3d6bb88709945dbd02c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
@mraleph, Could you please license this gist? I gonna use it.