unifold 0.3.6

Wiring in practice#

The rules of wiring are in the standard, section 3. This chapter shows how to use them: what to declare, how to name it, and the patterns of real repositories.

Reading a line#

wiring:
  billing.clock: platform.clock

"The need clock of billing is met by the capability clock of platform." The key is the side that needs, the value the side that provides. The unit holding the line is the one place where both sides are visible. billing itself never says who supplies its clock.

What is a need and what is not#

Declare a need when another unit of the project provides the thing, when a stand-in can replace it, or when it is an external system that the project depends on by contract.

Do not declare a need for what every unit of that sort has anyway: the language runtime, the standard library, the operating system, packages that the build tool fetches from a registry, logging.

At the root, a capability is an interface that code outside the project is written against or that a person uses, such as a library API, an HTTP API, a command line or a UI, and a need is something outside that the project's code uses, such as a device, a service or the platform. A project that nobody programs against or uses directly, such as a controller that reads sensors and switches a relay, has needs and no capability. Its scenarios start where a unit reacts to a need, as the scenario take-a-reading of plantwatch starts with device/sampling -> device/hal.alarm, and what it does is observed through the fakes of its needs.

Names#

Passing a need up#

A need that no sibling can meet goes to the parent, and from there upward until a unit meets it or the root states it as a need of the project. Each step is one line, child.need: need, and the names may differ: alerts.push: push-service. Nothing connects by an equal name; the check reports the missing line.

Several children, one provider#

When several children need the same thing from one provider, every child gets its own line; there is no wildcard:

wiring:
  cart.clock: platform.clock
  checkout.clock: platform.clock
  shipping.clock: platform.clock

The repetition is the point: the wiring lists every connection inside the unit, and adding a child cannot change one without a change to a manifest. Where three or more children pass a need up to the same need of the parent, the diagram lists them once below the picture, under "Shared connections, not drawn".

Alternatives and stand-ins#

wiring:
  test-rig.device-under-test:
    real: device.ble-link
    simulated: emulator.ble-link

A stand-in, such as an emulator, a fake or an in-memory store, is a provider like any other. The first alternative is the default. Use the same names for all needs that switch together.

unifold does not say which alternative a build or deployment uses. That stays in the build or deployment files. An alternative that does less than the need expects is not an alternative; it is another capability.

Alternatives implement one definition, so every alternative names the same contract as the need:

# search/unifold.yaml
needs:
  matcher:
    summary: Finds a pattern in text
    contract: ../docs/references/matcher.md
# regex/unifold.yaml
capabilities:
  matcher:
    summary: Regular expression search
    contract: ../docs/references/matcher.md

Adapters#

If a need and its provider state different kinds, for example library and http, the checker reports it. Usually a part is missing: an adapter, which is a unit that needs the one and provides the other. Do not change the kind of the need to make the finding go away.

Kinds of units#

kind on a unit is free text, and three values carry rules:

Kind Meaning Rule
logic A pure calculation. No input or output, no memory between calls. Takes time and randomness as parameters. Logic throughout: every child is a logic unit, the final provider of each need is a logic unit, and no need reaches a need of the root.
stateful Keeps state, such as a session or a state machine. May need anything.
adapter The one unit that talks to one external system, a need of the root. The final provider of none of its needs is an adapter.

A need of the root is finally reached only by adapters, unless it is marked platform: true. That mark is for a service of the host such as time or randomness, which is not an external system and whose contract describes a controllable fake, such as a clock that tests move forward. A unit that states no kind is not judged by this rule. A unit with children carries a kind too; it says what the box is to its parent's wiring. A logic composite has only logic children; a composite that holds state anywhere is stateful; one that exists to wrap one external system is an adapter. The root needs no kind but may state one.

What the kinds give: a logic unit can be tested without any fake, so its tests are the cheapest and its examples the most trustworthy; an adapter is the only place where an external protocol lives, so an integration harness puts fakes at the needs of the root and nowhere else; a stateful unit is where state machines live, so its runtime.md is where a reviewer looks for them. Any other value is allowed; the checker says so once, as information, and applies no rule.

Scenarios#

A scenario tells one story as the chain of calls it causes, in the manifest of a unit under scenarios. Each hop is caller -> target.part. A unit is named by its path from the holding unit; a capability or need of the holding unit is named bare:

scenarios:
  open:
    summary: The host opens a session
    source: proto ยง5
    hops:
      - host -> session.open
      - session -> transport.send
      - transport/serial -> uart.write
    expect: one frame 1F 00 on the line
Hop Resolves through The part is looked up in
unit -> other-unit.part A need of the caller, followed up and down until the target provides for it, at whatever level the contract of that need
capability -> unit.part The capability of the holding unit, followed down to the target that implements it the contract of that capability
unit -> need.part A need of the caller, passed up until it reaches that need of the holding unit the contract of that need
capability -> need.part, in a unit without children Nothing: the capability's implementation uses the need the contract of that need

The contract of a connection is one document at every level, so the part is found wherever the hop is written. A name is looked up as a descendant first, then as a capability or need of the holding unit; a name that is both is an error that asks for a rename, as are two connections between the same two units whose contracts both have the part. A scenario belongs to the lowest unit that contains every unit it names; a hop to a need of the root belongs to the root. A story may cross levels, and its diagram is drawn end to end. Every wiring line should appear in a hop of some scenario: a connection that no story needs should not exist.

Contracts of a connection#

A connection carries information in some form: a wire format, a schema, a trait, a command line. The contract of the connection is the reference that defines that form, with examples, and both sides name it. It is not the kind: http says how the sides talk, the contract says what they say. And it is not a schema file such as openapi.yaml or a .proto, even where code is generated from one: such a file stays with the code that uses it. The checker reports a connection whose two sides name different documents, or none.

A manifest never points into a sibling, so the contract of a connection between two children lives in the references of the unit that wires them, or higher: docs/references/clock.md of the parent, named as ../docs/references/clock.md by both. A protocol that several parts implement, such as a radio profile served by a device and by its emulator, is such a document, not a unit: a unit holds code. A definition from outside the project, such as the API of a vendor, gets a reference of the root as well, which links the definition by its URL.

A capability that a parent passes through (public-api: gateway.public-api) appears in two manifests, each describing it at its own level, and names the same document at both: a reference of the parent, or of a unit above it. A parent that only passes a capability through adds nothing of its own to the summary. A format that travels inside another contract, such as a reading inside an upload, is not a contract of its own: it is a reference that the context files of its users cite.

A unit with code of its own#

A unit with children MAY have code of its own, for example a main program that calls its libraries. What that code uses from a child is a need of the unit, wired in the unit's own wiring to the child's capability, clock: platform.clock; the standard shows the example. The unit above does not wire such a need, and at the root it is not a need of the project. A child's need of the same name still needs its own line, and it cannot be passed up to it.

The other direction is not modelled. When a child uses code of the unit above it, that code gets a folder with a manifest and a capability, and the child's need is wired to it like any other connection. A connection always joins two units; there is no line that names a folder.

Optional needs#

A need behind a feature flag is optional: true. It MAY be wired anyway. What only tests use, such as a fake or a test helper, is no need: declare a need for what the unit's code requires when it runs.

Services and external systems#

For a set of services in one repository:

This page as plain Markdown, and its source on GitHub.