using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa;
///
/// Phase 6.4 Stream D: materializes the OPC 40010 Machinery companion-spec Identification
/// sub-folder under an Equipment node. Reads the nine decision-#139 columns off the
/// row and emits one property per non-null field.
///
///
/// Pure-function shape — testable without a real OPC UA node manager. The caller
/// passes the builder scoped to the Equipment node; this class handles the Identification
/// sub-folder creation + per-field calls.
///
/// ACL binding: the sub-folder + its properties inherit the Equipment scope's
/// grants (no new scope level). Phase 6.2's trie treats them as part of the Equipment
/// ScopeId — a user with Equipment-level grant reads Identification; a user without the
/// grant gets BadUserAccessDenied on both the Equipment node + its Identification variables.
/// See docs/v2/acl-design.md §Identification cross-reference.
///
/// The nine fields per decision #139 are exposed exactly when they carry a non-null
/// value. A row with all nine null produces no Identification sub-folder at all — the
/// caller can use to skip the Folder call entirely
/// and avoid a pointless empty folder appearing in browse trees.
///
public static class IdentificationFolderBuilder
{
/// Browse + display name of the sub-folder — fixed per OPC 40010 convention.
public const string FolderName = "Identification";
///
/// Canonical decision #139 field set exposed in the Identification sub-folder. Order
/// matches the decision-log entry so any browse-order reader can cross-reference
/// without re-sorting.
///
public static IReadOnlyList FieldNames { get; } = new[]
{
"Manufacturer", "Model", "SerialNumber",
"HardwareRevision", "SoftwareRevision",
"YearOfConstruction", "AssetLocation",
"ManufacturerUri", "DeviceManualUri",
};
/// True when the equipment row has at least one non-null Identification field.
public static bool HasAnyFields(Equipment equipment)
{
ArgumentNullException.ThrowIfNull(equipment);
return equipment.Manufacturer is not null
|| equipment.Model is not null
|| equipment.SerialNumber is not null
|| equipment.HardwareRevision is not null
|| equipment.SoftwareRevision is not null
|| equipment.YearOfConstruction is not null
|| equipment.AssetLocation is not null
|| equipment.ManufacturerUri is not null
|| equipment.DeviceManualUri is not null;
}
///
/// Build the Identification sub-folder under . No-op
/// when every field is null. Returns the sub-folder builder (or null when no-op) so
/// callers can attach additional nodes underneath if needed.
///
public static IAddressSpaceBuilder? Build(IAddressSpaceBuilder equipmentBuilder, Equipment equipment)
{
ArgumentNullException.ThrowIfNull(equipmentBuilder);
ArgumentNullException.ThrowIfNull(equipment);
if (!HasAnyFields(equipment)) return null;
var folder = equipmentBuilder.Folder(FolderName, FolderName);
AddIfPresent(folder, "Manufacturer", DriverDataType.String, equipment.Manufacturer);
AddIfPresent(folder, "Model", DriverDataType.String, equipment.Model);
AddIfPresent(folder, "SerialNumber", DriverDataType.String, equipment.SerialNumber);
AddIfPresent(folder, "HardwareRevision", DriverDataType.String, equipment.HardwareRevision);
AddIfPresent(folder, "SoftwareRevision", DriverDataType.String, equipment.SoftwareRevision);
AddIfPresent(folder, "YearOfConstruction", DriverDataType.Int32,
equipment.YearOfConstruction is null ? null : (object)(int)equipment.YearOfConstruction.Value);
AddIfPresent(folder, "AssetLocation", DriverDataType.String, equipment.AssetLocation);
AddIfPresent(folder, "ManufacturerUri", DriverDataType.String, equipment.ManufacturerUri);
AddIfPresent(folder, "DeviceManualUri", DriverDataType.String, equipment.DeviceManualUri);
return folder;
}
private static void AddIfPresent(IAddressSpaceBuilder folder, string name, DriverDataType dataType, object? value)
{
if (value is null) return;
folder.AddProperty(name, dataType, value);
}
}