Friday, May 24, 2013

Transforming Results from the Yahoo Contacts API to a Backbone Collection

Continuing along with my last post about the nice features of Underscore, I decided to document my transformation of the Yahoo Social API contacts result set. As flexible as it may be for enabling syncing of copies of a user's address book, its not exceptionally usable in a Backbone collection. Here's an example contact from the response body:



{
"uri" : "...",
"id" : 132,
"isConnection" : false,
"created" : "2013-05-21T15:05:16Z",
"updated" : "2013-05-21T15:05:56Z",
"categories" : [ ],
"fields" : [
{
"categories" : [ ],
"created" : "2013-05-21T15:05:16Z",
"editedBy" : "OWNER",
"flags" : [ ],
"id" : 228,
"type" : "nickname",
"updated" : "2013-05-21T15:05:16Z",
"uri" : "...",
"value" : "Frank"
},
{
"categories" : [ ],
"created" : "2013-05-21T15:05:16Z",
"editedBy" : "OWNER",
"flags" : [ ],
"id" : 229,
"type" : "email",
"updated" : "2013-05-21T15:05:16Z",
"uri" : "...",
"value" : "frankyb@yahoo.com"
},
{
"categories" : [ ],
"created" : "2013-05-21T15:05:16Z",
"editedBy" : "OWNER",
"flags" : [ ],
"id" : 230,
"type" : "email",
"updated" : "2013-05-21T15:05:16Z",
"uri" : "...",
"value" : "peasandcarrots@gmail.com"
},
{
"categories" : [ ],
"created" : "2013-05-21T15:05:16Z",
"editedBy" : "OWNER",
"flags" : [ "MOBILE" ],
"id" : 231,
"type" : "phone",
"updated" : "2013-05-21T15:05:16Z",
"uri" : "...",
"value" : "333-555-1212"
},
{
"categories" : [ ],
"created" : "2013-05-21T15:05:16Z",
"editedBy" : "OWNER",
"flags" : [ "HOME" ],
"id" : 232,
"type" : "phone",
"updated" : "2013-05-21T15:05:16Z",
"uri" : "...",
"value" : "333-444-1212"
},
{
"categories" : [ ],
"created" : "2013-05-21T15:05:16Z",
"editedBy" : "OWNER",
"flags" : [ ],
"id" : 227,
"type" : "name",
"updated" : "2013-05-21T15:05:56Z",
"uri" : "...",
"value" : {
"familyName" : "Haberhorn",
"familyNameSound" : "",
"givenName" : "Franklin",
"givenNameSound" : "",
"middleName" : "",
"prefix" : "",
"suffix" : ""
}
},
{
"categories" : [ ],
"created" : "2013-05-21T15:05:56Z",
"editedBy" : "OWNER",
"flags" : [ ],
"id" : 233,
"type" : "address",
"updated" : "2013-05-21T15:05:56Z",
"uri" : "...",
"value" : {
"city" : "Lake Mary",
"country" : "United States",
"countryCode" : "US",
"postalCode" : "30555",
"stateOrProvince" : "MI",
"street" : "PO Box 4657848"
}
},
{
"categories" : [ ],
"created" : "2013-05-21T15:05:56Z",
"editedBy" : "OWNER",
"flags" : [ "WORK" ],
"id" : 234,
"type" : "address",
"updated" : "2013-05-21T15:05:56Z",
"uri" : "...",
"value" : {
"city" : "Orlando",
"country" : "United States",
"countryCode" : "US",
"postalCode" : "30555",
"stateOrProvince" : "FL",
"street" : "555 Rose Ct"
}
}
]
}




I purposely created a contact with multiple addresses, phone numbers, and emails to help visualize and test the mapping. My goal is to take that object and turn it into the following:





{
"uri" : "..."
"id" : 132,
"created" : "2013-05-21T15:05:16Z",
"updated" : "2013-05-21T15:05:56Z",
"isConnection" : false,
"categories" : [ "Work" ],
"address" : [
{
"flags" : [ ],
"value" : {
"city" : "Lake Mary",
"country" : "United States",
"countryCode" : "US",
"postalCode" : "30555",
"stateOrProvince" : "MI",
"street" : "PO Box 4657848"
}
},
{
"flags" : [ "WORK" ],
"value" : {
"city" : "Orlando",
"country" : "United States",
"countryCode" : "US",
"postalCode" : "30555",
"stateOrProvince" : "FL",
"street" : "555 Rose Ct"
}
}
],
"email" : [
{
"flags" : [ ],
"value" : "frankyb@yahoo.com"
},
{
"flags" : [ ],
"value" : "peasandcarrots@gmail.com"
}
],
"name" : {
"familyName" : "Haberhorn",
"familyNameSound" : "",
"givenName" : "Franklin",
"givenNameSound" : "",
"middleName" : "",
"prefix" : "",
"suffix" : ""
},
"nickname" : "Frank",
"phone" : [
{
"flags" : [ "MOBILE" ],
"value" : "333-555-1212"
},
{
"flags" : [ "HOME" ],
"value" : "333-444-1212"
}
]
}





I want my fields to be keys in my object which will reference the assigned value. In the case where one field repeats with different values, I want a single key to reference an array of values. Downstream, my model will know how to map this object into several child collections that will maintain the multiple items and set a primary version to use in lists and searches. Most likely, I'll add another layer to flatten/rename things to match with my model better. However, for now, I'm just focusing on a rough normalization of the data.

The following function will transform the JSON parsed response into the desired format:



function transform( data ) {

return (
_.map(data.contacts.contact, function (rec) {

return (

/*
* For each contact, build a new object hash from the
* data return by the API
*/
_.extend(

/* Start with the top-level keys. Leave the fields and categories for the next two steps */
_.omit(rec, "fields", "categories"),

/*
* Handle each object in the fields array.
* Since the "type" field can repeat for different values,
* they can't just be plucked out of the collection. Instead,
* they need to be grouped and mapped into either an array of
* values or just the single value.
*/
_.chain(rec.fields)

/*
* results in an object where the key is the unique set of types (field names)
* in the fields array and the value of the key is an array
* of the original field objects
* output: [ { type: [ { field_object } ], type: [ { field_object }, { field_object }, ... ], ... ]
*/
.groupBy(function (f) { return f.type; })

/*
* build key/value array pairs
* detect multiple values as output by the groupBy
* if more than one, the value of the key will be an array
* of hashes with the flag (sometimes distinguishes multiple values)
* and the actual value. If only one, the key will hold the value string
* (flat, no array)
* output: [ [ type, value ] | [ type, [ { flags: [ ... ], value: ... } ], ... ]
*/
.map(function (v, k) {
return [ k, v.length > 1 ? _.map(v, function ( o ) { return _.pick(o, "flags", "value"); }) : v[0].value ]})

/* Take the array of array key/value pairs and create the object */
.object()
.value(),

/* Add key with an array of categories */
{ categories: _.pluck(rec.categories, "name") })
);
})
);
}




Now you can call this from jQuery's deferred.done() after making the request. For testing, I saved a call to the API into a local file and used $.get() to retrieve it. The transform sets a global variable so I could easily inspect it in Firebug:



$(function() {

$.get('yahoo-contacts.json')
.done(function (data) {
YAHOO_CONTACTS = transform(JSON.parse(data));
});
});



To use the data in a Backbone collection, the transform call probably is best placed in the Collection.parse method. Simply returning the result from transform will then cause Backbone to pass each normalized object to Model.parse as it instantiates each model. The model instance can then define its own parse method to perform further processing:



var Contact = Backbone.Model.extend({

parse: function ( data ) {
// More work ...

return data;
}

});

var YahooContacts = Backbone.Collection.extend({

model: Contact,
parse: transform,
url: 'yahoo-contacts.json'

});

/*
* Now load all the data ...
*/
$(function() {
list = new YahooContacts();
list.fetch();
});



In my case, I'm trying to normalize several sources of contact data into one standard model representation. Different collections are defined for each source end-point and then utilize the same model for storing the actual dataset. All the presentation logic is unaffected and can display contacts from any source. Since I'm only providing read-only views of the data, maintaining the original format is not a concern since I won't be sending anything back.