Harness (proof-of-concept)
==========================

Language-agnostic microservices boilerplate as a code.
You define service configuration and implementation,
`Harness` takes care for it's life cycle:

- load configuration: read, validate
- initialize environment: database and other outgoing connections
- start servers/subscribers
- graceful shutdown

Configuration
~~~~~~~~~~~~~

`Harness` uses Protocol Buffers as a configuration definition language
(``svc.proto`` file):

.. code-block:: protobuf

  syntax = "proto3";

  package cafe;

  import "harness/options.proto";
  import "harness/postgres.proto";
  import "harness/grpc.proto";

  message Configuration {
    bool debug = 1;
    harness.postgres.DSN db = 2 [(harness.options).input = "python/asyncpg.v1:Connection"];
    harness.grpc.Channel taskqueue = 3 [(harness.options).input = "python/grpclib.v1:Channel"];
    harness.grpc.Endpoint listen = 4 [(harness.options).output = "python/grpclib.v1:Server"];
  }

As you can see from this configuration, our service has:

- debug mode
- database connection
- gRPC channel to the taskqueue service
- gRPC server to serve incoming requests

We can provide values for this configuration using YAML file
(``config.yaml`` file):

.. code-block:: yaml

  db:
    value: postgres://postgres:postgres@db.cafe.svc.cluster.local:5432/coffee
  taskqueue:
    host: taskqueue.platform.svc.cluster.local
    port: 50051
  listen:
    host: 0.0.0.0
    port: 50051

Service Definition
~~~~~~~~~~~~~~~~~~

`Harness` must be able to run your service. In order to do this you must
provide a ``main`` function, it's signature looks like this:

.. code-block:: python3

  from svc_wires import WiresIn, WiresOut

  async def main(wires_in: WiresIn) -> WiresOut:
      ...
      return WiresOut(...)

Where ``WiresIn`` and ``WiresOut`` are generated by the `Harness` from your
service configuration:

.. code-block:: python3

  @dataclass
  class WiresIn:
      db: harness.resources.asyncpg.v1.Connection
      taskqueue: harness.resources.grpclib.v1.Channel

  @dataclass
  class WiresOut:
      listen: harness.resources.grpclib.v1.Server

When `Harness` runs your service, you receive already initialized environment in
a ``WiresIn`` structure. All you have to do is to setup your request handler
and return it with a ``WiresOut`` structure (``svc.py`` file):

.. code-block:: python3

  from harness.resources.grpclib.v1 import Server

  from svc_grpc import CoffeeMachineBase

  class CoffeeMachine(CoffeeMachineBase):
      ...

  async def main(wires_in: WiresIn) -> WiresOut:
      print('Connection:', wires_in.db.connection)
      print('Channel:', wires_in.taskqueue.channel)
      server_resource = Server([
          CoffeeMachine(
              db=wires_in.db.connection,
              taskqueue=wires_in.taskqueue.channel,
          ),
      ])
      return WiresOut(listen=server_resource)

Files Generation
~~~~~~~~~~~~~~~~

We use ``protoc`` compiler to generate boilerplate from your configuration:

.. code-block:: console

  $ protoc -I $HARNESS_PROTO --python_out=. --python_grpc_out=. --python_harness_out=. --mypy_out=. svc.proto

Where ``$HARNESS_PROTO`` is where to find `Harness` proto-files.

Runtime
~~~~~~~

Use ``harness`` command to run your service:

.. code-block:: console

  $ harness svc:main config.yaml
