Working with trees

Async trees

ori is especially good at dealing with trees. More specifically, ori is designed to work with async trees: a tree whose nodes can enumerate their keys and, given a key, can return the corresponding value. Many common data structures can be represented as async trees.

One way to define an async tree is in YAML format.

$ ori greetings.yaml
Alice: Hello, Alice.
Bob: Hello, Bob.
Carol: Hello, Carol.

ori can interpret this file as the following tree:

g Alice Hello, Alice. ->Alice Alice Bob Hello, Bob. ->Bob Bob Carol Hello, Carol. ->Carol Carol

The YAML data format shown above can be easier for people to read than formats like JSON. If you prefer, you can just as easily use the ubiquitous JSON format.

ori itself natively understands several types of async trees:

  • JSON
  • YAML
  • JavaScript objects
  • JavaScript Array, Map, or Set instances
  • JavaScript functions
  • folder trees
  • web sites (some operations require support for .keys.json files, discussed later)
  • any object that implements the AsyncTree interface

Extract specific values out of a tree

You can use path syntax to extract a specific value from a tree.

$ ori greetings.yaml/Alice
Hello, Alice.

The greetings.yaml tree is a flat list, but it can be a hierarchical tree or arbitrarily complex.

An async tree can also be invoked like a function, so you also have the option of using function call syntax:

$ ori "greetings.yaml('Alice')"
Hello, Alice.

You can easily combine ori features like JSON/YAML parsing, path syntax, and function invocation to have ori parse a specific value out of a tree and feed that directly to your function.

$ ori uppercase.js greetings.yaml/Alice
HELLO, ALICE.

Translate JSON to YAML and vice versa

You can use ori to transform a tree from one format to another. By default, ori renders trees in YAML format, but you can ask for JSON format with the @json function:

$ ori greetings.yaml
Alice: Hello, Alice.
Bob: Hello, Bob.
Carol: Hello, Carol.
$ ori @json greetings.yaml
{
  "Alice": "Hello, Alice.",
  "Bob": "Hello, Bob.",
  "Carol": "Hello, Carol."
}

In the other direction, you can render a JSON file as YAML with the @yaml function:

$ ori letters.json
{
  "a": "The letter A",
  "b": "The letter B",
  "c": "The letter C"
}
$ ori @yaml letters.json
a: The letter A
b: The letter B
c: The letter C

The @json function isn’t a specific YAML-to-JSON transformation; it can transform any tree to JSON text. Similarly, @yaml can transform any tree to YAML text.

Parse JSON/YAML files

You can use ori to parse a JSON or YAML file into a plain JavaScript object that your JavaScript function can then handle.

Suppose you have a focused function that does something with a flat, plain object. Perhaps it returns the text of an object’s values:

$ ori text.js
export default function text(obj) {
  return Object.values(obj).join("\t");
}

You can use the built-in @plain function to convert a YAML file to a plain JavaScript object, then pass that to the sample text function:

$ ori text.js @plain greetings.yaml
Hello, Alice.   Hello, Bob.     Hello, Carol.

Or pass a parsed JSON file to your function:

$ ori text.js @plain letters.json
The letter A    The letter B    The letter C

Separating the parsing from your function like this lets you keep your function as general as possible.

Render the current file system tree as a tree

The file system is just another tree that ori natively understands. If you give ori a path to a folder, it will treat that as a tree. For example, you can specify the current folder with a period (.):

$ ori .

This will render the complete contents of the current folder, including subfolders, in YAML.

You can capture that result to package up the current folder as a YAML file.

$ ori . > files.yaml

Or package the folder as JSON:

$ ori @json . > files.json

Unpack files into the file system

You can unpack the greetings in greetings.yaml into individual files:

$ ori greetings.yaml
Alice: Hello, Alice.
Bob: Hello, Bob.
Carol: Hello, Carol.
$ ori @copy greetings.yaml, @files/greetings
$ ls greetings
Alice   Bob     Carol
$ cat greetings/Alice
Hello, Alice.

The @files/greetings argument indicates that @copy should copy the input YAML tree to a file system tree under a folder named greetings. As a result, the key/value pairs in the YAML file are now individual files in a greetings folder.

The important point here is that all trees look the same to ori. It doesn’t matter whether a tree is defined in a single file like YAML, or a collection of loose files in the file system. Having unpacked the greetings.yaml file above, we can ask ori to display the greetings folder we just created:

$ ori greetings
Alice: Hello, Alice.
Bob: Hello, Bob.
Carol: Hello, Carol.

It looks like greetings is a YAML file, but it’s not — it’s really a folder. ori is just displaying the folder’s contents in the default YAML output format. Each line of that output is actually coming from a different file.

The greetings folder and the greetings.yaml file both define the same tree, even though the underlying data is stored in completely different ways and accessed via different APIs.

Process a folder tree as a JavaScript object

Because the greetings folder created in the above example is just another tree ori can process, you can feed it to the simple JavaScript text.js function shown earlier that displayed the text values of a plain JavaScript object.

$ ori text.js @plain greetings
Hello, Alice.   Hello, Bob.     Hello, Carol.

This connects two ideas:

  • A folder like greetings is a tree ori can understand.
  • ori can convert any tree to a plain JavaScript object with the @plain function.

This means that you can use the @plain function to convert a folder to a plain JavaScript object too. The keys will be the file/folder names, and the values will be the file contents or folder subtrees.

Writing code to work with folder and files this way can be much easier than using Node’s file system API directly. There is a performance trade-off implied by building an in-memory object to hold the file system data, but in many cases this is still very fast. And in practice it can much easier to manipulate a complete file system hierarchy as an in-memory object than working with a file system API.

Another important benefit of working with async trees is that you can change your mind later about how you want to represent data without having to rewrite code that processes that data. You could start a small project by representing data in a single file and then, if your needs change later, switch to representing that data in a hierarchical tree of files, or data stored as web resources.

 

Next: Transforming data and trees »