# Custom item types

## Step 0: Understand expected_state and actual_state

To represent both the expected and actual state of an item, BundleWrap uses dictionaries with a specific structure:

* keys must be Unicode text
* every value must be of one of these simple data types:
    * bool
    * float
    * int
    * Unicode text
    * None
* ...or a list/tuple containing only instances of one of the types above

Additional information can be stored in these state-dicts by using keys that start with an underscore. You may only use this for caching purposes (e.g. storing rendered file template content while the "real" `actual_state` information only contains a hash of this content). BundleWrap will ignore these keys and hide them from the user. The type restrictions noted above do not apply.

## Step 1: Create an item module

Create a new file called `/your/bundlewrap/repo/items/foo.py`. You can use this as a template:

    from bundlewrap.items import Item


    class Foo(Item):
        """
        A foo.
        """
        BUNDLE_ATTRIBUTE_NAME = "foo"
        ITEM_ATTRIBUTES = {
            'attribute': "default value",
        }
        ITEM_TYPE_NAME = "foo"
        REQUIRED_ATTRIBUTES = ['attribute']

        @classmethod
        def block_concurrent(cls, node_os, node_os_version):
            """
            Return a list of item types that cannot be applied in parallel
            with this item type.
            """
            return []

        def __repr__(self):
            return "<Foo attribute:{}>".format(self.attributes['attribute'])

        @property
        def expected_state(self):
            """
            Return a dict describing the expected state of this item on
            the node. Returning `None` instead means that the item
            should not exist.

            Implementing this method is optional. The default implementation
            uses the attributes as defined in the bundle.
            """
            raise NotImplementedError

        @property
        def actual_state(self):
            """
            Return a dict describing the actual state of this item on
            the node. Returning `None` instead means that the item does
            not exist on the node.

            For the item to validate as correct, the values for all keys
            in self.expected_state have to match this actual_state.
            """
            raise NotImplementedError

        def display_on_create(self, expected_state):
            """
            Given an expected_state dict as implemented above, modify it to
            better suit interactive presentation when an item is created. If
            there are any when_creating attributes, they will be added to
            the expected_state before it is passed to this method.

            Implementing this method is optional.
            """
            return expected_state

        def display_on_fix(self, expected_state, actual_state, keys):
            """
            Given expected_state and actual_state as implemented above, modify them to
            better suit interactive presentation. The keys parameter is a
            set of keys whose values differ between expected_state and actual_state.

            Implementing this method is optional.
            """
            return (expected_state, actual_state, keys)

        def display_on_delete(self, actual_state):
            """
            Given an actual_state dict as implemented above, modify it to
            better suit interactive presentation when an item is deleted.

            Implementing this method is optional.
            """
            return actual_state

        def fix(self, status):
            """
            Do whatever is necessary to correct this item. The given ItemStatus
            object has the following useful information:

                status.keys_to_fix           set of expected_state keys that need fixing
                status.expected_state        cached copy of self.expected_state
                status.actual_state          cached copy of self.actual_state
            """
            raise NotImplementedError

        def get_auto_attrs(self, items):
            """
            Return a dict with any number of attributes. The respective
            sets will be merged with the user-supplied values. For example:

                return {
                    'needs': {
                        'file:/foo',
                    },
                }

            Note that only attributes from ALLOWED_ITEM_AUTO_ATTRIBUTES are
            allowed (see BundleWrap source code).
            """
            return {}

<br>

## Step 2: Define attributes

`BUNDLE_ATTRIBUTE_NAME` is the name of the variable defined in a bundle module that holds the items of this type. If your bundle looks like this:

    foo = { [...] }

...then you should put `BUNDLE_ATTRIBUTE_NAME = "foo"` here.

`ITEM_ATTRIBUTES` is a dictionary of the attributes users will be able to configure for your item. For files, that would be stuff like owner, group, and permissions. Every attribute (even if it's mandatory) needs a default value, `None` is totally acceptable:

    ITEM_ATTRIBUTES = {'attr1': "default1"}

`ITEM_TYPE_NAME` sets the first part of an items ID. For the file items, this is "file". Therefore, file ID look this this: `file:/path`. The second part is the name a user assigns to your item in a bundle. Example:

    ITEM_TYPE_NAME = "foo"

`REQUIRED_ATTRIBUTES` is a list of attribute names that must be set on each item of this type. If BundleWrap encounters an item without all these attributes during bundle inspection, an exception will be raised. Example:

    REQUIRED_ATTRIBUTES = ['attr1', 'attr2']

<br>

Step 3: Implement methods
-------------------------

You should probably start with `actual_state()`. Use `self.run("command")` to run shell commands on the current node and check the `stdout` property of the returned object.

The only other method you have to implement is `fix`. It doesn't have to return anything and just uses `self.run()` to fix the item. It receives a `status` object of type `ItemStatus` which carries the state-dicts `expected_state` and `actual_state`. Also, a set of additional keys are supplied which can help in writing efficient fix-methods:

- `keys_to_fix` contains a list of keys that differ between the `expected_state` and the `actual_state`
- `must_be_deleted` and `must_be_created` are booleans indicating that one the state-dicts is empty

`block_concurrent()` must return a list of item types (e.g. `['pkg_apt']`) that cannot be applied in parallel with this type of item. May include this very item type itself. For most items this is not an issue (e.g. creating multiple files at the same time), but some types of items have to be applied sequentially (e.g. package managers usually employ locks to ensure only one package is installed at a time).

If you're having trouble, try looking at the [source code for the items that come with BundleWrap](https://github.com/bundlewrap/bundlewrap/tree/master/bundlewrap/items). The `pkg_*` items are pretty simple and easy to understand while `files` is the most complex to date. Or just drop by on [IRC](irc://irc.libera.chat/bundlewrap) or [GitHub](https://github.com/bundlewrap/bundlewrap/discussions), we're glad to help.
