October 13, 2009

Variable References in JESON

This is a continuation of my series on JESON, an enhanced replacement for JSON. The previous articles were:
  1. An introduction to JESON
  2. A quick overview of where to get JESON and how to use it
In this part I describe an additional enhancement to JESON. I have been developing and redefining JESON as I go along, so a few things have changed and will continue to change as this specification evolves. These changes, of course, drift farther and farther away from the original JSON syntax, but all for the better. The following enhancement is variable support in values. In JSON and JESON, you can assign constant values to keys:
x = 23
y = 'hello'
z = [1, 2, 3]

and so on. But now in JESON, you can assign a variable to a key. This is done by simply giving the name of the variable:
x = 23
y = x
This will assign 23 to y. You can also use variable references in array elements:
x = 23
y = x
z = [x, y]
Basically, anywhere a value is allowed (in dictionary key assignments and elements of an array), a variable reference can be used.

Valid key/variable names

In order for this to work, your variables and dictionary keys cannot look like a constant, meaning:
  • names cannot be true or false or null or start with a number
  • names cannot have spaces in them
Variable/key names can be alpha-numerics (a-z, A-Z, 0-9), but can also include "." and "_". The need for "." will be apparent later. Note that the old JSON key name rules can still be applied, like putting spaces or weird character in key names. But if you do, you cannot reference that key in a value. For example, you can still do the first line, but there is no way to do the second line:
"my key" = true
x = my key // syntax error

Name scoping

Variables have scope, just like in any programming language. Each nested dictionary forms a context. When trying to resolve a variable, JESON will search upwards through the nested dictionaries. In the following example, x will be assigned 72 because that is the nearest scope for y:
outer {
    y = 23
    inner {
        y = 72
        x = y
If you comment out the inner y, then x is assigned 23. JESON will continue up the nested contexts until it finds it. If it doesn't, it uses null for the value.

Exposing Host Scope

Scope is not limited to the JESON file itself; context searches can spill out into your host application. In the above example, if there are no y's at any level, then JESON can ask the host application for the variable. To support this, the host application needs to set the hostContext property on the JESON parser object. The value to this property is an object that supports Key-Value coding. You could use self, or even construct a context via JESON, as in this example:

// create a parser object
JESON* parser = [JESON new];

// create a dictionary 
id context = [parser parseString:@"y='hello'"];

// set that dictionary as our external context
parser.hostContext = context;

// and parse something that references a variable
id root = [parser parseString:@"x = y"];
This code will assign to root a dictionary with the key "x" set to the value "hello". The extension to the NSString class has also been extended to support this:
NSString* jeson = @"x = y";
id root = [jeson parseAsJESON:jeson usingContext:context];

The beauty of this is a host application can provide himself as the context, and then the JESON has access to all its properties.
// set some properties
self.myProperty = somevalue;
self.anotherProperty = anothervalue;

// parse some JESON
parser.hostContext = self;
id root = [parser parseString:@"x = myProperty, y = anotherProperty"];
As a practical application of this technique, I am using the new TableFormEditor which can have the form described in JESON. I want to edit a form that can have a title that varies depending on the situation. The host application determines the title and puts it in a property for the JESON to reference:
if(editing) {
    self.formTitle = @"Edit data";
} else {
    self.formTitle = @"Add data";

id root = [parser parseString:myJESON];
And the JESON text could have something like this:
showLabels = true
title = formTitle

Variable Paths

To be consistent with Key/Value Coding, the variable name can also be a path in order to traverse nested contexts and to dig into dictionaries:
globals {
    y = "Hi"

outer {
    y = 23
    inner {
        y = 72
        x = globals.y
Instead of using one of the "local" y values, we pull y out of a different dictionary. You can use this syntax to dive into any dictionary:
stats {
    records {
        years {
            y2008 {
                july { income = 5, expenses = 3 }
                august {
                    income = 4
                    // expenses are the same as in July
                    expenses = july.expenses

income = stats.records.years.y2008.august.income

Note that the path is relative to where the reference is. JESON will still search up through the nested contexts, but instead of looking for a single key, it is looking for a path. This variable "path" is also supported when accessing host application data.
There is one caveat to what is supported, due to the way the nested dictionaries are parsed. You cannot reference a dictionary by name if you are a descendent of it. For example, the key "expenses" above could not reference "y2008.july.expenses" because expenses is a grandchild of y2008. This limitation is because the y2008 dictionary has not been created fully yet, as it is recursively getting built. In most cases, this is not a problem, as the contexts are searched upwards through the ancestors, so there is little reason to name your ancestor. Many people don't realize that key/value coding also works with arrays. But the key is not applied directly to the array. Instead it is applied to every element in the array and returns an array of those results. For example:
fields [
    { id = 1 }
    { id = 2 }
    { id = 55 }

ids = fields.id
// ids => [1, 2, 55]
An updated JESON parser has been made available with the features described here. Get it off the Osmorphis group: jeson.zip.

No comments:

Post a Comment