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.
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.
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.
Let’s start with some “non-functional” code (it works, but does not use a top-level function):
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"
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:
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"
And, finally, we may see how to actually “inject” a variant value:
moo -A var="hello world" compile examples/tla/tla.jsonnet
"hello world"
And one more example shows how we may reuse this same tla.jsonnet
as
ingredients to build yet higher-level structure in 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!"
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.
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:
moo -A var="{a:42, b:'Jsonnet code from the CLI'}" compile examples/tla/passthrough.jsonnet
{
"a": 42,
"b": "Jsonnet code from the CLI"
}
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).
This opens the door to a bit of craziness where one may supply data in JSON, INI, YAML, CSV, XLS spreadsheets or even XML:
moo -A var=examples/tla/data.xml compile examples/tla/passthrough.jsonnet
{
"data": {
"a": "42",
"b": "Hello from XML!"
}
}
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!