ClientUtils.Harness.Onboarding.Port (client_utils v0.1.30)

View Source

Calls an io: adapter — a module, or a module paired with the thing it acts on.

Why the names are what they are

These four operations are read_file/2, write_file/3, file_exists?/2 and cmd/4 because that is CodeMySpec.Environments' contract, verbatim. A host that already has a filesystem abstraction passes {ThatModule, its_handle} and writes no adapter at all — CodeMySpec passes {CodeMySpec.Environments, env}.

The port used to name them read/2, write/3, exists?/2, cmd/3 and take a bare root, which had two consequences. A host with state could not use it: CodeMySpec's specs drive a :memory environment holding an Agent pid in ref, and no adapter given only a path can find that agent — so the single case this port was justified by ("CodeMySpec passes an adapter over its own Environments", written into c570d32 as though it were already true) was the one it could not serve, and CodeMySpec kept a second implementation of onboard/2 and check/2 instead. And a host without state still had to write a translation module whose whole content was renaming read to read_file.

Naming the port after an interface a caller already implements costs this library nothing — its own FileIO is the only other implementation and it is ours to shape — and costs the caller nothing at all, which is the point.

The shapes

  • {module, state}module.read_file(state, path). The state is opaque here; it is whatever the host needs and is passed straight back.
  • modulemodule.read_file(root, path). The root is the state, which is all FileIO has ever needed.

A generated application passes neither and gets FileIO.

Summary

Types

t()

An adapter: a module, or a module with the handle it acts on.

Types

t()

@type t() :: module() | {module(), term()}

An adapter: a module, or a module with the handle it acts on.

Functions

cmd(io, root, command, args)

@spec cmd(t(), String.t(), String.t(), [String.t()]) ::
  {String.t(), non_neg_integer()}

exists?(io, root, path)

@spec exists?(t(), String.t(), String.t()) :: boolean()

module(module)

@spec module(t()) :: module()

The module an adapter dispatches to.

Exposed so a host can assert the adapter it passed is the one being called — the check whose absence let this port go unused for a week while its own documentation said otherwise.

read(io, root, path)

@spec read(t(), String.t(), String.t()) :: {:ok, String.t()} | {:error, term()}

write(io, root, path, contents)

@spec write(t(), String.t(), String.t(), String.t()) :: :ok | {:error, term()}