# `Lightning.Adaptors.Strategy`
[🔗](https://github.com/OpenFn/lightning/blob/main/lib/lightning/adaptors/strategy.ex#L1)

Behaviour every adaptor strategy implements.

A strategy is the sole boundary between the `Lightning.Adaptors.*`
subsystem and the outside world. It defines four callbacks:

  * `c:fetch_adaptor/1` returns a `t:adaptor_record/0` for one package
    name. Icon fields are not part of this record. The Scheduler stamps
    them on separately after joining the bulk icon pipeline.
  * `c:fetch_icon/2` returns the raw bytes and extension of one icon
    variant. The Store calls it when an icon is missing on disk.
  * `c:fetch_icons/1` bulk-fetches icons for every adaptor the strategy
    knows. The Scheduler runs it once per tick in parallel with its
    per-adaptor fan-out. See the callback docs for `:prior_etags`.
  * `c:list_adaptors/0` is the cheap change signal: `name` and
    `latest_version` for every package the strategy knows, which the
    Scheduler diffs against the `adaptors` table.

The active strategy module is resolved at runtime via
`Lightning.Adaptors.Config.strategy/0`. Implementations surface
transient failures (5xx, timeout, nxdomain) as `{:error, term()}` and
do not retry.

# `adaptor_record`

```elixir
@type adaptor_record() :: %{
  name: String.t(),
  description: String.t() | nil,
  homepage: String.t() | nil,
  repository: String.t() | nil,
  license: String.t() | nil,
  latest_version: String.t(),
  deprecated: boolean(),
  schema_data: String.t() | nil,
  schema_sha256: String.t() | nil,
  versions: [version_record()]
}
```

The record returned by `c:fetch_adaptor/1`. Icon fields are not on it.
The Scheduler persists them separately after joining `c:fetch_icons/1`.

`schema_data` is the credential schema as a JSON binary. `nil` means
the source sees no schema for this version; the Scheduler decides
whether that replaces a stored one.

# `icon_entry`

```elixir
@type icon_entry() :: %{
  :data =&gt; binary(),
  :ext =&gt; String.t(),
  :sha256 =&gt; binary(),
  optional(:etag) =&gt; String.t() | nil
}
```

Fresh-fetch icon entry inside the `c:fetch_icons/1` result map. `:etag`
is the upstream cache validator verbatim from the HTTP response, `nil`
when upstream sent none. Strategies with no transport-level validator,
such as `Lightning.Adaptors.Local`, omit the key entirely.

# `icon_shape_value`

```elixir
@type icon_shape_value() :: icon_entry() | :not_modified
```

Per-shape value inside the `c:fetch_icons/1` result map. Either a
fresh `t:icon_entry/0` (a 200) or `:not_modified` (a 304), which is
only ever returned when the caller supplied a prior etag via
`:prior_etags`.

# `icons_map`

```elixir
@type icons_map() :: %{
  required(String.t()) =&gt; %{
    optional(:square) =&gt; icon_shape_value(),
    optional(:rectangle) =&gt; icon_shape_value()
  }
}
```

Bulk icon map returned by `c:fetch_icons/1`. A shape that is absent
means upstream has no such icon. `:not_modified` means it is unchanged
since the prior etag. A map is a fresh fetch to apply.

# `version_record`

```elixir
@type version_record() :: %{
  version: String.t(),
  integrity: String.t() | nil,
  tarball_url: String.t() | nil,
  size_bytes: integer() | nil,
  dependencies: map(),
  peer_dependencies: map(),
  published_at: DateTime.t() | nil,
  deprecated: boolean()
}
```

Per-version metadata extracted from an upstream packument or local
`package.json`.

# `fetch_adaptor`

```elixir
@callback fetch_adaptor(name :: String.t()) :: {:ok, adaptor_record()} | {:error, term()}
```

Fetch the full structured record for a single adaptor package.

# `fetch_icon`

```elixir
@callback fetch_icon(name :: String.t(), :square | :rectangle) ::
  {:ok, %{data: binary(), ext: String.t()}} | {:error, term()}
```

Fetch the raw bytes for one icon variant (`:square` or `:rectangle`)
of an adaptor package, together with the file extension.

# `fetch_icons`

```elixir
@callback fetch_icons(opts :: keyword()) :: {:ok, icons_map()} | {:error, term()}
```

Bulk fetch every available icon for every adaptor known to the
strategy.

Returns `{:ok, icons_map}` (see `t:icons_map/0`). A top-level
`{:error, term()}` is only
returned when the whole pipeline cannot proceed, for example when the
`list_adaptors/0` call inside the bulk implementation fails.

## Options

  * `:prior_etags` - `%{name => %{optional(:square | :rectangle) => etag}}`,
    sent as `If-None-Match` per `(name, shape)`. Defaults to `%{}`.
    Strategies with no transport-level cache validator, such as
    `Lightning.Adaptors.Local`, ignore it and never return
    `:not_modified`.

# `list_adaptors`

```elixir
@callback list_adaptors() ::
  {:ok, [%{name: String.t(), latest_version: String.t()}]} | {:error, term()}
```

Cheap change signal: `name` and `latest_version` for every package the
strategy knows. The Scheduler diffs this against the `adaptors` table to
compute its work list.

`{:ok, []}` means the strategy looked and there is genuinely nothing
there, which settles the Store's first-load gate. A strategy that cannot
tell an empty source from an unreadable one must return `{:error, _}`:
`Lightning.Adaptors.Local` reports an empty checkout as `{:ok, []}`,
`Lightning.Adaptors.NPM` reports an empty org listing as
`{:error, :empty_listing}`.

# `digest_schema`

```elixir
@spec digest_schema(binary()) :: {:ok, {binary(), String.t()}} | {:error, term()}
```

Validate a schema body and pair it with its persisted digest.

Returns `{:ok, {body, sha256_hex}}` when `body` decodes as JSON, with
`sha256_hex` lowercase hex, matching the `adaptors.schema_sha256`
column format. `{:error, reason}` when it doesn't decode.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
