xJson Object Continued (Part II)

Today we are concluding our discussion of the xJSON object package. You should have read the following two articles and know what Model.js does, and understand the "extend" function with super class inheritance:

Improving Extend With Super
XJSON Objects

Download the most up-to-date xJSON package.

There are 4 JSON models in this package: JsonArray, JsonObject, XJsonArray, and XJsonObject. As a recap, the xJSON Objects are used to improve the performance of large, repeating data sets. For example, if you have the JSON array:

Example 1: JSON Array

[
{id:1, name: name1},
{id:2, name: name2},
{id:3, name: name3},
{id:4, name: name4},
{id:5, name: name5},
…
]

Each object in the array contains key/value pairs, with the keys repeated in each object. You can reduce the size of such a message by using a compression technique, known as xJSON:

Example 2: xJSON Array

{
scheme: [id, name],
set: [
	[1, name1],
	[2, name2],
	[3, name3],
	[4, name4],
	[5, name5],
	…
]
…
}

These xJSON Classes should produce similar structures (at least publicly) to their JSON counterparts. This way, you dont care if you have a JsonArray or XJsonArray, only if you have an array versus an object. For the most part, XJsonObject will only be instantiated from XJsonArray, as you are required to pass in a schema, which requires an extra variable that normal objects will not have. One of the trickiest parts is that a JSON object could contain nested JSON objects/arrays, which we need to detect and instantiate on demand. For this purpose I wrote the Model method "XJson" which detects whether the passed object is a JSON object/array or not and returns the appropriate class, null, or simply the variable itself. Therefore, I can use this class anytime I want JSON object, whether from a new JSON object or when evaluating the values inside of a JSON object.

Example 3: XJson

Core.Model.XJson = function(o) {
	if (! o) { // test 1
		return null;
	}
	else if (isType(o, array) && isType(o[0], object)) { // test 2
		return new Core.Model.JsonArray(o);
	}
	else if (isType(o, object) && ! isType(o, array)) { // test 3
		return (o.scheme)? new Core.Model.XJsonArray(o): new Core.Model.JsonObject(o);
	}
	else { // test 4
		return o;
	}
};

This Function examines the passed object, returning null if no parameter is passed (ie. there is no data) and the parameter itself when it does not match JSON criteria. The second test checks to see if the parameter is an array and that the first member is an object, which generally means it is a JsonArray (this is a weak assumption, please comment if you have a better idea). The third check tests if the parameter is an object, but not an array primitive. Then if it has a scheme value, the data object is assumed to an XJsonArray, otherwise, it is a JsonObject (the assumption here is that you never use scheme as a key for a non-xJSON object).

Event though we already discussed JsonObject and JsonArray, I want to revisit them today, as they have changed quite a bit and will form the basis on which we can discuss the xJSON objects.

Example 4: JSON Object

Core.Model.JsonObject = function(json) {
	this.update(json);
};

Core.extend(Core.Model.JsonObject, Core.Model.Model, {

	/**
	 * The number of values in the json object
	 *
	 * @property length
	 * @type int
	 */
	length: 0,

	/**
	 * Updates the object to use the passed data set
	 *
	 * @method update
	 * @param json {array} jsonobject object
	 * @public
	 */
	update: function(json) {
		// validation
		if (! isType(json, object)) {throw(JsonObject - Invalid data passed into Update);}
		this.parent.update.call(this, json);

		// private variables
		var data = {},
			that = this,
			n = 0;

		var fx = function(key, o) {
			var ckey = key.replace(/^is/, ).capitalize();
			data[key] = Core.Model.XJson(o);
			that[(isType(o, boolean)? is: get) + ckey] = function() {return data[key];};
			that[set + ckey] = function(o) {
				if ($type(o) !== $type(data[key])) {throw(JSONObject - invalid object passed into setter for:  + key);}
				data[key] = o;
			};
		};

		// iterate through the JSON object keys
		for (var key in json) {
			fx(key, json[key]);
			n += 1;
		};

		// update public variables
		this.length = n;
	}
});

The first thing to notice is that this object and all the future object will extend the Model object. They will also have a public value "length", which for the (x)JsonObject types will be the number of key/value pairs and for (x)JsonArray types will be the length of the JSON array. Each object also has an update method that first validates the data passed to it, then calls the update method of the super class "Model".

For JsonObject Class we first create a local "data" Object and the Function "fx" to create a closure around the "data" variable. Next we iterate through all the keys in the JSON Object and fill the "data" variable with the evaluated values. Lastly, we create the setter and getter function for each key/value pair, which set or update the value in the "data" variable. The only caveat is that booleans values get an "is" instead of a "get" in front of them. Booleans should always start with question verbs (such as "is", "has", etc.). I find that I use "is" 90% of the time, so I just went ahead and now use "is" 100% of the time. You can remove the ternary statement and just preface the methods with "get" if you do not structure your booleans this way. Lastly, when using a setter method, we check to ensure that both the object we are replacing and the object we are passing at least have the same type, which should help reduce errors.

Example 5: JSON Array

Core.Model.JsonArray = function(json) {
	this.update(json);
};

Core.extend(Core.Model.JsonArray, Core.Model.Model, {

	/**
	 * The number of values in the json array
	 *
	 * @property length
	 * @type int
	 */
	length: 0,

	/**
	 * Execute function fn on all elements in collection
	 *
	 * @method batch
	 * @param fn {function} the function to execute
	 * @public
	 */
	batch: function(fn) {
		Core.batch(this.data, function(o, i, scope) {
			fn(scope.get(i), i);
		}, this);
	},

	/**
	 * Retrieve the element at i from the collection; lazy definition to allow for converting objects on demand
	 *
	 * @method get
	 * @param i {int} index in the data
	 * @public
	 */
	get: function(i) {
		var that = this,
			data = [];

		this.get = function(i) {
			// commented because this is very strict and dont always want to use, sometimes returning undefined is ok
			//if (! that.data[i]) {throw(JsonArray - Invalid index passed into Get:  + i);}

			if (! data[i] && that.data[i]) {
				data[i] = Core.Model.XJson(that.data[i]);
			}

			return data[i];
		};

		return this.get(i);
	},

	/**
	 * Inserts another value into the data structure
	 *
	 * @method push
	 * @param o {array} new row of data
	 * @public
	 */
	push: function(o) {
		if (! o) {throw(JsonArray - Invalid Object passed into Push);}
		this.data.push(o);
		this.length = this.data.length;
	},

	/**
	 * Updates the object to use the passed data set
	 *
	 * @method update
	 * @param json {array} jsonarray object
	 * @public
	 */
	update: function(json) {
		// validation
		if (! isType(json, array)) {throw(JSONArray - Invalid JSON Array Object passed into Update);}
		this.parent.update.call(this, json);

		// update public variables
		this.length = json.length;
	}
});

First, notice that this Class has a similar structure to that of JsonObject (all of the JSON object types do). Now, a JSON array should just be a collection of JSON objects, and we have made that assumption here. The get method, uses lazy initialization to create an internal "data" variable, which will be updated each time an index "i" is requested. This prevents us from having to initialize each JsonObject until it is requested, thereby making this an inexpensive transformation. If you instantiated each JsonObject in the update method, you run the risk of monopolizing the clients CPU when working with medium to large JSON arrays. The "batch" function is a handle shortcut that calls "get" on all indexes, then executes the passed Function on the result. Lastly, I added "push" for convenience because I occasionally find myself adding elements to the object. A "pop" or "remove" method is probably useful, but I have not had a need for one yet.

Example 6: xJSON Object

Core.Model.XJsonObject = function(scheme, json) {
	if (! isType(scheme, array)) {throw(XJsonObject - Invalid scheme passed into Constructor);}
	this.scheme = scheme;
	this.update(json);
};

Core.extend(Core.Model.XJsonObject, Core.Model.Model, {

	/**
	 * The number of values in the xjson object
	 *
	 * @property length
	 * @type int
	 */
	length: 0,

	/**
	 * The object schema
	 *
	 * @property scheme
	 * @type array
	 */
	scheme: [],

	/**
	 * Updates the object to use the passed data set
	 *
	 * @method update
	 * @param json {object} XJsonObject object
	 * @public
	 */
	update: function(json) {
		// validation
		if (! isType(json, array)) {throw(XJsonObject - Invalid data passed into Update);}
		if (json.length !== this.scheme.length) {throw(XJsonObject - Invalid data (does not match scheme) passed into Update);}
		this.parent.update.call(this, json);

		// private variables
		var data = this.data,
			that = this;

		// update public variables
		this.length = data.length;

		Core.batch(this.scheme, function(key, i, json) {
			var o = json[i],
				ckey = key.replace(/^is/, ).capitalize();

			data[i] = Core.Model.XJson(o);

			that[(isType(o, boolean)? is: get) + ckey] = function() {return data[i];};
			that[set + ckey] = function(o) {
				if ($type(o) !== $type(data[key])) {throw(XJSONObject - invalid object passed into setter for:  + key);}
				data[i] = o;
			};
		}, data);
	}
});

XJsonObject is actually very similar to JsonObject, except instead of having to iterate on the key/value pairs of a JavaScript Object, you just iterate through an array of schema and a data array. The scheme is stored in a global array and except in rare circumstances when you have a constant schema that you use in several places, this method will only be called by XJsonArray.

Example 7: xJSON Array

Core.Model.XJsonArray = function(json) {
	this.update(json);
};

Core.extend(Core.Model.XJsonArray, Core.Model.Model, {

	/**
	 * The number of values in the xjson array
	 *
	 * @property length
	 * @type int
	 */
	length: 0,

	/**
	 * The object schema
	 *
	 * @property scheme
	 * @type array
	 */
	scheme: [],

	/**
	 * Execute function fn on all elements in collection
	 *
	 * @method batch
	 * @param fn {function} the function to execute
	 * @public
	 */
	batch: function(fn) {
		Core.batch(this.data, function(o, i, scope) {
			fn(scope.get(i), i);
		}, this);
	},

	/**
	 * Retrieve the element at i from the collection; lazy definition to allow for converting objects on demand
	 *
	 * @method get
	 * @param i {int} index in the data
	 * @public
	 */
	get: function(i) {
		var that = this,
			data = [];

		// internally redeclaring to scope the data structure, which will contain already converted objects
		this.get = function(i) {
			// commented because this is very strict and dont always want to use, sometimes returning undefined is ok
			//if (! that.data[i]) {throw(XJsonArray - Invalid index passed into Get:  + i);}

			if (! data[i]) {
				var o = that.data[i];

				// if array, then it is an XJsonObject
				if (isType(o, array) && o.length === that.scheme.length) {
					data[i] = new Core.Model.XJsonObject(that.scheme, o);
				}
				// otherwise, use generic method
				else {
					data[i] = Core.Model.XJson(o);
				}
			}

			return data[i];
		};

		return this.get(i);
	},

	/**
	 * Inserts another value into the data structure
	 *
	 * @method push
	 * @param o {array} new row of data
	 * @public
	 */
	push: function(o) {
		if (! (isType(o, array) && o.length === this.scheme.length)) {throw(XJsonArray - Invalid data passed into Push);}
		this.data.push(o);
		this.length = this.data.length;
	},

	/**
	 * Updates the object to use the passed data set
	 *
	 * @method update
	 * @param json {object} xjsonarray object
	 * @public
	 */
	update: function(json) {
		// validation
		if (! (json && isType(json, object) && isType(json.scheme, array) && isType(json.set, array))) {throw(XJsonArray - Invalid data passed into Update);}
		this.parent.update.call(this, json.set);

		// update public variables
		this.length = json.length;
		this.scheme = json.scheme;
	}
});

This Class is very similar to JsonArray, but with a public "scheme" array as with XJsonObject. When the "update" Function calls the super class "update", it passes in the "set" parameter, so the public "data" variable is an array like that of JsonArray. The "get" Function is only different from JsonArray, in that it first looks to see if the value at "i" is an xJSON object before it defers to other objects.

Obviously, this these Classes are fairly complex in their implementation, but they are easy to use. Simply, call "Mint.Model.XJson(yourJsonObject)" and the results will be evaluated and easy to use.

Download "xJson.js"

See My Simple Test Page