Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Views from JSON

Graph to page with XSLT turned the kernel’s readouts into pages with a stylesheet, because the kernel already publishes those readouts as graphs. Most data you will want to show is not a graph. It arrives as a JSON document from an HTTP API, as a CSV export from a spreadsheet, as rows from a database. This chapter and the next build a view over data like that, in the form it already has. Nothing in either chapter turns the data into RDF.

The data is a table everyone knows: the eight planets, each with its name, its kind and the length of its year in Earth days. The kernel inspector serves it twice, as a JSON document and as a CSV table, so that the next chapter can show one view over both.

The data, as it is

The JSON document is an atom, like the note in A page is a composed resource: the one kind of name in this chapter that holds state. Read it:

[
  { "name": "Mercury", "kind": "terrestrial", "year": 88 },
  { "name": "Venus", "kind": "terrestrial", "year": 225 },
  { "name": "Earth", "kind": "terrestrial", "year": 365 },
  { "name": "Mars", "kind": "terrestrial", "year": 687 },
  { "name": "Jupiter", "kind": "gas giant", "year": 4333 },
  { "name": "Saturn", "kind": "gas giant", "year": 10759 },
  { "name": "Uranus", "kind": "ice giant", "year": 30687 },
  { "name": "Neptune", "kind": "ice giant", "year": 60190 }
]
[computed]

That is the whole input: an array of records, each one an object. The view will be a table with one row per record.

A row is a template

The card in the first chapter was a template whose markers named resources. A row needs something slightly different: its values are not resources, they are the fields of one record. Compose (ikigai-fn’s template language) already has a marker for that. $h{{name}} splices the template’s argument name, escaped as text, the same way $h{urn:…} splices a resource. So the row template is three arguments in a table row:

<tr><th scope="row">$h{{name}}</th><td>$h{{kind}}</td><td>$h{{year}}</td></tr>
[computed]

Fill it with name=Earth kind=terrestrial year=365 and you have Earth’s row. What is missing is the step in between: take each record of the array and fill the template with that record’s fields as its arguments.

Once per record

Compose fills one template once. A table wants the row template filled once for every record, and compose has no “for each”: a template can splice a resource or an argument, but nothing in it repeats. Writing this chapter is how that gap was found, and it was filled where it belongs, in ikigai-fn beside compose. Since ikigai-fn 0.6.0 there is a resource for exactly this step:

urn:iki:fn:each?src=<a JSON array>&template=<a row template>: the template filled once for each element of the array, with the element’s fields as its arguments, concatenated in order.

An element’s fields reach the template only through an escaping marker ($h), so a field that holds markup arrives as text, and a template that tried to splice a field as it is would be refused. A string field is its text and a number its shortest decimal form, which is why the years arrive as 88:

<tr><th scope="row">Mercury</th><td>terrestrial</td><td>88</td></tr>
<tr><th scope="row">Venus</th><td>terrestrial</td><td>225</td></tr>
<tr><th scope="row">Earth</th><td>terrestrial</td><td>365</td></tr>
<tr><th scope="row">Mars</th><td>terrestrial</td><td>687</td></tr>
<tr><th scope="row">Jupiter</th><td>gas giant</td><td>4333</td></tr>
<tr><th scope="row">Saturn</th><td>gas giant</td><td>10759</td></tr>
<tr><th scope="row">Uranus</th><td>ice giant</td><td>30687</td></tr>
<tr><th scope="row">Neptune</th><td>ice giant</td><td>60190</td></tr>
[computed]

That is a composition, not code the inspector carries: the inspector binds ikigai-fn’s each at its conventional name and writes nothing for it.

The view

The view is one more template, with each as a marker. Read it, and notice that it never names the JSON document. It names records:{source}, so the same template serves every source:

<section id="view" class="inspect">
<h1>Planets</h1>
<p>One view, read from <code>urn:iki:tutorial:inspect:records:$h{{source}}</code>. The view does not know which source it was given: the same two templates show the JSON document and the CSV table.</p>
<table class="inspect-table">
<caption>The eight planets, from the $h{{source}} source</caption>
<thead><tr><th scope="col">Planet</th><th scope="col">Kind</th><th scope="col">Orbit (Earth days)</th></tr></thead>
<tbody>
$r{urn:iki:fn:each?src=urn:iki:tutorial:inspect:records:{source}&template=urn:iki:tutorial:inspect:template:planet-row}</tbody>
</table>
</section>
[computed]

records:json is a composition too. Its template is one marker that splices the JSON document, so for this source the records ARE the document. The inspector binds the view at fragment:planets-{source}, with the same composeOver the card used, and ikigai-ui’s shell turns it into a page at page:planets-json, in the inspector’s navigation beside the graph views:

    space
        .bind(
            Exact::new(format!("{BASE}data:planets.json")),
            atom_endpoint("inspect-data-json", "a JSON document", JSON, PLANETS_JSON),
        )
        .bind(
            Exact::new(format!("{BASE}data:planets.csv")),
            atom_endpoint("inspect-data-csv", "a CSV table", CSV, PLANETS_CSV),
        )
        .bind(
            Exact::new(format!("{BASE}records:json")),
            composed(
                "urn:iki:tutorial:inspect:template:records-json",
                "inspect-records-json",
            ),
        )
        .bind(
            Exact::new(format!("{BASE}records:csv")),
            composed(
                "urn:iki:tutorial:inspect:template:records-csv",
                "inspect-records-csv",
            ),
        )
        .bind(
            template(&format!("{BASE}fragment:planets-{{source}}")),
            composed(
                "urn:iki:tutorial:inspect:template:planets",
                "inspect-fragment-planets",
            ),
        )
        .bind(Exact::new("urn:iki:fn:each"), ikigai_fn::each())

Here is the view, in this page’s kernel:

<section id="view" class="inspect">
<h1>Planets</h1>
<p>One view, read from <code>urn:iki:tutorial:inspect:records:json</code>. The view does not know which source it was given: the same two templates show the JSON document and the CSV table.</p>
<table class="inspect-table">
<caption>The eight planets, from the json source</caption>
<thead><tr><th scope="col">Planet</th><th scope="col">Kind</th><th scope="col">Orbit (Earth days)</th></tr></thead>
<tbody>
<tr><th scope="row">Mercury</th><td>terrestrial</td><td>88</td></tr>
<tr><th scope="row">Venus</th><td>terrestrial</td><td>225</td></tr>
<tr><th scope="row">Earth</th><td>terrestrial</td><td>365</td></tr>
<tr><th scope="row">Mars</th><td>terrestrial</td><td>687</td></tr>
<tr><th scope="row">Jupiter</th><td>gas giant</td><td>4333</td></tr>
<tr><th scope="row">Saturn</th><td>gas giant</td><td>10759</td></tr>
<tr><th scope="row">Uranus</th><td>ice giant</td><td>30687</td></tr>
<tr><th scope="row">Neptune</th><td>ice giant</td><td>60190</td></tr>
</tbody>
</table>
</section>
[computed]

Every part of it is cached, so asking again computes nothing:

<section id="view" class="inspect">
<h1>Planets</h1>
<p>One view, read from <code>urn:iki:tutorial:inspect:records:json</code>. The view does not know which source it was given: the same two templates show the JSON document and the CSV table.</p>
<table class="inspect-table">
<caption>The eight planets, from the json source</caption>
<thead><tr><th scope="col">Planet</th><th scope="col">Kind</th><th scope="col">Orbit (Earth days)</th></tr></thead>
<tbody>
<tr><th scope="row">Mercury</th><td>terrestrial</td><td>88</td></tr>
<tr><th scope="row">Venus</th><td>terrestrial</td><td>225</td></tr>
<tr><th scope="row">Earth</th><td>terrestrial</td><td>365</td></tr>
<tr><th scope="row">Mars</th><td>terrestrial</td><td>687</td></tr>
<tr><th scope="row">Jupiter</th><td>gas giant</td><td>4333</td></tr>
<tr><th scope="row">Saturn</th><td>gas giant</td><td>10759</td></tr>
<tr><th scope="row">Uranus</th><td>ice giant</td><td>30687</td></tr>
<tr><th scope="row">Neptune</th><td>ice giant</td><td>60190</td></tr>
</tbody>
</table>
</section>
[cached]

A write is a cut, here too

The JSON document is an atom, so writing it cuts its golden thread, and everything made from it is recomputed on the next read: the records, each row, the view. Replace it with a one-record document:

written
[uncacheable]
<section id="view" class="inspect">
<h1>Planets</h1>
<p>One view, read from <code>urn:iki:tutorial:inspect:records:json</code>. The view does not know which source it was given: the same two templates show the JSON document and the CSV table.</p>
<table class="inspect-table">
<caption>The eight planets, from the json source</caption>
<thead><tr><th scope="col">Planet</th><th scope="col">Kind</th><th scope="col">Orbit (Earth days)</th></tr></thead>
<tbody>
<tr><th scope="row">Ceres</th><td>dwarf planet</td><td>1682</td></tr>
</tbody>
</table>
</section>
[computed]

Every value reached the page through $h, so a record whose name holds markup shows the markup as text, as the note did. A document that is not an array of objects fails the view with the parser’s reason, and never renders half a table.

Reaching into a larger document

The planets document is the array itself. A real API answer usually is not: the array sits inside an envelope, {"count": 8, "planets": [ … ]}, and the view wants .planets. Compose cannot reach into a JSON document by itself. ikigai-json can: urn:json:query runs a jq program (through jaq, sandboxed and bounded) over a JSON resource. Ask it for the first record’s name in the document you just wrote:

Ceres
[computed]

So records:json for an envelope is one marker, $r{urn:json:query?src=urn:…:planets.json&q=.planets}, and nothing else in this chapter changes. ikigai-fn 0.6.0 has a marker for the other common case: $j{<src> <program>} splices the result of a jq program as escaped text, so a template can show one field of a document ($j{urn:…:planets.json .[2].name}) without a resource of its own.

⚠ If you edit that cell, keep the program small. The sandbox bounds what a program may do, and in a native kernel a program past a bound is refused with a reason; in this page’s WebAssembly kernel the refusal stops the kernel instead, and the page has to be reloaded.

When a graph helps

JSON can become a graph. ikigai-jsonld reads JSON-LD, and a JSON-LD context maps plain JSON’s keys to vocabulary terms, after which everything in the XSLT chapter applies, and SPARQL too. That is worth doing when you want what a graph gives you: joins with other graphs, shapes to validate against, a query language. It is not the price of a view. The table above needed none of it.

What was built, and what it rules out

A view over a JSON document: an atom, two templates and three compositions (urn:iki:fn:each, the records, the view), and no code of the inspector’s own beyond the atom. Ruled out: converting the data to another model before it can be shown, and parsing JSON by hand.

Next, the same view from a CSV table: Views from CSV: one model, two sources.