5 Ja.js - Selector Transformer

Ja.js (previously named st.js) is a library that enables implementing practically any programming concept in a declarative manner, using JSON.

This is because ja.js is a low level building block for creating a Turing Complete JSON markup language.

ja.js is the core JSON parser that powers Jasonette, so you can build native iOS/Android apps by writing nothing but a JSON markup.

Ja.js

Ja.js

But Jasonette is just one implementation. Want to build your own turing complete JSON markup language?

5.2 Playground

Parse JSON anywhere, both frontend and backend, as easy as using JSON.stringify or JSON.parse.

https://jasonelle.com/docs/playground-ja/

Source Code:

https://github.com/jasonelle/docs/tree/develop/playground-ja.

5.3 Introduction

ja.js is a library that adds a couple of powerful methods to JavaScript’s native JSON.

So you can simply use it with the syntax ST.select(...).transform(...).

The library is just a single file, made up of stateless functions, with NO dependency. Which makes it effortless to embed anywhere without hassle. (Currently used in various environments including iOS, Android, node.js, browser, etc.).

What Can I Use It For?

JSON powers almost everything in the world. Being able to bend any JSON to your will means you can do all kinds of magical things.

Ja.js consist mainly of two operations:

  1. Select: Query any JSON tree to select exactly the subtree you are looking for.
  2. Transform: Transform any JSON object to another by parsing with a template, also written in JSON

You can also mix and match Select AND Transform to perform partial transform, modularize JSON objects, etc.

5.3.1 Select

Select a JSON object or its subtree that matches your filter function criteria.

Step 1. Take any JSON object

var data = {
  "links": [
    { "remote_url": "http://localhost" },
    { "file_url": "file://documents" },
    { "remote_url": "https://blahblah.com" }
  ],
  "preview": "https://image",
  "metadata": "This is a link collection"
}

Step 2. Find all key/value pairs that match a selector function

var sel = ST.select(data, function(key, val) {
  return /https?:/.test(val);
})

Step 3. Get the result

var keys = sel.keys();
//  [
//    "remote_url",
//    "remote_url",
//    "preview"
//  ]

var values = sel.values();
//  [
//    "http://localhost",
//    "https://blahblah.com",
//    "https://image"
//  ]

var paths = sel.paths();
//  [
//    "[\"links\"]",
//    "[\"links\"]",
//    ""
//  ]

5.3.2 Transform

Transform any JSON with a declarative template, also in JSON.

Step 1. Take any JSON object

var data = {
  "title": "List of websites",
  "description": "This is a list of popular websites"
  "data": {
    "sites": [{
      "name": "Google",
      "url": "https://google.com"
    }, {
      "name": "Facebook",
      "url": "https://facebook.com"
    }, {
      "name": "Twitter",
      "url": "https://twitter.com"
    }, {
      "name": "Github",
      "url": "https://github.com"
    }]
  }
}

Step 2. Select and transform with a template JSON object

var sel = ST.select(data, function(key, val){
            return key === 'sites';
          })
          .transformWith({
            "items": {
              "{{#each sites}}": {
                "tag": "<a href='{{url}}'>{{name}}</a>"
              }
            }
          })

Step 3. Get the result

var keys = sel.keys();
//  [
//    "tag",
//    "tag",
//    "tag",
//    "tag"
//  ]

var values = sel.values();
//  [
//    "<a href='https://google.com'>Google</a>",
//    "<a href='https://facebook.com'>Facebook</a>",
//    "<a href='https://twitter.com'>Twitter</a>",
//    "<a href='https://github.com'>Github</a>"
//  ]

var objects = sel.objects();
//  [
//    {
//      "tag": "<a href='https://google.com'>Google</a>"
//    }, {
//      "tag": "<a href='https://facebook.com'>Facebook</a>"
//    }, {
//      "tag": "<a href='https://twitter.com'>Twitter</a>"
//    }, {
//      "tag": "<a href='https://github.com'>Github</a>"
//    }
//  ]

var root = sel.root();
//  {
//    "items": [{
//      "tag": "<a href='https://google.com'>Google</a>"
//    }, {
//      "tag": "<a href='https://facebook.com'>Facebook</a>"
//    }, {
//      "tag": "<a href='https://twitter.com'>Twitter</a>"
//    }, {
//      "tag": "<a href='https://github.com'>Github</a>"
//    }]
//  }

5.3.3 Usage

5.3.3.1 In a browser

<script src="ja.js"></script>
<script>
var parsed = ST.select({ "items": [1,2,3,4] })
                .transformWith({
                  "{{#each items}}": {
                    "type": "label", "text": "{{this}}"
                  }
                })
                .root();
</script>

5.3.3.2 In node.js

Install through npm:

$ npm install stjs

Use

const ST = require('st');

const parsed = ST.select({ "items": [1,2,3,4] })
                .transformWith({
                  "{{#each items}}": {
                    "type": "label", "text": "{{this}}"
                  }
                })
                .root();

5.4 Select

Select a JSON object or its subtree that matches your filter function.

5.4.1 Syntax

ST.select(data, selectorFunction);

5.4.1.1 Parameters

  • data : Any JavaScript object
  • selectorFunction : selectorFunction is a predicate, to test each key/value pairs of the entire JSON tree. Return true to keep the element, false otherwise, taking two arguments:
    • key : the “key” of the current key/value pair being tested.
    • value : the “value” of the current key/value pair being tested.

5.4.1.2 Return Value

Returns a selection object which can be queried to retrieve the final result, using the following API:

  • values() : Array of values for all the key/value pair matches.
  • keys() : Array of keys for all the key/value pair matches.
  • paths() : Array of paths leading to all the key/value pair matches.
  • objects() : Array of full objects in which the key/value match was found.
  • root() : Retrieve the root node. Useful when used along with transform.

5.4.2 Example

You can select any JSON tree to find only the subtrees that satisfy your needs.

  1. Take any data
var data = {
  links: [
    { "remote_url": "http://localhost" },
    { "file_url": "file://documents" },
    { "remote_url": "https://blahblah.com" }
  ],
  preview: "https://image",
  metadata: "This is a link collection"
};
  1. Select subtree
var selection = ST.select(data, function(key, val) {
  return /https?:/.test(val);
})
  1. Query the selection object
var selected_values = selection.values();
//  [
//    "http://localhost",
//    "https://blahblah.com",
//    "https://image"
//  ]

var selected_keys = selection.keys();
//  [
//    "remote_url",
//    "remote_url",
//    "preview"
//  ]

var selected_objects = selection.objects();
//  [
//    { "remote_url": "http://localhost" },
//    { "remote_url": "https://blahblah.com" },
//    {
//      "links": [
//        { "remote_url": "http://localhost" },
//        { "file_url": "file://documents" },
//        { "remote_url": "https://blahblah.com" }
//      ],
//      "preview": "https://image",
//      "metadata": "This is a link collection"
//    }
//  ]
//
//

var selected_paths = selection.paths();
//  [
//    "[\"links\"][0]",
//    "[\"links\"][2]",
//    ""
//  ]

5.5 Transform

Transform any JSON with a declarative template, also in JSON.

5.5.1 Syntax

There are 2 ways of transforming an object:

5.5.1.1 transform()

Select a template or its subtree, and transform data with the selected template.

ST.select(TEMPLATE).transform(DATA)


ST.select({
      "{{#each items}}": {
        "type": "label",
        "text": "{{name}}"
      }
    })
    .transform({
      items: [
        { id: 1, name: "Ja" },
        { id: 2, name: "Ka" },
        { id: 3, name: "La" }
      ]
    })
    .root();

5.5.1.2 transformWith()

Select a data object or its subtree and transform with a template.

ST.select(DATA).transformWith(TEMPLATE)

ST.select({
      items: [
        { id: 1, name: "Ja" },
        { id: 2, name: "Ka" },
        { id: 3, name: "La" }
      ]
    })
    .transformWith({
      "{{#each items}}": {
        "type": "label",
        "text": "{{name}}"
      }
    })
    .root();

5.5.2 Basic

Use {{ }} notation to fill out a template with data to generate a new JSON.

  1. Template and Data
var template = {
  "menu": {
    "flavor": "{{flavor}}",
    "richness": "{{richness}}",
    "garlic amount": "{{garlic_amount}}",
    "green onion?": "{{green_onion}}",
    "sliced pork?": "{{pork_amount}}",
    "secret sauce": "{{sauce_amount}}",
    "noodle's texture": "{{texture}}"
  }
}

var data = {
  "flavor": "strong",
  "richness": "ultra rich",
  "garlic_amount": "1 clove",
  "green_onion": "thin green onion",
  "pork_amount": "with",
  "sauce_amount": "double",
  "texture": "extra firm"
}
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
{
  "menu": {
    "flavor": "strong",
    "richness": "ultra rich",
    "garlic amount": "1 clove",
    "green onion?": "thin green onion",
    "sliced pork?": "with",
    "secret sauce": "double",
    "noodle's texture": "extra firm"
  }
}

5.5.3 Loop

Use #each to iterate through items.

  1. Template and Data
var template = {
  "orders": {
    "{{#each customers}}": {
      "order": "One {{menu}} for {{name}}!"
    }
  }
}

var data = {
  "customers": [{
    "name": "Hatter",
    "menu": "miso ramen"
  }, {
    "name": "March Hare",
    "menu": "tonkotsu ramen"
  }, {
    "name": "Dormouse",
    "menu": "miso ramen"
  }, {
    "name": "Alice",
    "menu": "cup noodles"
  }]
}
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
{
  "orders": [{
    "order": "One miso ramen for Hatter!"
  }, {
    "order": "One tonkotsu ramen for March Hare!"
  }, {
    "order": "One miso ramen for Dormouse!"
  }, {
    "order": "One cup noodles for Alice!"
  }]
}

5.5.4 Conditionals

Use #if / #elseif / #else to selectively fill out a template.

  1. Template and Data
var template = {
  "response": [{
    "{{#if spicy < 7}}": {
      "message": "Coming right up!"
    }
  }, {
    "{{#elseif spicy < 9}}": {
      "message": "Are you sure? It is very spicy"
    }
  }, {
    "{{#else}}": {
      "message": "Please sign here where it says you're responsible for this decision"
    }
  }]
}

var data = {
  "spicy": 8
}
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
{
  "response": {
    "message": "Are you sure? It is very spicy"
  }
}

5.5.5 Existencial Operator

You can use the existential operator #? to exclude an attribute altogether if the template evaluates to a falsy value.

  1. Template and Data
var data = {
  notifications: {
    home: 1,
    invite: 2
  }
};
var template = {
  tabs: [{
    text: "home",
    badge: "{{#? notifications.home}}"
  }, {
    text: "message",
    badge: "{{#? notification.message}}"
  }, {
    text: "invite",
    badge: "{{#? notification.invite}}"
  }]
}
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
{
  tabs: [{
    text: "home",
    badge: 1
  }, {
    text: "message"
  }, {
    text: "invite",
    badge: 2
  }]
}

5.5.6 Concat

You can concatenate multiple items and arrays into a single array using the #concat operator.

  1. Template and Data
var data = {
  numbers: [1,2,3]
};
var template = {
  "items": {
    "{{#concat}}": [
      {
        "type": "label",
        "text": "Length: {{numbers.length}}"
      },
      {
        "{{#each numbers}}": {
          "type": "label",
          "text": "{{this}}"
        }
      }
    ]
  }
};
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
{
  "items": [{
    "type": "label",
    "text": "Length: 3"
  }, {
    "type": "label",
    "text": 1
  }, {
    "type": "label",
    "text": 2
  }, {
    "type": "label",
    "text": 3
  }]
}

5.5.7 Merge

You can merge multiple objects into a single object using the #merge operator. If there are any overlapping attributes, the ones that come later will override the previously set attribute.

  1. Template and Data
var data = {
  numbers: [1,2,3],
  align: "right",
  size: "14"
};

var template = {
  "{{#merge}}": [
    {
      "type": "label",
      "text": "Length: {{numbers.length}}"
    },
    {
      "style": {
        "align": "{{align}}",
        "size": "{{size}}"
      },
      "action": {
        "type": "$render"
      }
    }
  ]
};
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
{
  "type": "label",
  "text": "Length: 3",
  "style": {
    "align": "right",
    "size": "14"
  },
  "action": {
    "type": "$render"
  }
}

5.5.8 Inline JavaScript

You can use ANY native javascript expression inside the template.

  1. Template and Data
var template = {
  "ranking": {
    "{{#each players.sort(function(p1, p2) { return p2.quantity - p1.quantity; }) }}": "{{name}} ate {{quantity}}"
  },
  "winner": "{{players.sort(function(p1, p2) { return p2.quantity - p1.quantity; })[0].name }}"
};
var data = {
  "players": [{
    "name": "Alice",
    "quantity": 102
  }, {
    "name": "Mad Hatter",
    "quantity": 108
  }, {
    "name": "Red Queen",
    "quantity": 100
  }]
};
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
{
  "ranking": [
    "Mad Hatter ate 108",
    "Alice ate 102",
    "Red Queen ate 100"
  ],
  "winner": "Mad Hatter"
}

5.5.9 $root

Sometimes you need to refer to the root data object while iterating through an #each loop.

In this case you can use a special keyword named $root.

  1. Template and Data
var template = {
  "{{#each posts}}": [
    "{{content}}",
    "{{$root.users[user_id]}}"
  ]
}
var data = {
  users: ["Alice", "Bob", "Carol"],
  posts: [{
    content: "Show me the money",
    user_id: 1
  }, {
    content: "hello world",
    user_id: 0
  }, {
    content: "what is the meaning of life?",
    user_id: 2
  }]
}
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
[
  ["Show me the money", "Bob"],
  ["hello world", "Alice"],
  ["what is the meaning of life?", "Carol"]
]

5.5.10 $index

You can use a special variable named $index within #each loops.

  1. Template and Data
const template = {
  "rows": {
    "{{#each items}}": {
      "row_number": "{{$index}}",
      "columns": {
        "{{#each this}}": {
          "content": "{{this}}",
          "column_number": "{{$index}}"
        }
      }
    }
  }
};
const data = {
  "items": [
    ['a','b','c','d','e'],
    [1,2,3,4,5]
  ]
};

const result = ST.select(template)
                  .transform(data)
                  .root()

// or
// const result = ST.transform(template, data)
  1. Select and Transform
ST.select(template)
    .transform(data)
    .root();

// or
// ST.transform(template, data)
  1. Result
{
  "rows": [
    {
      "row_number": 0,
      "columns": [
        {
          "content": "a",
          "column_number": 0
        },
        {
          "content": "b",
          "column_number": 1
        },
        {
          "content": "c",
          "column_number": 2
        },
        {
          "content": "d",
          "column_number": 3
        },
        {
          "content": "e",
          "column_number": 4
        }
      ]
    },
    {
      "row_number": 1,
      "columns": [
        {
          "content": 1,
          "column_number": 0
        },
        {
          "content": 2,
          "column_number": 1
        },
        {
          "content": 3,
          "column_number": 2
        },
        {
          "content": 4,
          "column_number": 3
        },
        {
          "content": 5,
          "column_number": 4
        }
      ]
    }
  ]
}

5.5.11 Local Variables

You can use #let API to declare local variables. The #let API takes an array as a paremeter, which has two elements:

  • The first parameter: the {{#let}} statement which assigns any value to a variable.

  • The second parameter: the actual expression that will be evaluated.

Here’s an example:

  1. Template and Data
const data = {
  families: [{
    location: "Wonderland",
    members: [{
      name: "Alice"
    }, {
      name: "Bob"
    }]
  }, {
    location: "Springfield",
    members: [{
      name: "Bart"
    }, {
      name: "Marge"
    }, {
      name: "Lisa"
    }, {
      name: "Homer"
    }, {
      name: "Maggie"
    }]
  }]
}
const template = {
  "rows": {
    "{{#each families}}": {
      "{{#let}}": [{
        "family_location": "{{location}}"
      }, {
        "{{#each members}}": {
          "type": "label",
          "text": "{{name}} in {{family_location}}"
        }
      }]
    }
  }
}
  1. Select and Transform
const result = ST.select(template)
                  .transform(data)
                  .root()

// or
// const result = ST.transform(template, data)
  1. Result
{
  "rows": [
    [
      {
        "type": "label",
        "text": "Alice in Wonderland"
      },
      {
        "type": "label",
        "text": "Bob in Wonderland"
      }
    ],
    [
      {
        "type": "label",
        "text": "Bart in Springfield"
      },
      {
        "type": "label",
        "text": "Marge in Springfield"
      },
      {
        "type": "label",
        "text": "Lisa in Springfield"
      },
      {
        "type": "label",
        "text": "Homer in Springfield"
      },
      {
        "type": "label",
        "text": "Maggie in Springfield"
      }
    ]
  ]
}

The local variable feature is important when you are using nested loops.

You could use the $root variable to reach out of the current loop context, but this has limitations, because you can always only reach out to the root level.

By using the #let API, you can define a variable at any level of a loop and have it accessible from anywhere further down the loop WITHOUT using the $root variable.

5.5.12 Example 1 - Use only a subtree of a template

Sometimes you don’t want to use the entire template to parse data. In this case you can select a subtree of a template and use that to parse data.

var template = {
  body: {
    sections: [{
      items: {
        "{{#each items}}": {
          type: "{{type}}",
          url: "{{url}}"
        }
      }
    }]
  }
};

var finalTemplate = ST.select(template, function(key, val) {
                          return key === 'type';
                        })
                        .transform({ type: "image" }).root();

/*
finalTemplate = {
  body: {
    sections: [{
      items: {
        "{{#each items}}": {
          type: "image",
          url: "{{url}}"
        }
      }
    }]
  }
}
*/

5.5.13 Example 2 - Filter Data + Transform with template

Sometimes you have a large set of data but only want to transform a portion of it. In this case you can select a subtree of the data object and parse using a template.

var data = {
  "item": { "url": "http://localhost", "text": "localhost" },
  "items": [
    { "url": "file://documents", "text": "documents" },
    { "url": "https://blahblah.com", "text": "blah"  }
  ],
  "nestedItems": {
    "childItems": [{
      "url": "http://hahaha.com",
      "text": "haha"
    }, {
      "url": "https://hohoho.com",
      "text": "hoho"
    }]
  }
};

var selection = ST.select(data, function(key, val) {
  return key === 'url';
});

var urls = selection.values();
/**
*  urls = [
*    "http://localhost",
*    "file://documents",
*    "https://blahblah.com",
*    "http://hahaha.com",
*    "https://hohoho.com"
*  ]
*/

var transformed = selection.transformWith({
  "tag": "<a href='{{url}}'>{{text}}</a>"
})

var objects = transformed.objects()
/**
* objects = [
*   { "tag": "<a href='http://localhost'>localhost</a>" },
*   { "tag": "<a href='file://documents'>documents</a>" },
*   { "tag": "<a href='https://blahblah.com'>blah</a>" },
*   { "tag": "<a href='https://hahaha.com'>haha</a>" },
*   { "tag": "<a href='https://hohoho.com'>hoho</a>" }
* ]
*/

var values = transformed.values()
/**
* values = [
*   "<a href='http://localhost'>localhost</a>",
*   "<a href='file://documents'>documents</a>",
*   "<a href='https://blahblah.com'>blah</a>",
*   "<a href='https://hahaha.com'>haha</a>",
*   "<a href='https://hohoho.com'>hoho</a>"
* ]
*/

var keys = transformed.keys()
/**
* keys = ["tag", "tag", "tag", "tag", "tag"]
*/


var root = transformed.root()
/**
* root = {
*   "item": {
*     "tag": "<a href='http://localhost'>localhost</a>"
*   },
*   "items": [
*     { "tag": "<a href='file://documents'>documents</a>" },
*     { "tag": "<a href='https://blahblah.com'>blah</a>" },
*   ],
*   "nestedItems": {
*     "childItems": [
*       { "tag": "<a href='https://hahaha.com'>haha</a>" },
*       { "tag": "<a href='https://hohoho.com'>hoho</a>" }
*     ]
*   }
* };
*/

var transformed = ST.select({
  "{{#each items}}": {
    tag: "<a href='{{url}}'>{{text}}</a>"
  }
}).transform({ items: urls });

var root = transformed.root();
/*
root = {
  "item": { "tag": "<a href='http://localhost'>localhost</a>" },
  "items": [
    { "tag": "<a href='file://documents'>documents</a>" },
    { "tag": "<a href='https://blahblah.com'>blah</a>" }
  ],
  "nestedItems": {
    "childItems": {
      "tag": "<a href='http://hahaha.com'>haha</a>"
    },
    "tag": "<a href='https://hohoho.com'>hoho</a>"
  }
}
*/

var keys = transformed.keys();
/*
keys = ["tag", "tag", "tag", "tag", "tag"];
*/

var values = transformed.values();
/*
values = [
  "<a href='http://localhost'>localhost</a>",
  "<a href='file://documents'>documents</a>",
  "<a href='https://blahblah.com'>blah</a>",
  "<a href='http://hahaha.com'>haha</a>",
  "<a href='https://hohoho.com'>hoho</a>"
]
*/

var objects = transformed.objects();
/*
objects = [
  { "tag": "<a href='http://localhost'>localhost</a>" },
  { "tag": "<a href='file://documents'>documents</a>" },
  { "tag": "<a href='https://blahblah.com'>blah</a>" },
  { "tag": "<a href='http://hahaha.com'>haha</a>" },
  { "tag": "<a href='https://hohoho.com'>hoho</a>" }
]
*/

var paths = transformed.paths();

5.5.14 Example 3 - Nesting Templates

Sometimes you may want to reuse a template by nesting inside another template. In this case you just need to select a subtree of the parent template and plug in a child template.

var data = {
  "item": { "url": "http://localhost" },
  "items": [
    { "url": "file://documents" },
    { "url": "https://blahblah.com" }
  ],
  "nestedItems": {
    "childItems": [{
      "url": "http://hahaha.com",
      "text": "haha"
    }, {
      "url": "https://hohoho.com",
      "text": "hoho"
    }]
  }
};
var template = {
  "items": {
    "{{#each items}}": "{{partial}}"
  }
}
var partial = {
  "type": "label",
  "text": "{{name}}"
}
var selected = ST.select(template, function(key, val) {
  return val === '{{partial}}';
})
var finalTemplate = selected.transform({
  "partial": {
    "type": "label",
    "text": "{{name}}"
  }
}).root();

/*
  finalTemplate = {
    "items": {
      "{{#each items}}": {
        "type": "label",
        "text": "{{name}}"
      }
    }
  }
*/

5.6 Sample Projects

5.6.1 Declarative JSON API Template

Build JSON using a simple, human-readable, and declarative template instead of manually coding it.

See the example code at: https://github.com/jasonelle/st.js-api.template

Old way: Manually construct object


// app.js
app.get('/', function (req, res) {
  var response = {}
  response["current_user"] = {
    username: "@" + req.user.username,
    firstname: req.user.name.split(' ')[0],
    lastname: req.user.name.split(' ')[1]
  }
  var transformed_posts = db.posts.map(function(post){
    return {
      slug: post.slug
      permalink: "https://blahblahblah.blahblah/" + post.slug,
      post_title: post.title,
      post_content: post.content
    }
  })
  response["posts"] = transformed_posts
  res.json(response)
})

New way: Declarative approach with ja.js

// app.js
app.get('/', function (req, res) {
  res.json(ST.select(require('./template.json'))
              .transform({user: req.user, posts: db.posts})
              .root())
})

// template.json
{
  "current_user": {
    "username": "@{{user.username}}",
    "firstname": "{{user.name.split(' ')[0]}}",
    "lastname": "{{user.name.split(' ')[1]}}"
  },
  "posts": {
    "{{#each posts}}": {
      "slug": "{{slug}}",
      "permalink": "https://blahblahblah.blahblah/{{slug}}",
      "post_title": "{{title}}",
      "post_content": "{{content}}"
    }
  }
}

5.6.2 JSON as a JSON Query Language

Make complex API queries purely written in JSON.

Since templates in ST are written in JSON, you can pass them around anywhere just like any other data object.

Notice we’re not creating some new query language, it’s just JSON. No convoluted infrastructure to set up!

See the example code at: https://github.com/jasonelle/st.js-jsonql

5.6.3 App as data

Templates are normally used for views But the cool thing about JSON is it can be used to declaratively represent ANYTHING from Model to View to Controller.

What if we set executable functions as leaf nodes of an object, select & transform it, and then auto-trigger the resolved function? We have built a router in JSON!

Basically, the entire router logic is represented as a piece of data.

Browser


var rpc = {
  name: "add",
  args: [2,3,1]
}
fetch("http://localhost:3000", {
  method: "POST",
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(rpc)
).then(function(res) {
  console.log(res.json());
})

Server

// router.json
[{
  "{{#if 'name' in this}}": [{
    "{{#if name === 'add'}}": 'add_service'
  }, {
    "{{#elseif name === 'subtract'}}": [{
      "{{#if args.length === 2}}": 'subtract_service'
    }, {
      "{{#else}}": 'error_service'
    }]
  }]
}, {
  "{{#else}}": 'error_service'
}]

// express server
app.post('/',  (req, res) => {
  const Services = {
    add_service: function(){
        return Array.prototype.slice
                    .call(arguments)
                    .reduce((a,b) => {
                      return a+b;
                    }, 0)
    },
    subtract_service: function() {
      return arguments[0] - arguments[1]
    },
    error_service: function() {
      return 'error';
    }
  }
  const name = ST.transform(require('./router.json'), req.body);
  res.json(Services[name].apply(this, req.body.args));
});

5.6.4 Routerless Server

Let’s take the router example from right above. Since our router logic is just a JSON (router.json), we don’t even need it on the server side. What if we DON’T keep router.json on the server, but send it from the browser?

Browser

var router = [{
  "{{#if 'name' in this}}": [{
    "{{#if name === 'add'}}": 'add_service'
  }, {
    "{{#elseif name === 'subtract'}}": [{
      "{{#if args.length === 2}}": 'subtract_service'
    }, {
      "{{#else}}": 'error_service'
    }]
  }]
}, {
  "{{#else}}": 'error_service'
}];

var rpc = {
  name: "add",
  args: [2,3,1],
  router: router
}

fetch("http://localhost:3000", {
  method: "POST",
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(rpc)
).then(function(res) {
  console.log(res.json());
})

Server

// express server
app.post('/',  (req, res) => {
  const Services = {
    add_service: function(){
        return Array.prototype.slice
                    .call(arguments)
                    .reduce((a,b) => {
                      return a+b;
                    }, 0)
    },
    subtract_service: function() {
      return arguments[0] - arguments[1]
    },
    error_service: function() {
      return 'error';
    }
  }
  const name = ST.transform(req.body.router, req.body);
  res.json(Services[name].apply(this, req.body.args));
});

What’s going on here?

We are looking at a server WITHOUT a router. Instead of implementing a router on the server, we send the router itself as part of a network request!

This type of JSON-powered portability provides extreme flexibility when creating interfaces for microservices and RPC endpoints

Also, remember that you can bake validation, conditionals, loops, etc. all in a single JSON IPC/RPC call, which makes it extremely powerful and efficient.

5.6.5 Jasonette

Jasonette uses ja.js to:

    1. Transform an “action” JSON object into a native method call.
    1. Transform any client-side data into a view markup, and then into actual native view components.

Here’s an example where we describe an “action” (function) in a JSON object:

{
  "type": "$network.request",
  "options": {
    "url": "https://jsonplaceholder.typicode.com/posts"
  },
  "success": {
    "type": "$render",
    "options": {
      "data": "{{$jason}}"
    }
  }
}

Every function (called “action”) consists of up to four attributes:

    1. "type": Type of action to perform. Jasonette interprets this into an actual native method call.
    1. "options": Arguments to send to the action.
    1. "success": Success callback. You can chain another action here.
    1. "error": Error callback. You can chain another action here.

In above example we make a "$network.request" action call, with a payload of:

{
  "options": {
    "url": "https://jsonplaceholder.typicode.com/posts"
  }
}

This actually translates to a native Objective-c function call, and when it succeeds, it triggers its “success” callback which is another action:

{
  "success": {
    "type": "$render",
    "options": {
      "data": "{{$jason}}"
    }
  }
}

This is where ja.js comes in.

Whenever an action call is made, Jasonette automatically fills the $jason variable with the return value from its preceding action ($network.request) and runs a transform, thereby implementing an actual function call.