# Configuration validation (config binding + startup validation)

Normalized component for **startup configuration validation** across the three sister projects.
**Goal: path to shared code** — converge the apps onto one `IValidateOptions` failure-accumulation
base, a shared set of rule primitives, a single bind+validate+`ValidateOnStart` DI helper, and a
pre-host raw-config aggregator, extracted as the `ZB.MOM.WW.Configuration` library (single package),
while each app keeps its own options classes and domain rules.

- The one target: [`spec/SPEC.md`](spec/SPEC.md)
- The shared library (paper API): [`shared-contract/ZB.MOM.WW.Configuration.md`](shared-contract/ZB.MOM.WW.Configuration.md)
- Divergences + adoption backlog: [`GAPS.md`](GAPS.md)
- Current state, per project: [`current-state/`](current-state/)

## Why config validation is a normalization candidate

All three apps fail-fast on bad configuration at startup — and all three hand-roll the same
plumbing to do it:

- **OtOpcUa** has **no startup options validation at all** — `Ldap`/`OpcUa` are bound with bare
  `.Bind()` and trusted; a bad value fails opaquely on first use. (Its `DraftValidator` is runtime
  *config-content* validation, a different concern, out of scope.)
- **MxAccessGateway** has one large `GatewayOptionsValidator` (~360 LOC, nine sub-validators) with a
  private `List<string>` accumulator and `AddIfBlank`/`AddIfNotPositive`/`AddIfInvalidPath` helpers,
  wired through a bespoke `AddGatewayConfiguration` extension.
- **ScadaBridge** is the heaviest: **four** per-module `*OptionsValidator` (Cluster / Security /
  HealthMonitoring / AuditLog), each open-coding the same accumulation, **plus** a raw-config
  pre-Akka `StartupValidator`.

The common core — accumulate-all-failures `IValidateOptions`, reusable rule primitives,
`AddValidatedOptions`, and a `ConfigPreflight` that generalizes `StartupValidator` — is genuinely
shareable; the **options classes and domain rules stay per-project**. The unifying detail:
`ConfigPreflight.ThrowIfInvalid()` reproduces ScadaBridge's `StartupValidator` thrown message
**byte-for-byte**, so the heaviest migration is behaviour-preserving.

## Status by project

| Project | Options validators today | Pre-host preflight | Failure accumulation | Adoption status |
|---|---|---|---|---|
| **OtOpcUa** | ⛔ **none** (bare `.Bind()`; `DraftValidator` is out-of-scope runtime validation) | ⛔ none | n/a | Not started (additive, optional) |
| **MxAccessGateway** | 🟡 `GatewayOptionsValidator` (hand-rolled `IValidateOptions`) | ⛔ none | 🟡 manual `List<string>` | Not started (follow-on) |
| **ScadaBridge** | 🟡 four `*OptionsValidator` (hand-rolled) | ✅ `StartupValidator` (raw config, pre-Akka) | 🟡 manual `List<string>` ×4 | Not started (follow-on; heaviest) |

See each project's [`current-state/<project>/CURRENT-STATE.md`](current-state/) for the
code-verified detail and its adoption plan.

## Normalized vs. left per-project

**Normalized (the shared target):**

- `OptionsValidatorBase<TOptions>` — abstract `IValidateOptions<TOptions>`; override
  `protected void Validate(ValidationBuilder, TOptions)`; the base aggregates **all** failures and
  returns `Success` only when clean.
- `ValidationBuilder` rule primitives — `Required`, `Port`, `HostPort`, `PositiveTimeSpan`,
  `OneOf`, `MinCount`, plus `RequireThat`/`Add` for custom and cross-field rules; consistent
  `"<field> <reason>"` wording via the internal `Checks` seam.
- `AddValidatedOptions<TOptions, TValidator>(IConfiguration, sectionPath)` — bind + register
  validator + `ValidateOnStart` in one DI call; returns `OptionsBuilder<TOptions>`.
- `ConfigPreflight` — fluent pre-host raw-`IConfiguration` aggregator (`For`/`Require`/`RequireValue`/
  `RequirePort`/`When`/`ThrowIfInvalid`); generalizes `StartupValidator`, with a byte-compatible
  thrown message.
- The error-handling contract: accumulate ALL failures; two surfacing paths
  (`OptionsValidationException` at host start via `ValidateOnStart`, vs
  `ConfigPreflight.ThrowIfInvalid()`'s `InvalidOperationException`); `"<field> <reason>"` messages.

**Left per-project (not forced together):**

- Each app's options classes (`GatewayOptions`, `ClusterOptions`, `SecurityOptions`,
  `HealthMonitoringOptions`, `AuditLogOptions`, `NodeOptions`, …) and all of their domain rules —
  worker `.exe` paths, split-brain strategy, Akka heartbeat/threshold ordering, audit retention
  bounds, gRPC-port-vs-remoting-port topology, etc.
- OtOpcUa's draft/generation-content validation (DB-side `sp_ValidateDraft`; the C# `DraftValidator`
  / `DraftSnapshot` is dormant, no `src/` caller) — config-content validation, **out of scope** entirely.

## Package structure

`ZB.MOM.WW.Configuration` ships as a **single package, one DLL** — no third-party packages, no
ASP.NET Core framework reference:

| Package | Contents | Consumers |
|---|---|---|
| `ZB.MOM.WW.Configuration` | `OptionsValidatorBase<TOptions>`, `ValidationBuilder`, `ServiceCollectionExtensions.AddValidatedOptions`, `ConfigPreflight`, internal `Checks` | All three (ScadaBridge heaviest) |

Dependency closure: `Microsoft.Extensions.{Options, Options.ConfigurationExtensions,
Configuration.Abstractions, DependencyInjection.Abstractions}`.

## Component status

**Status: Draft. Library BUILT @ 0.1.0; NOT YET ADOPTED by the three apps. Adoption is the
backlog** (tracked in [`GAPS.md`](GAPS.md)). Unlike the observability pass, this release carries
**no in-pass sister-repo adoption** — it is library-only.

The shared library lives at
[`~/Desktop/scadaproj/ZB.MOM.WW.Configuration/`](../../ZB.MOM.WW.Configuration/) (.NET 10; single
package; 27 tests; `dotnet pack` → 1 nupkg @ 0.1.0). Build/test/pack from `ZB.MOM.WW.Configuration/`:

```bash
dotnet test ZB.MOM.WW.Configuration.slnx
dotnet pack ZB.MOM.WW.Configuration.slnx -c Release -o ./artifacts
```
