Added serialization of references and cycles
This commit is contained in:
parent
466193f88f
commit
fc66e2a70f
|
@ -3,5 +3,19 @@ module SpaceTac.Game {
|
|||
|
||||
// Base class for serializable objects
|
||||
export class Serializable {
|
||||
static _next_sid: number = 0;
|
||||
|
||||
private _sid: string;
|
||||
|
||||
constructor() {
|
||||
this._sid = null;
|
||||
}
|
||||
|
||||
getSerializeId(): string {
|
||||
if (this._sid === null) {
|
||||
this._sid = (Serializable._next_sid++).toString();
|
||||
}
|
||||
return this._sid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,17 @@ module SpaceTac.Game {
|
|||
|
||||
// Serializer to cascade through Serializable objects
|
||||
export class Serializer {
|
||||
// Mapping of IDs to objects
|
||||
refs: {[index: string]: any};
|
||||
|
||||
// Serializable classes
|
||||
classes: {[index: string]: typeof Serializable};
|
||||
|
||||
constructor() {
|
||||
this.refs = {};
|
||||
this.classes = this.collectSerializableClasses();
|
||||
}
|
||||
|
||||
// List all classes that implement "Serializable", with their full path in SpaceTac.Game namespace
|
||||
collectSerializableClasses(container: any = null, path: string = ""): {[index: string]: typeof Serializable} {
|
||||
if (container) {
|
||||
|
@ -26,10 +37,9 @@ module SpaceTac.Game {
|
|||
|
||||
// Get the full path in SpaceTac namespace, of a serializable object
|
||||
getClassPath(obj: Serializable): string {
|
||||
var classes = this.collectSerializableClasses();
|
||||
for (var class_path in classes) {
|
||||
if (classes.hasOwnProperty(class_path)) {
|
||||
var class_obj = classes[class_path];
|
||||
for (var class_path in this.classes) {
|
||||
if (this.classes.hasOwnProperty(class_path)) {
|
||||
var class_obj = this.classes[class_path];
|
||||
if (class_obj.prototype === obj.constructor.prototype) {
|
||||
return class_path;
|
||||
}
|
||||
|
@ -40,19 +50,39 @@ module SpaceTac.Game {
|
|||
|
||||
// Serialize an object to a string
|
||||
serialize(obj: Serializable): string {
|
||||
var data = this.toData(obj);
|
||||
this.refs = {};
|
||||
var data = {
|
||||
refs: this.refs,
|
||||
root: this.toData(obj)
|
||||
};
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
// Load an object from a serialized string
|
||||
unserialize(sdata: string): Serializable {
|
||||
var data = JSON.parse(sdata);
|
||||
var result = this.fromData(data);
|
||||
return result;
|
||||
this.refs = data.refs;
|
||||
return this.fromData(data.root);
|
||||
}
|
||||
|
||||
private toData(obj: Serializable): any {
|
||||
var sid = obj.getSerializeId();
|
||||
var cached = this.refs[sid];
|
||||
var data = {
|
||||
_s: "r",
|
||||
_i: sid
|
||||
};
|
||||
if (typeof cached !== "undefined") {
|
||||
return data;
|
||||
}
|
||||
|
||||
var fields = {};
|
||||
this.refs[sid] = {
|
||||
_s: "o",
|
||||
path: this.getClassPath(obj),
|
||||
fields: fields
|
||||
};
|
||||
|
||||
for (var field_name in obj) {
|
||||
if (obj.hasOwnProperty(field_name)) {
|
||||
var field_value = obj[field_name];
|
||||
|
@ -73,34 +103,37 @@ module SpaceTac.Game {
|
|||
}
|
||||
}
|
||||
|
||||
var data = {
|
||||
_s: "o",
|
||||
path: this.getClassPath(obj),
|
||||
fields: fields
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
private fromData(data: any): Serializable {
|
||||
var class_info = this.collectSerializableClasses()[data.path];
|
||||
var obj = Object.create(class_info.prototype);
|
||||
for (var field_name in data.fields) {
|
||||
if (data.fields.hasOwnProperty(field_name)) {
|
||||
var field_value = data.fields[field_name];
|
||||
if (typeof field_value === "object" && field_value._s === "o") {
|
||||
obj[field_name] = this.fromData(field_value);
|
||||
} else if (typeof field_value === "object" && field_value._s === "a") {
|
||||
var items: Serializable[] = [];
|
||||
field_value.items.forEach((item: any) => {
|
||||
items.push(this.fromData(item));
|
||||
});
|
||||
obj[field_name] = items;
|
||||
} else {
|
||||
obj[field_name] = field_value;
|
||||
var sid = data._i;
|
||||
var cached = this.refs[sid];
|
||||
|
||||
if (cached._s === "o") {
|
||||
var class_info = this.classes[cached.path];
|
||||
var obj = Object.create(class_info.prototype);
|
||||
this.refs[sid] = obj;
|
||||
for (var field_name in cached.fields) {
|
||||
if (cached.fields.hasOwnProperty(field_name)) {
|
||||
var field_value = cached.fields[field_name];
|
||||
if (field_value !== null && typeof field_value === "object" && field_value._s === "r") {
|
||||
obj[field_name] = this.fromData(field_value);
|
||||
} else if (field_value !== null && typeof field_value === "object" && field_value._s === "a") {
|
||||
var items: Serializable[] = [];
|
||||
field_value.items.forEach((item: any) => {
|
||||
items.push(this.fromData(item));
|
||||
});
|
||||
obj[field_name] = items;
|
||||
} else {
|
||||
obj[field_name] = field_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return cached;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,20 @@
|
|||
module SpaceTac.Game.Specs {
|
||||
"use strict";
|
||||
|
||||
export class SerializableTestObj3 extends Serializable {
|
||||
a: boolean;
|
||||
b: SerializableTestObj1;
|
||||
}
|
||||
|
||||
export class SerializableTestObj2 extends Serializable {
|
||||
a: string;
|
||||
|
||||
b: SerializableTestObj3;
|
||||
|
||||
constructor(a: string = "test") {
|
||||
super();
|
||||
this.a = a;
|
||||
this.b = null;
|
||||
}
|
||||
|
||||
prepend(prefix: string): string {
|
||||
|
@ -39,6 +47,7 @@ module SpaceTac.Game.Specs {
|
|||
|
||||
expect(classes["SpaceTac.Game.Specs.SerializableTestObj1"]).toBe(SerializableTestObj1);
|
||||
expect(classes["SpaceTac.Game.Specs.SerializableTestObj2"]).toBe(SerializableTestObj2);
|
||||
expect(classes["SpaceTac.Game.Specs.SerializableTestObj3"]).toBe(SerializableTestObj3);
|
||||
expect(classes["SpaceTac.Game.Range"]).toBe(Range);
|
||||
expect(classes["SpaceTac.Game.Equipments.GatlingGun"]).toBe(Equipments.GatlingGun);
|
||||
});
|
||||
|
@ -47,6 +56,7 @@ module SpaceTac.Game.Specs {
|
|||
var serializer = new Serializer();
|
||||
|
||||
expect(serializer.getClassPath(new SerializableTestObj1())).toBe("SpaceTac.Game.Specs.SerializableTestObj1");
|
||||
expect(serializer.getClassPath(new Range(0, 1))).toBe("SpaceTac.Game.Range");
|
||||
});
|
||||
|
||||
it("serializes and deserializes simple typescript objects", () => {
|
||||
|
@ -72,5 +82,35 @@ module SpaceTac.Game.Specs {
|
|||
expect((<SerializableTestObj1>loaded).b.prepend("this is a ")).toEqual("this is a test");
|
||||
expect((<SerializableTestObj1>loaded).c[1].prepend("this is a ")).toEqual("this is a third test");
|
||||
});
|
||||
|
||||
it("does not create copies of same object", () => {
|
||||
var serializer = new Serializer();
|
||||
var obj = new SerializableTestObj1(8, new SerializableTestObj2("test"));
|
||||
obj.c.push(obj.b);
|
||||
|
||||
var dumped = serializer.serialize(obj);
|
||||
var loaded = serializer.unserialize(dumped);
|
||||
|
||||
expect(loaded).toEqual(obj);
|
||||
expect((<SerializableTestObj1>loaded).b).toBe((<SerializableTestObj1>loaded).c[0]);
|
||||
});
|
||||
|
||||
it("handles reference cycles", () => {
|
||||
var serializer = new Serializer();
|
||||
var obj3 = new SerializableTestObj3();
|
||||
obj3.a = true;
|
||||
var obj2 = new SerializableTestObj2("test");
|
||||
var obj1 = new SerializableTestObj1(8, obj2);
|
||||
|
||||
obj3.b = obj1;
|
||||
obj2.b = obj3;
|
||||
|
||||
var dumped = serializer.serialize(obj1);
|
||||
var loaded = serializer.unserialize(dumped);
|
||||
|
||||
expect(loaded).toEqual(obj1);
|
||||
expect((<SerializableTestObj1>loaded).b.b.a).toBe(true);
|
||||
expect((<SerializableTestObj1>loaded).b.b.b).toBe(loaded);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue