73 lines
4 KiB
Text
73 lines
4 KiB
Text
|
|
{
|
|||
|
|
id = "adr-001",
|
|||
|
|
title = "Non-exhaustive structs with named constructors as the external construction contract",
|
|||
|
|
status = 'accepted,
|
|||
|
|
date = "2026-05-12",
|
|||
|
|
|
|||
|
|
context = m%"
|
|||
|
|
`Instance` and `ProviderHealth` are the two core output types of the
|
|||
|
|
`ComputeProvider` trait. Both are marked `#[non_exhaustive]` — a forward-
|
|||
|
|
compatibility guarantee that allows adding fields without a SemVer bump.
|
|||
|
|
The attribute blocks struct literal syntax in external crates (E0639), which
|
|||
|
|
became concrete when `lian-build`'s `LianBuildScriptProvider` attempted to
|
|||
|
|
construct `Instance { handle, provider, ... }` as the first γ-posture
|
|||
|
|
external consumer.
|
|||
|
|
|
|||
|
|
Three remediation paths exist:
|
|||
|
|
1. Remove `#[non_exhaustive]`: construction works, but every new field is a
|
|||
|
|
breaking change at all destructuring and pattern-match sites.
|
|||
|
|
2. Named constructors: keep the attribute, add `::new()` / `::healthy()` /
|
|||
|
|
`::unhealthy()` as the stable external API.
|
|||
|
|
3. Builder pattern: type-state or `Default` + field-by-field assignment.
|
|||
|
|
|
|||
|
|
The crate is a library for provider implementors, not application code. The
|
|||
|
|
field set of `Instance` (ip, ssh_host, ssh_port, expires_at, metadata) is
|
|||
|
|
expected to grow as new provider kinds surface cloud-specific properties.
|
|||
|
|
Preserving `#[non_exhaustive]` is the correct API stability posture.
|
|||
|
|
"%,
|
|||
|
|
|
|||
|
|
decision = m%"
|
|||
|
|
Keep `#[non_exhaustive]` on all public output structs. Add named constructors
|
|||
|
|
as the sole supported construction path for external crates:
|
|||
|
|
|
|||
|
|
- `Instance::new(handle, provider)` — zero-optional-field form; callers
|
|||
|
|
assign optional fields (`ssh_host`, `ip`, `expires_at`, `metadata`)
|
|||
|
|
after construction.
|
|||
|
|
- `ProviderHealth::healthy(latency_ms)` — ok=true, latency set, detail=None.
|
|||
|
|
- `ProviderHealth::unhealthy(latency_ms, reason)` — ok=false, detail set.
|
|||
|
|
|
|||
|
|
Builder and Default are explicitly excluded: a builder adds three abstractions
|
|||
|
|
for two required fields; Default would require callers to know which fields are
|
|||
|
|
meaningful versus zero-valued.
|
|||
|
|
|
|||
|
|
The `InstanceHandle` and `ProviderKind` enums require no change — they are
|
|||
|
|
exhaustive and constructible via standard syntax.
|
|||
|
|
"%,
|
|||
|
|
|
|||
|
|
alternatives = [
|
|||
|
|
m%"Remove `#[non_exhaustive]`. Rejected: adding fields to `Instance` (e.g. `ipv6_host`) would be a breaking change at all match/destructuring sites in consumer crates."%,
|
|||
|
|
m%"Builder pattern. Rejected: over-engineered for structs with 2–3 meaningful initialization parameters and all-optional fields beyond the required handle/provider pair."%,
|
|||
|
|
m%"`Default` impl + field assignment. Rejected: requires callers to know which fields are semantically required vs truly optional; a zero-valued `provider` string is silently wrong."%,
|
|||
|
|
],
|
|||
|
|
|
|||
|
|
consequences = m%"
|
|||
|
|
Named constructors become a committed API surface. Adding a new *required*
|
|||
|
|
semantic field to `Instance` now requires both a struct field and a constructor
|
|||
|
|
parameter — the same cost as removing `#[non_exhaustive]` would impose, but
|
|||
|
|
scoped to the constructor rather than all consumers. Adding new *optional*
|
|||
|
|
fields is still backward-compatible: the struct gains the field, the constructor
|
|||
|
|
initializes it to `None`/default, existing callers use the field by name after
|
|||
|
|
construction if they want it.
|
|||
|
|
|
|||
|
|
The `lian-build` `LianBuildScriptProvider` is the canonical usage example:
|
|||
|
|
`Instance::new(handle, "lian-build-script")` followed by direct field
|
|||
|
|
assignment for `ssh_host`, `ssh_port`, `expires_at`.
|
|||
|
|
"%,
|
|||
|
|
|
|||
|
|
constraints = [
|
|||
|
|
{ kind = 'hard, statement = "`Instance`, `ProviderHealth`, and any future public output struct MUST remain `#[non_exhaustive]`." },
|
|||
|
|
{ kind = 'hard, statement = "Public constructors MUST initialize all required semantic fields; optional fields MUST default to `None` or the zero value." },
|
|||
|
|
{ kind = 'soft, statement = "Convenience constructors (`healthy`, `unhealthy`) SHOULD cover the two most common `ProviderHealth` shapes to keep provider implementations free of boilerplate." },
|
|||
|
|
],
|
|||
|
|
}
|