#
Language Elements
When a Protean transform is executed, the result is determined by a specification (or spec), which is a JSON object whose nodes are the components of the spec. All specs (whether component or root) resolve to an output JSON object that may vary given different input JSON objects.
#
Untyped Spec
The simplest example of a spec is formed from basic JSON nodes where no object node contains the key #type
and
no string contains a JSONPath. In this case, the output will be a verbatim copy of the spec and the input
is completely ignored.
When a string does contain a JSONPath, that string node will resolve to the result of applying the JSONPath to
the input JSON object, or other source objects depending on the syntax used (see
#
Typed Spec
If an object does contain the key #type
, the behaviour will be determined by the string value of #type
(e.g. for_each
),
and the other key/value pairs will be used as parameters for the spec of that type instead of being rendered in the output directly.
Typically, the value of component spec parameters will themselves be component specs, allowing logical components and static templates to be plugged into one another and chained together arbitrarily.
A complete reference for all typed specs can be found in the Component Reference section.
#
Paths
Proteus uses JSONPath to pull data from various sources into the output of a Protean transform. It provides idiosyncratic extensions to the basic syntax of JSONPath to achieve this.
#
Atomic Paths
A single path is formed from three constituent parts. Listing them in the order they appear in the path, they are: flags, source specifier, and the base path.
// In this example, "[R,E]" are the flags, "$" is the source specifier and ".path.to.value" is the base path
[R,E]$.path.to.value
Flags take the form of a comma-delimited list of upper-case characters enclosed by square brackets.
When a flag is present, it will activate certain behaviours.
For example, the [R]
flag will cause an exception to be thrown if a path is not resolvable in the input document,
rather than returning null, as is the default behaviour.
The source specifier can be one of the following characters: $
, %
, &
or #
, which cause the path to be resolved
against the input, properties
map, arguments
map or spec, respectively. The properties
map is populated by the Glyph/transform request
config, whereas the contents of the arguments
map is driven by the internal logic of certain spec components.
The base path is the part that conforms to the natural syntax of JSONPath (except for omitting the leading $
,
which is subsumed by the source specifier). All ordinary JSONPath features are available such as predicates and recursive descent:
[R]%.properties.path[0].with[?(@.predicate == 'something')]['and']..recursive..descent
.
Proteus provides scoping by retaining a stack to which object references are pushed each time a scoping operation occurs (see Component Reference for details on scoping operations). For example, let the input be a JSON object that contains a list of objects and a string.
{
"type": "object",
"list": [
{
"name": "item_1"
},
{
"name": "item_2"
},
{
"name": "item_3"
}
]
}
We can iterate over the list using a for_each
component, but this will scope our paths to each entry of the list.
However, if we use $$
instead of $
, then it will always refer to the root of the input. E.g. the following spec
{
"items": {
"#type": "for_each",
"values": "$.list",
"spec": {
"id": "$.name",
"type": "$$.type"
}
}
}
will yield output
{
"items": [
{
"id": "item-1",
"type": "object"
},
{
"id": "item-2",
"type": "object"
},
{
"id": "item-3",
"type": "object"
}
]
}
If we want to refer to a specific element of the stack, then include the index of the desired element between the dollar signs, enclosed in parentheses:
$(2)$.path.resolved.against.third.stack.element
Note that the index is resolved cyclically, so $(-1)$
refers to the last element of the stack (so is equivalent to $
).
It follows that $(0)$
is equivalent to $$
.
We use the qualifier "atomic" when distinguishing this type of path from composite paths discussed in the next section.
#
Composite Paths
We can mix static strings with those obtained from paths by embedding atomic paths in the string and surrounding each in curly braces. E.g.
{
"composite_value": "This is a {$.adjective} example of {$$.verb}ing composite {[R]%.paths}"
}
These can be used to form data-dependent atomic paths using the [C]
flag:
{
"my_value": "[C]$.im.not.sure.what.{%.key}.i.want.yet"
}
In the above example, the path %.key
would be resolved first (against properties in this case), then the result would be
substituted into the outer path before it is resolved against input.