Previous: Simple Transforms

Paths

Paths, or more properly Path Transforms, are where you begin to see the power of sUTL.

So far, none of the transforms we've seen do anything interesting; they've just evaluated to themselves. But Path Transforms are different.

Path Transforms let us provide a path into some JSON, and they evaluate to the value at that point in the path.

For example, take the following transform:

"^$.name"

This is a Path Transform. You read it left to right, as follows:

^ - This says that this string is a path transform, and distinguishes it from a string literal. 

$ - This is the selector, and says we are selecting from the source. So to evaluate this path, grab the source.

. - This is a path separator. It says that the thing after it will be something we can use to index into what we've got so far. In this case, we'll be indexing into the source.

name - This is the thing we'll be using to index in. If it was an integer, we could use it on an array as an index, or as an attribute in a dictionary as a string. But as this isn't an integer, it'll only work on a dictionary.

Long story short: This transform says "Take the source and select the value of "name" from it.

eg: if source is

source = {
    "name": "Fred",
    "addr": "thing st"
}

Then applying the transform "^$.name" evaluates to "Fred". 

Try it out:

That's pretty cool! But we can combine transforms and make this even cooler.

Remember our General Dictionary Transform? The values of attributes in the dictionary are sub-transforms. These could be paths. 

Maybe we want to change the shape of the source above? Using this transform:

transform = {
    "person": {
        "fullname": "^$.name",
        "title": "Mr"
    },
    "address": {
        "line1": "^$.addr"
    }
}

This transform is a dictionary, with two attributes: person and address. Both attributes have dictionaries as values, with more attributes which are either literals (the string "Mr"), or paths. How does it evaluate?

evaluate ( source, transform, scope, library )
= {
    "person": evaluate ( source, transform["person"], scope, library ),
    "address": evaluate ( source, transform["address"], scope, library ),
}
= {
    "person": {
        "fullname": evaluate ( source, transform["person"]["fullname"], scope, library ),
        "title": evaluate ( source, transform["person"]["title"], scope, library )
    },
    "address": {
        "line1": evaluate ( source, transform["address"]["line1"], scope, library )
    }
}
= {
       "person": {
        "fullname": "Fred",
        "title": "Mr"
    },
    "address": {
        "line1": "thing st"
    }
}

Try it out:

Selectors

We've seen that the selector "$" lets us select from the source. But there are a bunch of others.

  • $ - Selects from the source
  • @ - Selects from the scope
  • ~ - Selects from the transform
  • * - Selects from the library 

Let's have a look at what these do. Using the previous source, let's evaluate the following transform:

transform = {
  "source": "^$",
  "scope": "^@",
  "transform": "^~",
  "library": "^*"
}

This evaluates to the following result:

result = {
  "source": {
    "name": "Fred",
    "addr": "thing st"
  },
  "transform": {
    "source": "^$",
    "transform": "^~",
    "library": "^*",
    "scope": "^@"
  },
  "library": {},
  "scope": {
    "name": "Fred",
    "addr": "thing st"
  }
}

You can see that the scope is the same as the source in this transform (we'll see them differ in other types of transform), and that the library is empty.

Try it here:

Wildcards

Sometimes you don't know what a section of path should be, or don't want to know, or want to select multiple things with a path. There are two wildcards to help, which can be used in place of an index.

* - Matches all attributes or indices at the current level

** - Matches all attributes or indices at the current level and below, transitively.

When you use wildcards, the path can now match more than one result. If you use the ^ operator at the start of the string, only one matching value will be returned (if you're selecting from an list it'll be the head, if you're selecting from a dictionary any value might be returned, unspecified). 

Alternatively, you can use the & operator. This means the path evaluates to a list of matching results.

Here's an example:

Let's look at each attribute in the transform.

people: This uses &, which evaluates to a list of all possible results. It uses the scope selector (from here I will tend to use the scope selector instead of the source selector, for reasons which will become clear further on). It then chooses people from the path, then the single wildcard * which says we should select everything one level below people. So the result is a list containing all the dictionaries under people in the source.

alsopeople: This simply selects the value of the people attribute in the scope, which is a list containing all the dictionaries under people. It has exactly the same result as people above, but is calculated in a different way.

names: This selects the list ( ) from the scope ( @ ) of everything under people, indexing into those by name.

allnames: This selects names just like names, but looks more widely; it doesn't specify that we restrict our search to the people attribute. Instead, it'll select the value of any attribute of any dictionary in the scope called name, no matter how deeply nested. So it finds the top level attribute name: "person list", which names doesn't.

a person: This shows what happens when you combine the ^ operator with wildcards. It doesn't evaluate to a list, but only a single value. Because it matches multiple values, it evaluates to only one, the head of the list it matched.

allmypaths: This one is an odd one. It selects from the transform itself, and pulls out all the paths, as a list. It just shows how you can use a path transform in an unusual way.

Paths in List and Dictionary forms

The string form of paths that we've seen above is actually a shortened form. Paths can be expressed in a list form and in a dictionary form.

"^@.name"

can be expressed as

[ "^@", "name" ]

or

{
    "&": "@",
    "head": true,
    "args": ["name"]
}

You can see all three forms being used here:

Why are there three different forms?

In the section on builtin transforms, you'll see that the dictionary form above is actually the builtin dictionary evaluate transform, and in the builtins section you'll see that @ is a builtin, which selects the scope.

The list form is actually the builtin list evaluate transform; a more concise way of writing the evaluate builtin transform. It's also useful for mathematical calculations.

Finally, the string form of paths is really the builtin string evaluate transform. It's been specifically designed for pathing, but can be used for other builtins sometimes.

Next: Structured Builtins