docs(config): components/configuration normalization (spec, shared-contract, current-state x3, GAPS, README)

This commit is contained in:
Joseph Doherty
2026-06-01 09:48:49 -04:00
parent b754873a44
commit 46c4bfae31
7 changed files with 1033 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
# 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 runtime draft/snapshot validation (`DraftValidator` + `DraftSnapshot`) — 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
```