feat(transport): name-map types + preview/selection/summary extensions (M8 A1)
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How a source-environment site or connection referenced by an incoming bundle is
|
||||||
|
/// resolved against the destination environment during import.
|
||||||
|
/// </summary>
|
||||||
|
public enum MappingAction
|
||||||
|
{
|
||||||
|
/// <summary>Bind the source name to an existing destination site/connection
|
||||||
|
/// (the corresponding <c>Target*</c> identifier is required).</summary>
|
||||||
|
MapToExisting,
|
||||||
|
|
||||||
|
/// <summary>Create a new destination site/connection from the bundle's payload
|
||||||
|
/// (the corresponding <c>Target*</c> identifier is left null).</summary>
|
||||||
|
CreateNew,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves one site referenced by an incoming bundle (M8 site/instance-scoped
|
||||||
|
/// transport). <paramref name="TargetSiteIdentifier"/> is required when
|
||||||
|
/// <paramref name="Action"/> is <see cref="MappingAction.MapToExisting"/> and null
|
||||||
|
/// when it is <see cref="MappingAction.CreateNew"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SiteMapping(
|
||||||
|
string SourceSiteIdentifier,
|
||||||
|
MappingAction Action,
|
||||||
|
string? TargetSiteIdentifier);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves one data connection (scoped to a source site) referenced by an incoming
|
||||||
|
/// bundle. <paramref name="TargetConnectionName"/> is required when
|
||||||
|
/// <paramref name="Action"/> is <see cref="MappingAction.MapToExisting"/> and null
|
||||||
|
/// when it is <see cref="MappingAction.CreateNew"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ConnectionMapping(
|
||||||
|
string SourceSiteIdentifier,
|
||||||
|
string SourceConnectionName,
|
||||||
|
MappingAction Action,
|
||||||
|
string? TargetConnectionName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The operator-supplied resolution of every source-environment site and connection
|
||||||
|
/// name an incoming bundle references, applied during import. Crosses the Central UI ⇆
|
||||||
|
/// ManagementActor boundary via System.Text.Json.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record BundleNameMap(
|
||||||
|
IReadOnlyList<SiteMapping> Sites,
|
||||||
|
IReadOnlyList<ConnectionMapping> Connections)
|
||||||
|
{
|
||||||
|
/// <summary>An empty map (no site or connection mappings).</summary>
|
||||||
|
public static BundleNameMap Empty { get; } =
|
||||||
|
new(Array.Empty<SiteMapping>(), Array.Empty<ConnectionMapping>());
|
||||||
|
}
|
||||||
@@ -10,4 +10,9 @@ public sealed record BundleSummary(
|
|||||||
int DbConnections,
|
int DbConnections,
|
||||||
int NotificationLists,
|
int NotificationLists,
|
||||||
int SmtpConfigs,
|
int SmtpConfigs,
|
||||||
int ApiMethods);
|
int ApiMethods,
|
||||||
|
// Additive (M8 A1): site/instance-scoped artifacts. Defaulted to 0 so manifests
|
||||||
|
// written by older exporters (which omit these JSON properties) deserialize fine.
|
||||||
|
int Sites = 0,
|
||||||
|
int DataConnections = 0,
|
||||||
|
int Instances = 0);
|
||||||
|
|||||||
@@ -13,4 +13,15 @@ public sealed record ExportSelection(
|
|||||||
IReadOnlyList<int> NotificationListIds,
|
IReadOnlyList<int> NotificationListIds,
|
||||||
IReadOnlyList<int> SmtpConfigurationIds,
|
IReadOnlyList<int> SmtpConfigurationIds,
|
||||||
IReadOnlyList<int> ApiMethodIds,
|
IReadOnlyList<int> ApiMethodIds,
|
||||||
bool IncludeDependencies);
|
bool IncludeDependencies,
|
||||||
|
// Additive (M8 A1): site/instance-scoped export. Defaulted to empty so every
|
||||||
|
// existing positional caller keeps compiling; older callers select no sites/instances.
|
||||||
|
IReadOnlyList<int>? SiteIds = null,
|
||||||
|
IReadOnlyList<int>? InstanceIds = null)
|
||||||
|
{
|
||||||
|
/// <summary>Sites selected for site/instance-scoped export (M8). Never null.</summary>
|
||||||
|
public IReadOnlyList<int> SiteIds { get; init; } = SiteIds ?? Array.Empty<int>();
|
||||||
|
|
||||||
|
/// <summary>Instances selected for site/instance-scoped export (M8). Never null.</summary>
|
||||||
|
public IReadOnlyList<int> InstanceIds { get; init; } = InstanceIds ?? Array.Empty<int>();
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,40 @@ public sealed record ImportPreviewItem(
|
|||||||
string? FieldDiffJson,
|
string? FieldDiffJson,
|
||||||
string? BlockerReason);
|
string? BlockerReason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A site reference in an incoming bundle that the operator must resolve before import
|
||||||
|
/// (M8 A1). <paramref name="AutoMatchTargetIdentifier"/> is the destination site the
|
||||||
|
/// importer auto-matched by identifier, or null when no match was found.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record RequiredSiteMapping(
|
||||||
|
string SourceSiteIdentifier,
|
||||||
|
string SourceSiteName,
|
||||||
|
string? AutoMatchTargetIdentifier);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A connection reference (scoped to a source site) in an incoming bundle that the
|
||||||
|
/// operator must resolve before import (M8 A1). <paramref name="AutoMatchTargetName"/>
|
||||||
|
/// is the destination connection the importer auto-matched by name, or null when no
|
||||||
|
/// match was found.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record RequiredConnectionMapping(
|
||||||
|
string SourceSiteIdentifier,
|
||||||
|
string SourceConnectionName,
|
||||||
|
string? AutoMatchTargetName);
|
||||||
|
|
||||||
public sealed record ImportPreview(
|
public sealed record ImportPreview(
|
||||||
Guid SessionId,
|
Guid SessionId,
|
||||||
IReadOnlyList<ImportPreviewItem> Items);
|
IReadOnlyList<ImportPreviewItem> Items,
|
||||||
|
// Additive (M8 A1): site/connection references the operator must resolve before
|
||||||
|
// applying. Defaulted to empty so every existing positional caller keeps compiling.
|
||||||
|
IReadOnlyList<RequiredSiteMapping>? RequiredSiteMappings = null,
|
||||||
|
IReadOnlyList<RequiredConnectionMapping>? RequiredConnectionMappings = null)
|
||||||
|
{
|
||||||
|
/// <summary>Site references the operator must resolve before import (M8). Never null.</summary>
|
||||||
|
public IReadOnlyList<RequiredSiteMapping> RequiredSiteMappings { get; init; } =
|
||||||
|
RequiredSiteMappings ?? Array.Empty<RequiredSiteMapping>();
|
||||||
|
|
||||||
|
/// <summary>Connection references the operator must resolve before import (M8). Never null.</summary>
|
||||||
|
public IReadOnlyList<RequiredConnectionMapping> RequiredConnectionMappings { get; init; } =
|
||||||
|
RequiredConnectionMappings ?? Array.Empty<RequiredConnectionMapping>();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Transport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// M8 A1: focused shape / value-equality / round-trip tests for the Transport (#24)
|
||||||
|
/// name-mapping DTOs — <see cref="SiteMapping"/>, <see cref="ConnectionMapping"/>,
|
||||||
|
/// and <see cref="BundleNameMap"/>. These records cross the Central UI ⇆ ManagementActor
|
||||||
|
/// boundary (and may be persisted in an import session) via System.Text.Json, so a
|
||||||
|
/// positional/tuple slip would silently mis-map sites/connections during import.
|
||||||
|
/// Uses plain <see cref="JsonSerializer"/> (no dependency on the Transport project's
|
||||||
|
/// BundleJsonOptions) per the A1 task contract.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BundleNameMapTests
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Empty_HasNoSitesOrConnections()
|
||||||
|
{
|
||||||
|
var map = BundleNameMap.Empty;
|
||||||
|
|
||||||
|
Assert.Empty(map.Sites);
|
||||||
|
Assert.Empty(map.Connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Empty_IsStableInstance()
|
||||||
|
{
|
||||||
|
// The Empty singleton must be reusable without aliasing surprises.
|
||||||
|
Assert.Same(BundleNameMap.Empty, BundleNameMap.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SiteMapping_ValueEquality_HoldsForIdenticalFields()
|
||||||
|
{
|
||||||
|
var a = new SiteMapping("site-east", MappingAction.MapToExisting, "site-1");
|
||||||
|
var b = new SiteMapping("site-east", MappingAction.MapToExisting, "site-1");
|
||||||
|
|
||||||
|
Assert.Equal(a, b);
|
||||||
|
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SiteMapping_ValueEquality_DiffersWhenActionDiffers()
|
||||||
|
{
|
||||||
|
var mapTo = new SiteMapping("site-east", MappingAction.MapToExisting, "site-1");
|
||||||
|
var createNew = new SiteMapping("site-east", MappingAction.CreateNew, null);
|
||||||
|
|
||||||
|
Assert.NotEqual(mapTo, createNew);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConnectionMapping_ValueEquality_HoldsForIdenticalFields()
|
||||||
|
{
|
||||||
|
var a = new ConnectionMapping("site-east", "OPC-Main", MappingAction.MapToExisting, "OPC-Primary");
|
||||||
|
var b = new ConnectionMapping("site-east", "OPC-Main", MappingAction.MapToExisting, "OPC-Primary");
|
||||||
|
|
||||||
|
Assert.Equal(a, b);
|
||||||
|
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapToExisting_CarriesNonNullTarget()
|
||||||
|
{
|
||||||
|
var site = new SiteMapping("site-east", MappingAction.MapToExisting, "site-1");
|
||||||
|
var conn = new ConnectionMapping("site-east", "OPC-Main", MappingAction.MapToExisting, "OPC-Primary");
|
||||||
|
|
||||||
|
Assert.Equal(MappingAction.MapToExisting, site.Action);
|
||||||
|
Assert.NotNull(site.TargetSiteIdentifier);
|
||||||
|
Assert.Equal("site-1", site.TargetSiteIdentifier);
|
||||||
|
|
||||||
|
Assert.Equal(MappingAction.MapToExisting, conn.Action);
|
||||||
|
Assert.NotNull(conn.TargetConnectionName);
|
||||||
|
Assert.Equal("OPC-Primary", conn.TargetConnectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateNew_LeavesTargetNull()
|
||||||
|
{
|
||||||
|
var site = new SiteMapping("site-west", MappingAction.CreateNew, null);
|
||||||
|
var conn = new ConnectionMapping("site-west", "OPC-Aux", MappingAction.CreateNew, null);
|
||||||
|
|
||||||
|
Assert.Null(site.TargetSiteIdentifier);
|
||||||
|
Assert.Null(conn.TargetConnectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BundleNameMap_JsonRoundTrip_PreservesSitesAndConnections()
|
||||||
|
{
|
||||||
|
var map = new BundleNameMap(
|
||||||
|
Sites: new[]
|
||||||
|
{
|
||||||
|
new SiteMapping("site-east", MappingAction.MapToExisting, "site-1"),
|
||||||
|
new SiteMapping("site-west", MappingAction.CreateNew, null),
|
||||||
|
},
|
||||||
|
Connections: new[]
|
||||||
|
{
|
||||||
|
new ConnectionMapping("site-east", "OPC-Main", MappingAction.MapToExisting, "OPC-Primary"),
|
||||||
|
new ConnectionMapping("site-west", "OPC-Aux", MappingAction.CreateNew, null),
|
||||||
|
});
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(map, JsonOpts);
|
||||||
|
var rt = JsonSerializer.Deserialize<BundleNameMap>(json, JsonOpts);
|
||||||
|
|
||||||
|
Assert.NotNull(rt);
|
||||||
|
// BundleNameMap's record Equals is reference-based over its IReadOnlyList members,
|
||||||
|
// and JSON deserialization yields List<T> rather than the original T[] — so the
|
||||||
|
// collections never reference-equal. The records *within* the lists have proper
|
||||||
|
// value-equality, so compare element-wise (SequenceEqual uses SiteMapping/
|
||||||
|
// ConnectionMapping value-equality).
|
||||||
|
Assert.True(map.Sites.SequenceEqual(rt!.Sites));
|
||||||
|
Assert.True(map.Connections.SequenceEqual(rt.Connections));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BundleNameMap_Empty_JsonRoundTrip_PreservesEmptiness()
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(BundleNameMap.Empty, JsonOpts);
|
||||||
|
var rt = JsonSerializer.Deserialize<BundleNameMap>(json, JsonOpts);
|
||||||
|
|
||||||
|
Assert.NotNull(rt);
|
||||||
|
Assert.Empty(rt!.Sites);
|
||||||
|
Assert.Empty(rt.Connections);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user