Permalink
Browse files

Encapsulate serialization and deserialization

Previously, concerns about how key names
for given hashes returned from the server were
spread across several different objects.

Now, the serialization and materialization
(converting an opaque data hash returned from
the server into meaningful results for Ember Data)
are handled by the DS.Serializer object.

Instead of jamming this responsibility into the
adapter, like so many clowns in a clown car, the
adapter is now solely responsible for deciding
which records should be committed, loading
records, and notifying the store of changes in
to record state.
  • Loading branch information...
1 parent 91ff6f3 commit 3f134e6b31f1735cda688e03170d0fd3d8cadbd9 tomhuda committed Jul 13, 2012
@@ -66,6 +66,8 @@ DS.Adapter = Ember.Object.extend({
*/
find: null,
+ serializer: DS.Serializer.create(),
+
/**
If the globally unique IDs for your records should be generated on the client,
implement the `generateIdForRecord()` method. This method will be invoked
@@ -87,49 +89,18 @@ DS.Adapter = Ember.Object.extend({
*/
generateIdForRecord: null,
- extractId: function(type, hash) {
- return hash.id;
- },
-
materialize: function(record, hash) {
- this.materializeId(record, hash);
- this.materializeAttributes(record, hash);
-
- get(record.constructor, 'associationsByName').forEach(function(name, meta) {
- if (meta.kind === 'hasMany') {
- this.materializeHasMany(record, hash, name);
- } else if (meta.kind === 'belongsTo') {
- this.materializeBelongsTo(record, hash, name);
- }
- }, this);
- },
-
- materializeId: function(record, hash) {
- record.materializeId(this.extractId(record.constructor, hash));
- },
-
- materializeAttributes: function(record, hash) {
- record.eachAttribute(function(name, attribute) {
- this.materializeAttribute(record, hash, name);
- }, this);
- },
-
- materializeAttribute: function(record, hash, name) {
- record.materializeAttribute(name, hash[name]);
- },
-
- materializeHasMany: function(record, hash, name) {
- record.materializeHasMany(name, hash[name]);
- },
-
- materializeBelongsTo: function(record, hash, name) {
- record.materializeBelongsTo(name, hash[name]);
+ get(this, 'serializer').materializeFromJSON(record, hash);
},
toJSON: function(record, options) {
return get(this, 'serializer').toJSON(record, options);
},
+ extractId: function(type, hash) {
+ return get(this, 'serializer').extractId(type, hash);
+ },
+
shouldCommit: function(record, relationships) {
return true;
},
@@ -7,6 +7,15 @@ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
var serializer = DS.Serializer.create({
primaryKey: function(record) {
return 'id';
+ },
+
+ extractBelongsTo: function(record, hash, relationship) {
+ return hash[relationship.key + "_id"];
+ },
+
+ extractAttribute: function(type, hash, attributeName) {
+ var underscoredName = Ember.String.decamelize(attributeName);
+ return hash[underscoredName];
}
});
@@ -15,15 +24,6 @@ DS.RESTAdapter = DS.Adapter.extend({
serializer: serializer,
- materializeBelongsTo: function(record, hash, name) {
- record.materializeBelongsTo(name, hash[name + "_id"]);
- },
-
- materializeAttribute: function(record, hash, name) {
- var underscoredName = Ember.String.decamelize(name);
- record.materializeAttribute(name, hash[underscoredName]);
- },
-
createRecord: function(store, type, record) {
var root = this.rootForType(type);
@@ -1,37 +1,184 @@
var get = Ember.get;
DS.Serializer = Ember.Object.extend({
+ /**
+ NAMING CONVENTIONS
+
+ The most commonly overridden APIs of the serializer are
+ the naming convention methods:
+
+ * `keyForAttributeName`: converts a camelized attribute name
+ into a key in the adapter-provided data hash. For example,
+ if the model's attribute name was `firstName`, and the
+ server used underscored names, you would return `first_name`.
+ * `primaryKey`: returns the key that should be used to
+ extract the id from the adapter-provided data hash. It is
+ also used when serializing a record.
+ */
+
+ keyForAttributeName: function(type, name) {
+ return name;
+ },
+
+ primaryKey: function(type) {
+ return "id";
+ },
+
+ /**
+ SERIALIZATION
+
+ These methods are responsible for taking a record and
+ producing a JSON object.
+
+ These methods are designed in layers, like a delicious 7-layer
+ cake (but with fewer layers).
+
+ The main entry point for serialization is the `toJSON`
+ method, which takes the record and options.
+
+ The `toJSON` method is responsible for:
+
+ * turning the record's attributes (`DS.attr`) into
+ attributes on the JSON object.
+ * optionally adding the record's ID onto the hash
+ * adding relationships (`DS.hasMany` and `DS.belongsTo`)
+ to the JSON object.
+
+ Depending on the backend, the serializer can choose
+ whether to include the `hasMany` or `belongsTo`
+ relationships on the JSON hash.
+
+ For very custom serialization, you can implement your
+ own `toJSON` method. In general, however, you will want
+ to override the hooks described below.
+
+ ## Adding the ID
+
+ The default `toJSON` will optionally call your serializer's
+ `addId` method with the JSON hash it is creating, the
+ record's type, and the record's ID. The `toJSON` method
+ will not call `addId` if the record's ID is undefined.
+
+ Your adapter must specifically request ID inclusion by
+ passing `{ includeId: true }` as an option to `toJSON`.
+
+ NOTE: You may not want to include the ID when updating an
+ existing record, because your server will likely disallow
+ changing an ID after it is created, and the PUT request
+ itself will include the record's identification.
+
+ By default, `addId` will:
+
+ 1. Get the primary key name for the record by calling
+ the serializer's `primaryKey` with the record's type.
+ Unless you override the `primaryKey` method, this
+ will be `'id'`.
+ 2. Assign the record's ID to the primary key in the
+ JSON hash being built.
+
+ If your backend expects a JSON object with the primary
+ key at the root, you can just override the `primaryKey`
+ method on your serializer subclass.
+
+ Otherwise, you can override the `addId` method for
+ more specialized handling.
+
+ ## Adding Attributes
+
+ By default, the serializer's `toJSON` method will call
+ `addAttributes` with the JSON object it is creating
+ and the record to serialize.
+
+ The `addAttributes` method will then call `addAttribute`
+ in turn, with the JSON object, the record to serialize,
+ the attribute's name and its type.
+
+ Finally, the `addAttribute` method will serialize the
+ attribute:
+
+ 1. It will call `keyForAttributeName` to determine
+ the key to use in the JSON hash.
+ 2. It will get the value from the record.
+ 3. It will call `transformValueToJSON` with the attribute's
+ value and attribute type to convert it into a
+ JSON-compatible value. For example, it will convert a
+ Date into a String.
+
+ If your backend expects a JSON object with attributes as
+ keys at the root, you can just override the `transformValueToJSON`
+ and `keyForAttributeName` methods in your serializer
+ subclass and let the base class do the heavy lifting.
+
+ If you need something more specialized, you can probably
+ override `addAttribute` and let the default `addAttributes`
+ handle the nitty gritty.
+
+ ## Adding Relationships
+
+ By default, `toJSON` will call your serializer's
+ `addRelationships` method with the JSON object that is
+ being built and the record being serialized. The default
+ implementation of this method is to loop over all of the
+ relationships defined on your record type and:
+
+ * If the relationship is a `DS.hasMany` relationship,
+ call `addHasMany` with the JSON object, the record
+ and a description of the relationship.
+ * If the relationship is a `DS.belongsTo` relationship,
+ call `addBelongsTo` with the JSON object, the record
+ and a description of the relationship.
+
+ The relationship description has the following keys:
+
+ * `type`: the class of the associated information (the
+ first parameter to `DS.hasMany` or `DS.belongsTo`)
+ * `kind`: either `hasMany` or `belongsTo`
+
+ The relationship description may get additional
+ information in the future if more capabilities or
+ relationship types are added. However, it will
+ remain backwards-compatible, so the mere existence
+ of new features should not break existing adapters.
+ */
+
+ transformValueToJSON: function(value, attributeType) {
+ return value;
+ },
+
toJSON: function(record, options) {
options = options || {};
- var hash = {};
-
- this.addAttributes(hash, record);
+ var hash = {}, id;
if (options.includeId) {
- this.addId(hash, record);
+ if (id = get(record, 'id')) {
+ this.addId(hash, record.constructor, id);
+ }
}
+ this.addAttributes(hash, record);
+
this.addRelationships(hash, record);
return hash;
},
addAttributes: function(hash, record) {
record.eachAttribute(function(name, attribute) {
- var attributeName = this.attributeName(attribute, record);
- var value = get(record, attribute.name);
-
- hash[attributeName] = this.transform(value, attribute);
+ this.addAttribute(hash, record, name, attribute.type);
}, this);
},
- attributeName: function(attribute, record) {
- return attribute.name;
+ addAttribute: function(hash, record, attributeName, attributeType) {
+ var key = this.keyForAttributeName(record.constructor, attributeName);
+ var value = get(record, attributeName);
+
+ hash[key] = this.transformValueToJSON(value, attributeType);
},
- transform: function(value, attribute) {
- return value;
+ addId: function(hash, type, id) {
+ var primaryKey = this.primaryKey(type);
+ hash[primaryKey] = id;
},
addRelationships: function(hash, record) {
@@ -44,25 +191,56 @@ DS.Serializer = Ember.Object.extend({
}, this);
},
- addBelongsTo: function() {
- Ember.assert("Serializers must implement addBelongsTo", false);
+ addBelongsTo: Ember.K,
+ addHasMany: Ember.K,
+
+ /**
+ DESERIALIZATION
+ */
+
+ transformValueFromJSON: function(value, attributeType) {
+
},
- addHasMany: function() {
- Ember.assert("Serializers must implement addHasMany", false);
+ materializeFromJSON: function(record, hash) {
+ record.materializeId(this.extractId(record.constructor, hash));
+
+ this.materializeAttributes(record, hash);
+ this.materializeRelationships(record, hash);
},
- addId: function(hash, record) {
- var id = get(record, 'id');
+ materializeAttributes: function(record, hash) {
+ record.eachAttribute(function(name, attribute) {
+ record.materializeAttribute(name, this.extractAttribute(record.constructor, hash, name));
+ }, this);
+ },
- if (id !== undefined) {
- var primaryKey = this.primaryKey(record);
- hash[primaryKey] = id;
- }
+ extractAttribute: function(type, hash, attributeName) {
+ var key = this.keyForAttributeName(type, attributeName);
+ return hash[key];
+ },
+
+ extractId: function(type, hash) {
+ var primaryKey = this.primaryKey(type);
+ return hash[primaryKey];
+ },
+
+ materializeRelationships: function(record, hash) {
+ record.eachAssociation(function(name, relationship) {
+ if (relationship.kind === 'hasMany') {
+ record.materializeHasMany(name, this.extractHasMany(record, hash, relationship));
+ } else if (relationship.kind === 'belongsTo') {
+ record.materializeBelongsTo(name, this.extractBelongsTo(record, hash, relationship));
+ }
+ }, this);
+ },
+
+ extractHasMany: function(record, hash, relationship) {
+ return hash[relationship.key];
},
- primaryKey: function() {
- Ember.assert("Serializers must implement primaryKey", false);
+ extractBelongsTo: function(record, hash, relationship) {
+ return hash[relationship.key];
}
});
Oops, something went wrong.

0 comments on commit 3f134e6

Please sign in to comment.