Skip to content

Instantly share code, notes, and snippets.

@mraleph mraleph/decode.dart
Last active Jan 28, 2019

Embed
What would you like to do?
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';
}
}
}
@rootext

This comment has been minimized.

Copy link

rootext commented Jan 28, 2019

@mraleph, Could you please license this gist? I gonna use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.