moo 無 tla
Parameterized moo Jsonnet

Table of Contents

Overview

When making a moo schema or other data structures in Jsonnet one often finds that some aspect can be nicely factored out so as to make the Jsonnet file more general.

Here we describe two ways that Jsonnet supports such factoring of information. moo rejects the first approach for reasons explained.

External Variables

Jsonnet provides a useful mechanism called "external variables". It is similar to querying the os.environ dictionary in Python. You may use it in Jsonnet anywhere you might use a value. For example:

// extvar.jsonnet
local myvar = std.extVar("MY_EXTERNAL_VARIABLE");
{
  abc: "Answer: %d" % myvar,
  xyz: "Command line says, %s" % std.extVar("MY_OTHER_VARIABLE"),
}

Using jsonnet directly you may compile this code with a command like:

$ jsonnet -V MY_OTHER_VARIABLE="hello" -V MY_EXTERNAL_VARIABLE=42 extvar.jsonnet

The std.extVar() form is very easy to use when writing Jsonnet but one quickly sees limitations. This is one place where moo is opinionated and so does not offer an equivalent to jsonnet -V. The reasons for this are:

  • There is no way to provide a default value so the user must always provide values.
  • There is no way to provide values internally in Jsonnet (the feature is called external variables after all). This limits ability to compose the Jsonnet into ever higher order structure.
  • This is effectively a "global variable" anti-pattern. One must analyze the body of code to understand where the external variables are applied.

Top-Level Arguments

Happily, Jsonnet provides a second feature to "inject" values into Jsonnet code which addresses the problems with "external variables". It is called "top-level arguments" or TLA. moo not just supports TLA but embraces it.

TLA works with a Jsonnet file that produces as its top-level result a Jsonnet function object. It is the arguments to this top-level function which are the TLAs. Jsonnet function arguments may be given default values and this removes one problem with the use of std.extVar(). And, given that the Jsonnet function may be evaluated as the top-level compiled object or called from other higher-level Jsonnet removes the other. Finally, since it is a functional programming pattern it is easy for developers to trace how the information is used.

Okay, on to how to use it.

Non-functional Jsonnet file

Let's start with some "non-functional" code (it works, but does not use a top-level function):

local var="hello";
local result="%s"%var;
result

This example is rather contrived. The var will be our variable of interest. The result represents some intermediate structure construction, possibly very complex in a real world use. The last line provides the value of the file as a whole.

We may compile this with moo or jsonnet:

moo compile examples/tla/notla.jsonnet
"hello"

Refactor to top-level function

To refactor this code, we define a top-level function and move var into its argument list and its body holds the intermediate structure construction:

function (var="hello") {
    local result = "%s"%var,
    return: result
}.return

There are certainly other ways to "shape" the function body. What is shown here is the use of an "inside-out pattern". The inner result value is used to hold the final construction in the context of an object. We then set another value called return which is exposed as the value of the function. This is admittedly overkill for this simple example but shows a common pattern used when the structure requires various intermediates.

Because of the default value for the TLA we may compile it as before:

moo compile examples/tla/tla.jsonnet
"hello"

Provide TLA value on CLI

And, finally, we may see how to actually "inject" a variant value:

moo -A var="hello world" compile examples/tla/tla.jsonnet
"hello world"

Using top-level function from Jsonnet

And one more example shows how we may reuse this same tla.jsonnet as ingredients to build yet higher-level structure in Jsonnet.

local tla = import "tla.jsonnet";
tla("hi from inside Jsonnet!")

Here, we provide a hard-wired value for the var. Of course, this Jsonnet file also could instead provide a function with TLAs and we may continue the trend. But, for simplicity we cap off the pattern.

Here is the exciting result:

moo compile examples/tla/usetla.jsonnet
"hi from inside Jsonnet!"

Taking TLA further with moo

Jsonnet allows setting of TLAs to simple scalar values as well as Jsonnet structure and moo takes this further to allow TLAs to be set with values or structure provided in any support moo format.

Some examples follow to show the power.

TLA code on command line

First we may write some Jsonnet code directly on the command line.

moo -A var="{a:42, b:'Jsonnet code from the CLI'}.b" compile examples/tla/tla.jsonnet
"Jsonnet code from the CLI"

Here, we use the "inside-out pattern" again because tla.jsonnet ultimately expects var to be a string. Other Jsonnet TLA may expect some complex structure and we could supply that. Take this most trivial function:

function(var={}) var
moo -A var="{a:42, b:'Jsonnet code from the CLI'}" compile examples/tla/passthrough.jsonnet
{
    "a": 42,
    "b": "Jsonnet code from the CLI"
}

TLA value from Jsonnet file

moo goes even further and checks if the -A argument "looks like" a file.

moo -A var=examples/tla/notla.jsonnet compile examples/tla/passthrough.jsonnet
"hello"

Here, we have reused the notla.jsonnet file but now its value is "injected" back into Jsonnet via var. Of course, compared to a pure-Jsonnet context this routing is very circuitous compared to using Jsonnet import.

However, it allows for moo to provide one more trick. Any TLA value which "looks like" a file to moo will be parsed and moo knows how to parse many languages (thanks to anyconfig).

TLA value from any file

This opens the door to a bit of craziness where one may supply data in JSON, INI, YAML, CSV, XLS spreadsheets or even XML:

<data>
  <a>42</a>
  <b>Hello from XML!</b>
</data>
moo -A var=examples/tla/data.xml compile examples/tla/passthrough.jsonnet
{
    "data": {
        "a": "42",
        "b": "Hello from XML!"
    }
}

When or when not to use TLA

It is almost never "wrong" to structure a Jsonnet file as a top-level function. At most, it means wrapping the otherwise top-level result in a function() and maybe adding the "inside-out" pattern. But this small cost makes the Jsonent file useful in more contexts.

The real effort is of course in factoring the code itself. But then, that's the point. In the process of factoring one may find that parts can be abstracted into many files each providing a top-level function(). Thus is the nature of well factored, functional programming.

One caution to consider. In applying TLA one should be sensitive to how the abstracted information that will be fed back through TLAs will ultimately be provided. In the context of a build system it may go into, eg, CMake files. One should evaluate if adding complexity there makes sense. To best provide an "external" TLA interface consider providing one or more layers of outer Jsonnet scope to "whittle down" the amount of abstracted parameters by providing sane defaults along the way.

One example of this layering from moo is how the omodel is used to connect a simple array of type schema structures to an overall schema expected by some of the codegen templates. The "model" is "just another Jsonnet" file and can house default values and bring together structure.

Both values and structure can be provided on the moo command line (see various main options to moo) but that puts more structure outside of Jsonnet which means the user must remember the right CLI args or the script or build system calling moo grows more complex, etc. Striking a balance can take "trial and anger" to get right.

When in doubt, use TLAs and provide another layer of Jsonnet!

Author: Brett Viren

Created: 2021-01-13 Wed 10:53

Validate