Skip to content

Instantly share code, notes, and snippets.

@tombentley
Forked from gavinking/serialization.ceylon
Last active August 29, 2015 14:11
Show Gist options
  • Save tombentley/888585d07a0a03a38549 to your computer and use it in GitHub Desktop.
Save tombentley/888585d07a0a03a38549 to your computer and use it in GitHub Desktop.

TO DO

  • SL = serialization library.
  • SC = SerializationContext
  • DC = DeserializationContext

SerializationContext.getReference()

Add method SerializationContext.getReference<Instance>(Instance instance) to get the SerializableReference of an already registered instance:

  • Done, but the definition of == for Integer and Float means we can't use those as instances, which seems wrong.

Done: Review tabular having done this: It should be simpler

"Recursive" serialization.

SerializableReference.serialize() is the problem here. It originally returned a Deconstructed, which fits the recursive case quite well, because then the SL has the job of picking over/iterating the values in the Deconstructed and emitting them to the format (e.g. copying them into a JSON Object, or whatever).

But I changed the API because:

  1. It seemed suboptimal to me that we would (effectively) have to instantiate a few maps for every instance we needed to persist, just to buffer the state of the instance. Far better, I thought, for instance to push its state into a Deconstructed provided by the SL than the SL have to pull it out of something that has to be allocated fresh each time.
  2. But doing that made the API more SAX-like, and really the SL needed notification of each ClassDeclaration we encountered on the way up the inheritance hierarchy. Hence the value parameter became a function parameter.

Member class instances

Fix the serialization of member classes

Generics

How we handle generics is fucked up: tabular uses Deconstructor.putTypeArgument(), but not Deconstructed.getTypeArgument(). Instead it looks at the type of the argument as it was persisted in order to figure out the ClassModel it needs to instantiate. Having instantiated the right thing there's no need for the SL to call Deconstructed.getTypeArgument() when setting the state.

JSON-based SL

Problem: ambiguous attribute names

What he has doesn't handle class hierarchies -- we've no way to associate the names of attributes with the class declaration they're from. This is a problem because there canbe multiple attributes with the same name in a class hierarchy.

class One(String s) {
    shared String captureS => s;
}
class Two(String s) extends One("1") {
    shared String captureAnotherS => s;
}

That sort of thing can't be handled with a format like this:

{ "$" -> 1;
    "x" : 0.0;
    "y": 1.0
}

Because it doesn't make clear that it's Point's x. We could use dot notation:

{ "$": 1;
    "example::Point.x" : 0.0;
    "example::Point.y": 1.0
}

But that makes the serialized form a lot bigger because of all the duplication of "Point." etc. Trying to shorten the keys makes the format harder to read. I suppose we could use the unambiguous format "example::Point.x" only when there is an ambiguity, and otherwise just use "x", but that requires another whole load of maps because we don't know about the ambiguity until we encounter it during serialization.

I suppose another option would be to use an annotation, to control the name each attribute gets mapped to. By default it would be the simple name of the attribute. We would error when we encountered an ambiguity:

class One(s) {
    jsonKey("One::s")
    String s;
    shared String captureS => s;
}
class Two(s) extends One("1") {
    jsonKey("Two::s")
    String s;
    shared String captureAnotherS => s;
}

Problem: The type of the instances

Gavin's idea doesn't persist the name of the type of the instance. That's not necessarily a problem if the stream isn't supposed to be self-describing (i.e. we know out-of-band that the stream is one of Points), but I suspect it is a problem really.

{ "$": 1; 
    "$type": "example.Point", 
    ... 
}

this is fine, but we end up needing a parser for class type names (with generics). Also that's more redundancy of class names.

Again we can reduce the redundancy of the class names by using a mapping of short name to full name, but that harms readability. Or we compress the stream in its serialized form (gzip etc).

Problem: Collections

This SL will almost certainly need to support Array, List, Tuple, ArraySequence etc. In particular, which of those gets to map to JSON's Array type?

{"$":0,"$type":"jsonsl::NamedPoint","name":"Origin","x":0.0,"y":0.0}
{"$":1,"$type":"jsonsl::NamedPoint","name":"(1,1)","x":1.0,"y":1.0}
{"$":2,"$type":"jsonsl::Line","end":1,"start":0}
{"$":3,"$type":"jsonsl::Disk","centre":0,"radius":1.0}
import ceylon.language.serialization {
serialization,
Deconstructor
}
import ceylon.language.meta.model {
Type
}
import ceylon.language.meta {
type
}
import ceylon.language.meta.declaration {
ValueDeclaration,
TypeParameter
}
import ceylon.json {
Object,
nil
}
"A point in ℝ^2, defined in Cartesian coordinates."
serializable class Point(shared Float x, shared Float y) {}
"A named point in ℝ^2, defined in Cartesian coordinates."
serializable class NamedPoint(Float x, Float y, shared String name) extends Point(x,y){}
"A line segment between two points."
serializable class Line(shared Point start, shared Point end) {}
"A disk in ℝ^2"
serializable class Disk(shared Point centre, shared Float radius) {}
variable Integer generator = 0;
shared void test() {
value context = serialization();
value origin = NamedPoint(0.0, 0.0, "Origin");
value x1y1 = NamedPoint(1.0, 1.0, "(1,1)");
value items = [
origin,
x1y1,
Line(origin, x1y1),
Disk(origin, 1.0)
];
for (item in items) {
value id = generator++;
value ref = context.reference(id, item);
}
for (item in items) {
assert(exists ref = context.getReference(item),
is Integer id = ref.id);
value result = Object { "$"->id };
ref.serialize(function (classModel) {
result.put("$type", type(ref.instance()).string);
return object satisfies Deconstructor {
shared actual void putElement<Instance>(Integer index, Instance referenced) {}
shared actual void putOuterInstance<Instance>(Instance outerInstance) {}
shared actual void putTypeArgument(TypeParameter typeParameter, Type<Anything> typeArgument) {}
shared actual void putValue<Instance>(ValueDeclaration attribute, Instance referenced) {
String attName = attribute.name;
switch (ref = referenced of Anything)
case (is String|Integer|Float) {
result.put(attName, ref);
}
case (null) {
result.put(attName, nil);
}
else {
if (is Identifiable ref) { //a reference to another object in stream
assert(exists referred = context.getReference(ref),
is Integer referredId = referred.id);
result.put(attName,
referredId);
}
else { //an "embedded" object
value obj = Object();
//TODO: recurse its properties!
result.put(attName, obj);
}
}
}
};
}
);
print (result);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment