Skip to content

Instantly share code, notes, and snippets.

@mraleph
Last active April 1, 2022 06:32
Show Gist options
  • Save mraleph/7dc5b77b6a77d3d6bb88709945dbd02c to your computer and use it in GitHub Desktop.
Save mraleph/7dc5b77b6a77d3d6bb88709945dbd02c to your computer and use it in GitHub Desktop.
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
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