Skip to content

Declarative Hooks

Declarative hooks are hooks that are written within tackle files and almost mirror the functionality of python hooks. They are useful when you want to create reusable logic, validate schemas, or create self documenting CLI's out of tackle files. They can have strongly typed fields and can have object-oriented properties such as inheritance and methods that can be passed between hooks.

Basic Usage

Declarative hooks are keys that end with an arrow to the left, <- for public hooks / <_ for private hooks (more on this later), which is the same as hook calls (ie ->/_>) but in the opposite direction.

For instance the following shows both a declarative hook and a call of that hook.

declarative_hook<-:
  input: str
hook_call:
  ->: declarative_hook
  input: stuff

Declarative hooks have no required fields as in the example above the hook is simply used to validate the input and by default will simply return an object with its fields.

hook_call:
  input: stuff

Input fields

Declarative hooks can have input fields with types, defaults, validators, and other parameters that mirrors functionality of pydantic's Field. There are many forms that

In each of the previous examples, the input type for the target field was inferred by the default value but it is possible to make the input fields strongly typed just like python hooks. This can be done in two different ways, by giving the type as a string or as a map with named fields the same as pydantic's Field function.

When the types are given as literals, they are also by default required. For instance the following hook would require each of the inputs with their corresponding type.

some_hook<-:
  a_str: str
  a_bool: bool
  a_int: int
  a_float: float
  a_list: list
  a_dict: dict
call:
  ->: some_hook
  a_str: foo
  a_bool: true
  a_int: 1
  a_float: 1.2
  a_list: ['stuff', 'things']  
  a_dict: {'stuff': 'things'}

Additionally, hook fields can be declared with key value pairs corresponding to pydantic's Field inputs. For instance the following would be able to validate the type of an input field of type string:

some_hook<-:
  input:
    type: str
    default: foo
    pattern: ^foo.*  # Like a regex validate
    description: A bar
pass->: some_hook --input foo-bar
fail->: some_hook --input bar --try --except {{print('Not valid')}}

When building declarative CLIs, one often populates the description field for the help screen so the method has docs.

exec method

If you want the hook to actually do something, the hook will need to have an exec method similar to how a python hooks have the same. As a simple example, here we are creating a hook greeter that prints Hello world!:

greeter<-:
  exec:
    target: world
    hi_>: print Hello {{target}}!
call->: greeter

By default, declarative hooks return the entire context from the exec method so in the example above, the output would be:

call:
  target: world

This is because the key hi_> is a private hook call (see memory management for more info) and only the public context is returned.

Alternatively the target field could have been in the base of the hook and woul d

return key

If you want to only return a part of the exec call, the return key is available for this. For instance given the following example:

greeter<-:
  exec:
    target: world
    hi->: print Hello {{target}}!
  return: target
call->: greeter

The output would now be:

call: world

As the return key dereferences the target from the exec method.

TODO: Future versions will allow more flexible return inputs #93.

Field Default Type

The type field does not need to be populated if given a default and the same type can be inferred.

some_hook<-:
  input:
    default:
      - stuff
      - things
call:
  ->: some_hook --input foo --try
  except:
    p->: print Wrong type!!

Fields With Hooks

Hooks can be used for the field's default value and is useful if you want to call some hook when the value is not supplied. These hook field defaults can be written in a couple ways.

example.yaml

a_hook<-:
  literal_compact->: input
  literal_expanded:
    ->: input
  field_default_compact:
    type: str
    default->: input

Each one of these will in the absense of supplying a value call the input hook as below.

? literal_compact >>> foo
? literal_expanded >>> bar
? field_default_compact >>> baz

Resulting in the following context easily viewable with the following command:

tackle example.yaml a_hook -p
literal_compact: foo
literal_expanded: bar
field_default_compact: baz

Methods

Declarative hooks can have methods that can be called similar to how the exec method is called. For instance the following would print out "Hello world!".

words<-:
  hi: Wadup
  say<-:
    target: str
    exec:
      p->: print {{hi}} {{target}}

p->: words.say --hi Hello --target world!

Here you can see that there is method say that when called executes its own exec method which is able to access the base attribute hi. This is useful in many contexts where one wants to extend base objects with additional functionality.

Future versions are contemplating ways to do method overloading based on types. Stay tuned.

Additionally, methods are callable from the command line. For instance if the above was in a file .tackle.yaml, you could call the method with:

tackle words say --hi Hello --target world!

Extending Hooks

Tackle has the notion of extending hooks from base hooks similar to inheritance patterns found in OOP languages. This is useful if you have a schema that you want to use in multiple hooks or you want to create generic methods that apply to multiple schemas. For instance:

base<-:
  hi:
    default: Hello

words<-:
  extends: base
  say<-:
    target: str
    exec:
      p->: print {{hi}} {{target}}

p->: words.say --target world!

Here we can see some base hook base which is then extended in the words hook.

Note: Future versions of tackle will support inheriting from schema specs like OpenAPI.

Public vs Private Hooks

Hooks are divided into two namespaces, public and private hooks which are mainly used to inform the display of a help screen. For instance when running tackle example.yaml help against this file:

public<-:
  help: A public hook
private<_:
  help: A private hook

Only the public hook is shown as below:

usage: tackle example.yaml

methods:
    public     A public hook

Private hooks are those hooks that perform some internal reusable business logic whereas public hooks are those that you want the user to be able to call from the command line and be documented from the help screen.

More info in Declarative CLI docs on the details of the help screen and calling from the command line.