Tackle File Parsing Logic¶
This document aims to provide an overview of the core parsing logic for tackle files which are arbitrary yaml files that have hooks embedded in them.
Basics¶
Tackle sequentially parses arbitrary json or yaml files with the parser only changing the data structure when hooks are called denoted by forward arrows, ie ->
/_>
at the end of a key / item in a list. Hooks can perform a variety of different actions such as prompting for inputs, making web requests, or generating code and return values that are stored in the key they were called from. After any key / value / item in a list is parsed, it is available to be referenced / reused in additional hook calls through jinja rendering.
Hook Call Forms¶
Hooks can be called in directly in expanded and compact forms or within jinja as an extensions or filters. For instance running:
example.yaml
expanded:
->: input
message: Say hello to?
compact->: input Say hello to?
# Or with jinja
jinja_expression->: "{{input('Say hello to?')}}"
Which when run with tackle docs/scratch.yaml -pf yaml
:
? Say hello to? Alice
? Say hello to? Bob
? Say hello to? Jane
Results in the following context:
expanded: 'Alice'
compact: 'Bob'
jinja_expression: 'Jane'
In this example we are calling the input hook that prompts for a string input which has one mapped argument, message
, which in the compact form of calling input allows it to be written in a single line. The exact semantics of how arguments are mapped can be found in the python hooks and declarative hooks documentation.
The input hook has several parameters that are not mapped as arguments such as default
which can be used in both expanded and compact forms but not in jinja which relies on mapped arguments. For instance:
expanded:
->: input
message: Say hello to?
default: world
# Equivalent to
compact->: input Say hello to? --default world
# Notice the additional argument
print->: print Hello {{compact}} Hello {{expanded}}
Control Flow¶
Tackle also enables conditionals, loops, and other base methods that are also able to be expressed in both hook call forms. For instance here we can see a for
loop in both forms:
ttd:
- stuff
- and
- things
expanded:
->: input
for: ttd # Strings are rendered by default for `for` loops
message: What do you want to do?
default: "{{item}}" # Here we must explicitly render as default could be a str
# Equivalent to
compact->: input What do you want to do? --for ttd --default "{{ item }}"
For more information on loops and conditional, check out the hook methods documentation.
Public vs Private Hook Calls¶
Thus far all the examples have been of public hook calls denoted by ->
arrows which run the hook and store the value in the key but sometimes you might want to call hooks but not have the key stored in the output. To do this you would instead run a private hook denoted by _>
arrow. Such cases exist when you are dealing with a strict schema and want to embed actions / logic in that schema or you want to keep a clean context and ignore the output of a key. The output of a private hook call is still available to be used later in the same context and is only removed when the context changes such as when a tackle hook is called that parses another tackle file / provider.
For more information, check out the memory management docs.
Special Cases¶
While all logic can be expressed simply through calling hooks, several convenient shorthand forms exist for calling common hooks such as var
for rendering a variable and block
for parsing a level of the input.
Rendering Variables¶
Values / keys are not rendered by default but instead need to be rendered through a hook call. To make this easier, a special case exists where if the value of a hook call is wrapped with braces (ie key->: "{{ another_key }}"
) it is recursively rendered right away. For instance:
a_map:
stuff: things
reference->: "{{ a_map }}"
Would result in:
a_map:
stuff: things
reference:
stuff: things
This allows creation of renderable templates in keys that one can reuse depending on what the current context is. For instance:
stuff: things
a_map:
more-stuff: "{{ stuff }}"
reference->: "{{ a_map }}"
Would result in:
stuff: things
a_map:
more-stuff: "{{ stuff }}"
reference:
stuff: things
Blocks¶
Sometimes it is convenient to be able to apply logic to entire blocks of yaml for which there is a special case embedded in the parser. For instance, it is common to use a single / multi selector in a tackle file to restrict users to running a certain set of functions:
action:
->: select What are we doing today?
choices:
- code: Code tackle stuff
- do: Do things
code->:
if: action == 'code'
# Run a number of hooks conditional on the `action`
arbitrary:
contex: ...
gen->: tackle robcxyz/tackle-provider
open->: command touch code.py
# ...
do->:
if: action == 'do'
check_schedule->: webbrowser https://calendar.google.com/
# ...
If this example was run, the user would be prompted for a selection which based on their input, the block of code hooks would be executed based on the if
condition. Under the hood the parser is re-writing the input to execute a block
hook like this example though the code above makes it simpler:
code:
->: block
if: action == 'code'
items:
arbitrary:
contex: ...
gen->: tackle robcxyz/tackle-provider
open->: command touch code.py
Block render context¶
When writing blocks, one has access to two different render contexts, a local block context which is referenced and the outer global context. For instance:
stuff: things
foo: bar
code->:
# ...
foo: baz
inner-context->: "{{ foo }}"
outer-context->: "{{ stuff }}"
In this example, the key "inner-context" would equal "baz" while the "outer-context" is able to reference "stuff".
Check the memory management docs for more information on different contexts.
Side note on blocks - Try out match
hooks¶
For another way of conditionally parsing blocks of yaml, checkout the match
hook which performs similarly to match / switch case statements per the below example.
action:
->: select What are we doing today?
choices:
- code: Code tackle stuff
- do: Do things
run_action:
->: match
value: "{{ action }}"
case:
code:
gen->: tackle robcxyz/tackle-provider
# ...
do:
if: action == 'do'
check_schedule->: webbrowser https://calendar.google.com/
# ...
Check out the declarative hooks docs for patterns on how you can wrap this logic with reusable interfaces.