# yaq-traits

Command line tools for working with yaq traits and protocols.

## installation

yaq-traits can be installed via PyPI or conda-forge.

    $ pip install yaq-traits

    $ conda config --add channels conda-forge
    $ conda install yaq-traits

## definitions

Avro Protocol (AVPR)
  The Avro protocol is the fully specified description of the daemon. It describes the exposed method signatures (names, arguments, defaults to arguments, text description). In addition, yaq avpr files include information about the configuration, state, and traits of a daemon. Avpr is a JSON file.
TOML
  TOML is a configuration file. Here, we use TOML files to specify the minimum info about a daemon. (i.e. behavior and descriptions inherited via traits is not included in the TOML) A full reference is available below.
Trait
  A trait is simply a collection of exposed methods, configuration, and state used to encourage shared behavior among similar yaq daemons with different implementations

## usage

yaq-traits is a command line application.

### help

Help: learn more, right from your terminal.

    $ yaq-traits --help
    Usage: yaq-traits [OPTIONS] COMMAND [ARGS]...

    Options:
      --version  Show the version and exit.
      --help     Show this message and exit.

    Commands:
      check
      compose

Try `yaq-traits --help` to learn more about a particular command.

### list

List: list available traits

    $ yaq-traits list
    has-limits
    has-measure-trigger
    has-position
    has-turret
    is-daemon
    is-discrete
    is-homeable
    is-sensor
    uses-i2c
    uses-serial
    uses-uart

### compose

Compose: Convert a simplified TOML file to a fully specified AVPR. This takes a path to a TOML file as an argument, and prints the AVPR to standard out.

    $ yaq-traits compose my-daemon.toml > my-daemon.avpr

### get

Get: Retrieve a fully specified AVPR for a trait (similar to compose, but not a full daemon). This takes a name of a trait as an argument, and prints the AVPR to standard out.

    $ yaq-traits get has-limits > has-limits.avpr

### check

Check: Verify that an AVPR file matches current trait behavior. This takes a path to an AVPR file as an argument, and prints a table of traits and whether the trait was found to be accurate. If it fails, the traits which are not specified are explicitly printed and a nonzero exit status is returned. The primary intent of `check` is to be used by Continuous Integration tests, which consistently get the latest version and will alert you if there is a change to the traits.

    $ yaq-traits check fake-continuous-hardware.avpr
    +---------------------+----------+----------+
    | trait               | expected | measured |
    +---------------------+----------+----------+
    | has-limits          | true     | true     |
    | has-position        | true     | true     |
    | has-turret          | false    | false    |
    | is-daemon           | true     | false    |
    | is-discrete         | false    | false    |
    | is-homeable         | false    | false    |
    | uses-i2c            | false    | false    |
    | uses-serial         | false    | false    |
    | uses-uart           | false    | false    |
    | has-measure-trigger | false    | false    |
    | is-sensor           | false    | false    |
    +---------------------+----------+----------+
    Error: failed to verify expected trait(s):
      is-daemon

What to do when a check fails:

- check that you have the most up to date `yaq-traits`
- check the changelog for `yaq-traits` to see what has been changed recently
- In many cases, the accompanying behavior change will be implemented in the `core` package for your language (e.g. Python). If so, all you need to do is pin the version of core and rerun `yaq-traits compose`
- if the change requires your attention, make the necessary code changes and then rerun `yaq-traits compose`
- feel free to reach out to the yaq developers or raise an issue if you are unsure of what is needed

## yaq TOML file specification

In general, the TOML file is used to specify the minimal description of a daemon. Inherited behavior from traits are not included. You may assign or reassign the default value of the default, but should not change the documentation, type, or arguments to a message. The same format is used to specify traits within `yaq-traits`.

### frontmatter

These identifying information about the daemon are provided as top level keys.

protocol (string)
  The name of the daemon. The equivalent for a trait definition is trait.
doc (string)
  A description of the daemon, rendered at the top of its documentation page.
traits ({'items': 'string', 'type': 'array'})
  List of traits implemented by the daemon. In trait definitions, the analogous entry is requires, which lists traits that are implied by using that trait. If a traits is implied by other traits (e.g. `has-limits` requires `has-position`) the required trait need not be specified. `is-daemon` must be specified by all daemons.
hardware ({'items': 'string', 'type': 'array'})
  A list of supported hardware, formatted as 'make:model'. Supported hardware should also appear in known-hardware. Used only for building documentation.

### links

The links are a map of arbitrary keys to URLs. In general links to documentation, source, and a bugtracker are good to have. Additionally links to the manufacturer/library used are common.

    [links]  # all optional, arbitrary keys supported
    documentation = "https://yaq.fyi/daemons/example-daemon"
    source = "https://git.example.com/example-daemon"
    bugtracker = "https://git.example.com/example-daemon/-/issues"

### installation

Installation is just like links, except with the specific goal of pointing to installable package references. For python, this likely includes a link to PyPI and/or conda-forge.

    [installation]  # all optional, arbitrary keys supported
    PyPI = "https://pypi.org/project/yaqd-example"
    conda-forge = "https://anaconda.org/conda-forge/yaqd-example"

### types

All valid Avro types are valid for yaq.
In addition, `yaq-tratis` provides a definition for an N-dimensional homogeneous array, which can be used by putting the string `"ndarray"` as the type.
Unions of types are specified using TOML arrays.
Since TOML does not include a null value (and null is a valid default value, distinct from not having a default) the string `"__null__"` is used in place of the null literal. Note that when referring to the type, `"null"` is used, just as `"int"` is used to specify the integer type.
Named types (e.g. records and enums) may be defined inline where they are used, but may also be provided as an array of tables:

    [[types]]
    name="Greeting"
    type="record"
    [[types.fields]]
    name="message"
    type="string"

    [[types]]
    name="Curse"
    type="record"
    # You may find inline tables/arrays more readable here
    # Note that toml forbids multi-line inline tables (arrays can be multi-line)
    fields = [{name="message", "type"="string"}]

### config

Each config parameter is a table with the name of the parameter as the key within the `[config]` table with the following keys:

type (required)
  Avro type definition, commonly a string such as `"int"` or `"ndarray"`, but may be a table representing collection types or a record or an array representing a union of types.
doc (optional)
  A string description of the config parameter
default (optional)
  The default value if the config parameter is omitted in the daemon configuration. If no default is given, it is considered required.

A daemon may override the default value defined by a trait (or provide one where none was given). Additionally, rather then overwriting the doc, a key addendum may be added to provide additional context to the variable (e.g. communicating that a parameter is required despite a null default value being defined in the trait).

    [config]

    [config.a_binary_config]
    type = "boolean"
    default = false
    doc = "An example of a boolean config param"

    [config.an_optional_array]
    type = ["null", {type = "array", items = "int"}]
    default = "__null__"

    # Overriding a config from a trait
    [config.baud_rate]
    default = 57600
    addendum = "I want to provide more info about why 57600 is the default"

### state

The state is configured exactly the same as config, except that all state variable must have a default value. The same rules about overriding defaults and addenda apply to state variables.

    [state]

    [state.a_binary_state]
    type = "boolean"
    default = true
    doc = "An example of a boolean state variable"

### messages

Messages are the exposed remote procedure calls over the TCP interface. This follows closely the specification provided by Avro. Note that all errors in the Python implementation are treated as strings, though that may change in the future.
If the request field is omitted, the default is `[]`, i.e. no parameters.
If the response field is omitted, the default is `"null"`.
Messages defined by traits should be omitted, only messages unique to the daemon should be included. Messages should not be overridden, as the behavior being the same to the client program is the intent of the traits system.

    [messages.set_int]
    request = [{"name"="int_value", "type"="int"}]
    doc = "Set an example int value."

    [messages.get_int]
    response = "int"
    doc = "Get the example int value."

## yaq AVPR file specification

The AVPR files generated are compliant with the Avro specification, however `yaq-traits` does add additional information such that the avpr alone can be used to render the documentation.
The AVPR files include the complete list of `config` and `state` (including those inherited from traits). The links (including installation links) are also included. Additional keys specifying which trait provided a method or config/state variable are also added by `yaq-traits`

CC0: no copyright