chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)

Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions
@@ -0,0 +1,68 @@
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Parsed <c>ab://gateway[:port]/cip-path</c> host-address string used by the AbCip driver
/// as the <c>hostName</c> key across <see cref="Core.Abstractions.IHostConnectivityProbe"/>,
/// <see cref="Core.Abstractions.IPerCallHostResolver"/>, and the Polly bulkhead key
/// <c>(DriverInstanceId, hostName)</c> per v2 plan decision #144.
/// </summary>
/// <remarks>
/// <para>Format matches what libplctag's <c>gateway=...</c> + <c>path=...</c> attributes
/// consume, so no translation is needed at the wire layer — the parsed <see cref="CipPath"/>
/// is handed to the native library verbatim.</para>
/// <list type="bullet">
/// <item><c>ab://10.0.0.5/1,0</c> — single-chassis ControlLogix, CPU in slot 0.</item>
/// <item><c>ab://10.0.0.5/1,4</c> — CPU in slot 4.</item>
/// <item><c>ab://10.0.0.5/1,2,2,192.168.50.20,1,0</c> — bridged ControlLogix.</item>
/// <item><c>ab://10.0.0.5/</c> (empty path) — Micro800 / MicroLogix without backplane routing.</item>
/// <item><c>ab://10.0.0.5:44818/1,0</c> — explicit EIP port (default 44818).</item>
/// </list>
/// <para>Opaque to the rest of the stack: Admin UI, telemetry, and logs display the full
/// string so an incident ticket can be matched to the exact gateway + CIP route.</para>
/// </remarks>
public sealed record AbCipHostAddress(string Gateway, int Port, string CipPath)
{
/// <summary>Default EtherNet/IP TCP port — spec-reserved.</summary>
public const int DefaultEipPort = 44818;
/// <summary>Recompose the canonical <c>ab://...</c> form.</summary>
public override string ToString() => Port == DefaultEipPort
? $"ab://{Gateway}/{CipPath}"
: $"ab://{Gateway}:{Port}/{CipPath}";
/// <summary>
/// Parse <paramref name="value"/>. Returns <c>null</c> on any malformed input — callers
/// should treat a null return as a config-validation failure rather than catching.
/// </summary>
public static AbCipHostAddress? TryParse(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
const string prefix = "ab://";
if (!value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return null;
var remainder = value[prefix.Length..];
var slashIdx = remainder.IndexOf('/');
if (slashIdx < 0) return null;
var authority = remainder[..slashIdx];
var cipPath = remainder[(slashIdx + 1)..];
if (string.IsNullOrEmpty(authority)) return null;
var port = DefaultEipPort;
var colonIdx = authority.LastIndexOf(':');
string gateway;
if (colonIdx >= 0)
{
gateway = authority[..colonIdx];
if (!int.TryParse(authority[(colonIdx + 1)..], out port) || port <= 0 || port > 65535)
return null;
}
else
{
gateway = authority;
}
if (string.IsNullOrEmpty(gateway)) return null;
return new AbCipHostAddress(gateway, port, cipPath);
}
}