Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions csharp/samples/AdvancedLocalIds/AdvancedLocalIds.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

<ItemGroup>
<ProjectReference Include="../../src/Vista.SDK/Vista.SDK.csproj" />
<ProjectReference Include="../../src/Vista.SDK.Mqtt/Vista.SDK.Mqtt.csproj" />
</ItemGroup>

</Project>
114 changes: 114 additions & 0 deletions csharp/samples/AdvancedLocalIds/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Advanced Local ID Operations - Vista SDK C#
- Local ID validation and inspection
*/
using Vista.SDK;
using Vista.SDK.Mqtt;

Console.WriteLine("=== Advanced Local ID Operations ===\n");

Expand Down Expand Up @@ -179,4 +180,117 @@ Advanced Local ID Operations - Vista SDK C#
Console.WriteLine($" Verbose mode: {verboseLocalId}");
Console.WriteLine($" Regular mode: {regularLocalId}");

// 7. MQTT LocalId: formatting and components
Console.WriteLine("\n7. MQTT LocalId: MQTT-compatible formatting...");

string[] mqttExamples =
[
"/dnv-v2/vis-3-4a/411.1/C101.31-2/meta/qty-temperature",
"/dnv-v2/vis-3-4a/411.1/C101.31-2/meta/qty-temperature/cnt-exhaust.gas/pos-inlet",
"/dnv-v2/vis-3-4a/621.21/S90/sec/411.1/C101/meta/qty-mass/cnt-fuel.oil/pos-inlet",
];

foreach (var localIdStr in mqttExamples)
{
if (LocalId.TryParse(localIdStr, out _, out var parsedLocalId))
{
var mqttLocalId = parsedLocalId.Builder.BuildMqtt();
Console.WriteLine($" Standard : {parsedLocalId}");
Console.WriteLine($" MQTT : {mqttLocalId}");
Console.WriteLine();
}
}

// All 8 slots filled
var fullPrimary = gmod.ParsePath("411.1/C101.31-2");
var fullSecondary = gmod.ParsePath("411.1/C101.31-5");
var fullBuilder = LocalIdBuilder
.Create(version)
.WithPrimaryItem(fullPrimary)
.WithSecondaryItem(fullSecondary)
.WithMetadataTag(codebooks.CreateTag(CodebookName.Quantity, "temperature"))
.WithMetadataTag(codebooks.CreateTag(CodebookName.Content, "exhaust.gas"))
.WithMetadataTag(codebooks.CreateTag(CodebookName.Calculation, "average"))
.WithMetadataTag(codebooks.CreateTag(CodebookName.State, "high"))
.WithMetadataTag(codebooks.CreateTag(CodebookName.Command, "start"))
.WithMetadataTag(codebooks.CreateTag(CodebookName.Type, "instantaneous"))
.WithMetadataTag(codebooks.CreateTag(CodebookName.Position, "inlet"))
.WithMetadataTag(codebooks.CreateTag(CodebookName.Detail, "my_sensor_42"));

var fullStandard = fullBuilder.Build();
var fullMqtt = fullBuilder.BuildMqtt();

Console.WriteLine(" All 8 slots + secondary + free-form detail:");
Console.WriteLine($" Standard : {fullStandard}");
Console.WriteLine($" MQTT : {fullMqtt}");
Console.WriteLine($" Slots : qty/cnt/calc/state/cmd/type/pos/detail");
Console.WriteLine($" Detail : '{fullStandard.Detail?.Value}' (IsCustom: {fullStandard.Detail?.IsCustom})");
Console.WriteLine();

// Reading components
var compPrimary = gmod.ParsePath("411.1/C101.31-2");
var compSecondary = gmod.ParsePath("411.1/C101.31-5");
var compBuilder = LocalIdBuilder
.Create(version)
.WithPrimaryItem(compPrimary)
.WithSecondaryItem(compSecondary)
.WithMetadataTag(codebooks.CreateTag(CodebookName.Quantity, "temperature"))
.WithMetadataTag(codebooks.CreateTag(CodebookName.Content, "exhaust.gas"));

var mqttComp = compBuilder.BuildMqtt();

Console.WriteLine(" Reading components:");
Console.WriteLine($" VisVersion : {mqttComp.VisVersion.ToVersionString()}");
Console.WriteLine($" PrimaryItem : {mqttComp.PrimaryItem}");
Console.WriteLine($" SecondaryItem : {mqttComp.SecondaryItem}");
Console.WriteLine($" Quantity : {mqttComp.Quantity}");
Console.WriteLine($" Content : {mqttComp.Content}");
Console.WriteLine($" Calculation : {mqttComp.Calculation?.ToString() ?? "(none)"}");
Console.WriteLine();

// 8. MQTT LocalId: builder-level state and equality
Console.WriteLine("8. MQTT LocalId: builder-level state and equality...");

// Builder-level state not reflected in MQTT format
var verbosePrimary = gmod.ParsePath("411.1/C101.63/S206");
var verboseBuilder = LocalIdBuilder
.Create(version)
.WithVerboseMode(true)
.WithPrimaryItem(verbosePrimary)
.WithMetadataTag(codebooks.CreateTag(CodebookName.Quantity, "temperature"));

var mqttVerbose = verboseBuilder.BuildMqtt();

Console.WriteLine(" Builder-level state (accessible via .Builder):");
Console.WriteLine(" VerboseMode/HasCustomTag/MetadataTags are not duplicated on MqttLocalId.");
Console.WriteLine(" VerboseMode has no effect on the MQTT format (unlike LocalId.ToString()).");
Console.WriteLine($" Builder.VerboseMode : {mqttVerbose.Builder.VerboseMode}");
Console.WriteLine($" Builder.HasCustomTag : {mqttVerbose.Builder.HasCustomTag}");
Console.WriteLine($" Builder.MetadataTags : {mqttVerbose.Builder.MetadataTags.Count} tag(s)");
Console.WriteLine($" MQTT (no '~' despite VerboseMode=true): {mqttVerbose}");
Console.WriteLine();

// Equality
var eqPrimary = gmod.ParsePath("411.1/C101.31-2");
var eqBuilder = LocalIdBuilder
.Create(version)
.WithPrimaryItem(eqPrimary)
.WithMetadataTag(codebooks.CreateTag(CodebookName.Quantity, "temperature"));

var a = eqBuilder.BuildMqtt();
var b = eqBuilder.BuildMqtt();
var c = eqBuilder.WithMetadataTag(codebooks.CreateTag(CodebookName.Content, "exhaust.gas")).BuildMqtt();

Console.WriteLine(" Equality:");
Console.WriteLine($" a == b (same builder) : {a == b}");
Console.WriteLine($" a == c (extra tag) : {a == c}");
Console.WriteLine();

Console.WriteLine(" MQTT format differences vs standard:");
Console.WriteLine(" - No leading '/'");
Console.WriteLine(" - Underscores instead of slashes in paths");
Console.WriteLine(" - No 'meta/' section");
Console.WriteLine(" - '_' placeholder for absent metadata slots");
Console.WriteLine(" - 8 fixed slots: qty/cnt/calc/state/cmd/type/pos/detail");

Console.WriteLine("\n=== Advanced operations completed! ===");
63 changes: 60 additions & 3 deletions csharp/src/Vista.SDK.Mqtt/MqttLocalId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,73 @@ public static class LocalIdBuilderExtensions
public static MqttLocalId BuildMqtt(this LocalIdBuilder builder) => new MqttLocalId(builder);
}

public class MqttLocalId : LocalId
public class MqttLocalId : IEquatable<MqttLocalId>
{
private static readonly char _internal_separator = '_';

private readonly LocalIdBuilder _builder;

public MqttLocalId(LocalIdBuilder builder)
: base(builder) { }
{
if (builder.IsEmpty)
throw new ArgumentException("LocalId cannot be constructed from empty LocalIdBuilder");
if (!builder.IsValid)
throw new ArgumentException("LocalId cannot be constructed from invalid LocalIdBuilder");
_builder = builder;
}

public LocalIdBuilder Builder => _builder;

public VisVersion VisVersion => _builder.VisVersion!.Value;

public GmodPath PrimaryItem => _builder.PrimaryItem!;

public GmodPath? SecondaryItem => _builder.SecondaryItem;

public MetadataTag? Quantity => _builder.Quantity;

public MetadataTag? Content => _builder.Content;

public MetadataTag? Calculation => _builder.Calculation;

public MetadataTag? State => _builder.State;

public MetadataTag? Command => _builder.Command;

public MetadataTag? Type => _builder.Type;

public MetadataTag? Position => _builder.Position;

public MetadataTag? Detail => _builder.Detail;

public sealed override bool Equals(object? obj) => Equals(obj as MqttLocalId);

public bool Equals(MqttLocalId? other)
{
if (other is null)
return false;

return _builder.Equals(other._builder);
}

public static bool operator !=(MqttLocalId? left, MqttLocalId? right) => !(left == right);

public static bool operator ==(MqttLocalId? left, MqttLocalId? right)
{
if (!ReferenceEquals(left, right))
{
if (left is not null)
return left.Equals(right);
return false;
}
return true;
}

public sealed override int GetHashCode() => _builder.GetHashCode();

public override string ToString()
{
string namingRule = $"{NamingRule}/";
string namingRule = $"{LocalId.NamingRule}/";
using var lease = StringBuilderPool.Get();

var builder = lease.Builder;
Expand Down
107 changes: 107 additions & 0 deletions csharp/test/Vista.SDK.Tests/LocalIdTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,113 @@ public sealed record class Input(
},
};

[Fact]
public void Test_MqttLocalId_Is_Not_A_LocalId_The_Former_LSP_Violation_Is_Now_Impossible_By_Construction()
{
// MqttLocalId must not inherit from LocalId
typeof(LocalId).IsAssignableFrom(typeof(MqttLocalId)).Should().BeFalse();
}

[Fact]
public void Test_MqttLocalId_Getters()
{
var (_, vis) = VISTests.GetVis();
var visVersion = VisVersion.v3_4a;
var gmod = vis.GetGmod(visVersion);
var codebooks = vis.GetCodebooks(visVersion);

var primaryItem = gmod.ParsePath("411.1/C101.31-2");
var secondaryItem = gmod.ParsePath("411.1/C101.31-5");
var qtyTag = codebooks.TryCreateTag(CodebookName.Quantity, "temperature");
var cntTag = codebooks.TryCreateTag(CodebookName.Content, "exhaust.gas");

var builder = LocalIdBuilder
.Create(visVersion)
.TryWithPrimaryItem(primaryItem)
.TryWithSecondaryItem(secondaryItem)
.TryWithMetadataTag(qtyTag)
.TryWithMetadataTag(cntTag);

var mqttLocalId = builder.BuildMqtt();

mqttLocalId.VisVersion.Should().Be(visVersion);
mqttLocalId.PrimaryItem.Should().Be(primaryItem);
mqttLocalId.SecondaryItem.Should().Be(secondaryItem);
mqttLocalId.Quantity.Should().Be(qtyTag);
mqttLocalId.Content.Should().Be(cntTag);
mqttLocalId.Calculation.Should().BeNull();
mqttLocalId.State.Should().BeNull();
mqttLocalId.Command.Should().BeNull();
mqttLocalId.Type.Should().BeNull();
mqttLocalId.Position.Should().BeNull();
mqttLocalId.Detail.Should().BeNull();
}

[Fact]
public void Test_MqttLocalId_Builder_Exposes_Members_Not_Reflected_In_The_Mqtt_Format()
{
// VerboseMode/HasCustomTag/MetadataTags have no effect on MqttLocalId.ToString() (the MQTT
// format never reads them), so MqttLocalId does not duplicate them as its own properties -
// they remain reachable via .Builder, same as any other LocalIdBuilder-derived information.
var (_, vis) = VISTests.GetVis();
var visVersion = VisVersion.v3_4a;
var gmod = vis.GetGmod(visVersion);
var codebooks = vis.GetCodebooks(visVersion);

var primaryItem = gmod.ParsePath("411.1/C101.63/S206");
var qtyTag = codebooks.TryCreateTag(CodebookName.Quantity, "temperature");

var builder = LocalIdBuilder
.Create(visVersion)
.WithVerboseMode(true)
.TryWithPrimaryItem(primaryItem)
.TryWithMetadataTag(qtyTag);

var mqttLocalId = builder.BuildMqtt();

mqttLocalId.Builder.VerboseMode.Should().BeTrue();
mqttLocalId.Builder.HasCustomTag.Should().BeFalse();
mqttLocalId.Builder.MetadataTags.Should().ContainSingle();

// Confirms VerboseMode has no effect on the MQTT format itself: no '~' common-name segments
// appear even though verbose mode is on and this same primary item DOES produce '~' segments
// in the standard LocalId format (see Valid_Test_Data's "411.1/C101.63/S206" verbose case).
mqttLocalId.ToString().Should().NotContain("~");
}

[Fact]
public void Test_MqttLocalId_Equality()
{
var (_, vis) = VISTests.GetVis();
var visVersion = VisVersion.v3_4a;
var gmod = vis.GetGmod(visVersion);
var codebooks = vis.GetCodebooks(visVersion);

var primaryItem = gmod.ParsePath("411.1/C101.31-2");
var qtyTag = codebooks.TryCreateTag(CodebookName.Quantity, "temperature");

var builder = LocalIdBuilder.Create(visVersion).TryWithPrimaryItem(primaryItem).TryWithMetadataTag(qtyTag);

var a = builder.BuildMqtt();
var b = builder.BuildMqtt();

var otherTag = codebooks.TryCreateTag(CodebookName.Quantity, "pressure");
var otherBuilder = LocalIdBuilder
.Create(visVersion)
.TryWithPrimaryItem(primaryItem)
.TryWithMetadataTag(otherTag);
var c = otherBuilder.BuildMqtt();

a.Should().Be(b);
(a == b).Should().BeTrue();
(a != b).Should().BeFalse();
a.GetHashCode().Should().Be(b.GetHashCode());

a.Should().NotBe(c);
(a == c).Should().BeFalse();
(a != c).Should().BeTrue();
}

[Theory]
[MemberData(nameof(Valid_Test_Data))]
public void Test_LocalId_Build_Valid(Input input, string expectedOutput)
Expand Down
Loading
Loading