.. Licensed to the Apache Software Foundation (ASF) under one
   or more contributor license agreements.  See the NOTICE file
   distributed with this work for additional information
   regarding copyright ownership.  The ASF licenses this file
   to you under the Apache License, Version 2.0 (the
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   KIND, either express or implied.  See the License for the
   specific language governing permissions and limitations
   under the License.

.. include:: ../../common.defs

.. _developer-doc-adding-domains:

Creating New Domains
********************

In the event a new type of object or reference needs to be documented, and none
of the existing markup options or domains are appropriate, it is possible to
extend |RST| and Sphinx by adding custom domains.

Each domain may be designed to accept any number of required and optional
arguments, as well as any collection of domain options, and each option may be
designed to support arbitrary values, restricted (enumerated) values, or to
simply act as flags.

All custom domain definitions should be located in ``doc/ext/traffic-server.py``
and consist of, at a bare minimum, a domain class definition and a domain
reference class definition. Sphinx domains are implemented in Python.

For this section, we will use the contrived example of creating a domain which
permits us to define and reference a set of variables which are constrained by
the following characteristics:

#. Each variable in the domain must be one of known list of data types, which
   we will limit here to the possibilities of :literal:`integer`,
   :literal:`float`, :literal:`string`.

#. Where the data type is not specified, we can assume it is :literal:`string`.

#. Variables which are numeric in their type may have a range of permissible
   values.

#. Variables in the domain may still be present and supported in the system, but
   are planned to be removed in some future release.

#. Every variable is associated with a single URI protocol, though there is no
   validation performed on the value used to represent the protocol name.

As stated, this example is fairly contrived and would not match any particularly
likely real-world needs, but it will allow us to demonstrate the full extent of
custom domain definition without needless complexity, reader's suspension of
disbelief permitting.

For this chapter's purpose, we will call this domain simply *Variables*, and we
will construct classes which allow us to document variables thusly::

    .. ts:variable:: http_enabled http integer
       :deprecated:

       Enables (any positive, non-zero value) or disables (any zero or negative
       value) processing of HTTP requests.

And referencing of those variables defined with this domain via::

    :ts:variable:`http_enabled`

Defining the Domain
===================

Each domain is defined by a class which inherits from ``std.Target``. Several
class attributes are expected, which determine how domain object definitions are
processed. |TS| convention is to name each domain's class in camel case,
beginning with :literal:`TS` to prevent any class name collisions with builtin
Sphinx classes.

.. code::

    class TSVariable(std.Target):

We have named the domain's defining class as *TSVariable* and inherited from the
:literal:`std.Target` class. Given the earlier stated requirements, we need a
domain which supports at least two required attributes (a name, of course, and
a URI protocol with which it is associated) and a commonly defined, though
optional, third attribute (a data type). We'll deal with the value ranges and
deprecation status later.

.. code::

    class TSVariable(std.Target):
        required_arguments = 2
        optional_arguments = 1
        final_argument_whitespace = False
        has_content = True

We've now specified the appropriate number of required and optional arguments,
though not what each one happens to be or in what order the required arguments
need be written. Additionally, we've declared that definitions using this
domain do not permit whitespace in the final argument, but definitions can have
a block of text content which follows them and should be associated with the
item being defined.

.. note::

   Permitting whitespace in the final argument causes the final value of a valid
   definition to *slurp* the remaining content of the definition. Normally, each
   argument is separated by whitespace, thus ``foo bar baz`` would only be a
   valid definition if the domain's required and optional argument counts added
   up to exactly three. If the domain defined only two arguments as expected,
   but sets ``final_argument_whitespace`` to *True*, then the definition would
   be valid and the second argument in this case would be ``bar baz``.

Our requirements also state support for optional value ranges, and a flag to
indicate whether the variable is being deprecated. These can easily be
supported through the ``option_spec``, which allows for options to be tagged
on to a domain item, on the lines immediately following its definition.

.. code::

    class TSVariable(std.Target):
        ...
        option_spec = {
            'deprecated' : rst.directives.flag,
            'range' : rst.directives.unchanged
        }

For our example, ``deprecated`` is simply a boolean flag, and ``range`` will be
an arbitrary string on which we will perform no particular transformation or
validation (good behavior will be left up to those documenting their variables
with this domain). The ``rst.directives`` module may be consulted for a wider
range of predefined option types, including the ability to define your own
types which can perform any complexity of validation you may desire to
implement.

It would be good form to also include a docstring for the class explaining the
expected arguments in brief. With that included, our class now looks like:

.. code::

    class TSVariable(std.Target):
        """
        Description of a Traffic Server protocol variable.

        Required arguments, in order, are:

            URI Protocol
            Variable name

        Optional argument is the data type of the variable, with "string" the
        the default. Possible values are: "string", "integer", and "float".

        Options supported are:

            :deprecated: - A simple flag option indicating whether the variable
            is slated for removal in future releases.

            :range: - A string describing the permissible range of values the
            variable may contain.
        """

        option_spec = {
            'deprecated' : rst.directives.flag,
            'range' : rst.directives.unchanged
        }

        required_arguments = 2
        optional_arguments = 1
        final_argument_whitespace = False
        has_content = True

Every domain class must also provide a ``run`` method, which is called every
time an item definition using the domain is encountered. This method is where
all argument and option validations are performed, and where transformation of
the definition into the documentation's rendered output occurs.

The core responsibilities of the ``run`` method in a domain class are to
populate the domain's data dictionary, for use by references, as well as to
transform the item's definition into a document structure suitable for
rendering. The default title to be used for references will be constructed in
this method, and all arguments and options will be processed.

Our variables domain might have the following ``run`` method:

.. code::

    def run(self):
        var_name, var_proto = self.arguments[0:2]
        var_type = 'string'

        if (len(self.arguments) > 2):
            var_type = self.arguments[2]

        # Create a documentation node to use as the parent.
        node = sphinx.addnodes.desc()
        node.document = self.state.document
        node['objtype'] = 'variable'

        # Add the signature child node for permalinks.
        title = sphinx.addnodes.desc_signature(var_name, '')
        title['ids'].append(nodes.make_id('variable-'+var_name))
        title['names'].append(var_name)
        title['first'] = False
        title['objtype'] = node['objtype']
        self.add_name(title)
        title.set_class('ts-variable-title')

        title += sphinx.addnodes.desc_name(var_name, var_name)
        node.append(title)

        env.domaindata['ts']['variable'][var_name] = env.docname

        # Create table detailing all provided domain options
        fl = nodes.field_list()

        if ('deprecated' in self.options):
            fl.append(self.make_field('Deprecated', 'Yes'))

        if ('range' in self.options):
            fl.append(self.make_field('Value range:', self.options['range']))

        # Parse any associated block content for the item's description
        nn = nodes.compound()
        self.state.nested_parse(self.content, self.content_offset, nn)

        # Create an index node so Sphinx will list this variable and its
        # references in the index section.
        indexnode = sphinx.addnodes.index(entries=[])
        indexnode['entries'].append(
            ('single', _('%s') % var_name, nodes.make_id(var_name), '')
        )

        return [ indexnode, node, fl, nn ]

Defining the Domain Reference
=============================

Domain reference definitions are quite simple in comparison to the full domain
definition. As with the domain itself, they are defined by a single class, but
inherit from ``XRefRole`` instead. There are no attributes necessary, and only
a single method, ``process_link`` need be defined.

For our variables domain references, the class definition is a very short one.
|TS| convention is to name the reference class the same as the domain class, but
with :literal:`Ref` appended to the name. Thus, the domain class ``TSVariable``
is accompanied by a ``TSVariableRef`` reference class.

.. code::

    class TSVariableRef(XRefRole):
        def process_link(self, env, ref_node, explicit_title_p, title, target):
            return title, target

The ``process_link`` method will receive several arguments, as described below,
and should return two values: a string containing the title of the reference,
and a hyperlink target to be used for the rendered documentation.

The ``process_link`` method receives the following arguments:

``self``
    The reference instance object, as per Python method conventions.

``env``
    A dictionary object containing the environment of the documentation
    processor in its state at the time of the reference encounter.

``ref_node``
    The node object of the reference as encountered in the documentation source.

``explicit_title_p``
    Contains the text content of the reference's explicit title overriding, if
    present in the reference markup.

``title``
    The processed form of the reference title, which may be the result of domain
    class transformations or an overriding of the reference title within the
    reference itself.

``target``
    The computed target of the reference, suitable for use by Sphinx to
    construct hyperlinks to the location of the item's definition, wherever it
    may reside in the final rendered form of the documentation.

In our reference class, we have simply returned the processed title (allowing
the documentation to override the variable's name if desired, or defaulting to
the domain class's representation of the variable name in all other cases) and
the parser's computed target.

It is recommended to leave the ``target`` untouched, however you may choose to
perform any transformations you wish on the value of the ``title``, bearing in
mind that whatever string is returned will appear verbatim in the rendered
documentation everywhere references for this domain are used.

Exporting the Domain
====================

With both the domain itself and references to it now defined, the final step is
to register those classes as domain and reference handlers in a namespace. This
is done for |TS| (in its ``:ts:`` namespace) quite easily by modifying the
``TrafficServerDomain`` class, also located in ``doc/ext/traffic-server.py``.

The following dictionaries defined by that class should be updated to include
the new domain and reference. In each case, the key used when adding to the
dictionary should be the string you wish to use in documentation markup for
your new domain. In our example's case, we will choose ``variable`` since it
aligns with the Python classes we've created above, and their contrived purpose.

object_types
    Used to define the actual markup string

directives
    Defines which class is used to implement a given domain.

roles
    Defines the class used to implement references to a domain.

initial_data
    Used to initialized the dictionary which tracks all encountered instances of
    each domain. This should always be set to an empty dictionary for each
    domain.

dangling_warnings
    May be used to provide a default warning if a reference is attempted to a
    non-existent item for a domain.

