Skip to content
Draft
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
132 changes: 132 additions & 0 deletions libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditionWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.

using MTConnect.Observations;
using System.Text.Json.Serialization;

namespace MTConnect.Streams.Json
{
/// <summary>
/// One Condition observation as it appears on the wire in the cppagent
/// JSON v2 shape: an object with exactly one of <see cref="Fault"/>,
/// <see cref="Warning"/>, <see cref="Normal"/>, or <see cref="Unavailable"/>
/// set and the other three <see langword="null"/>. The three null members
/// are suppressed on serialization by
/// <see cref="JsonIgnoreCondition.WhenWritingNull"/>, producing the
/// single-key envelope cppagent emits (e.g. <c>{"Normal": {...}}</c>).
/// </summary>
/// <remarks>
/// The four properties are <b>data-carriers only</b>: setting more than
/// one at a time produces a wire shape cppagent will reject on read but
/// the type does not enforce single-key invariants at runtime. Callers
/// should prefer the <c>Of*</c> factory methods, which construct a
/// wrapper with exactly one property populated.
/// <para>
/// <see cref="Value"/> and <see cref="Level"/> are convenience read-side
/// accessors for consumers that iterate a <see cref="JsonConditions"/>
/// list without pattern-matching on which property is non-null; both are
/// suppressed from serialization.
/// </para>
/// </remarks>
public sealed class JsonConditionWrapper
{
/// <summary>
/// Condition entry at <c>FAULT</c> level, or <see langword="null"/>
/// when this wrapper carries a different level. Serializes as the
/// wire property <c>Fault</c>.
/// </summary>
[JsonPropertyName("Fault")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonCondition Fault { get; set; }

/// <summary>
/// Condition entry at <c>WARNING</c> level, or <see langword="null"/>
/// when this wrapper carries a different level. Serializes as the
/// wire property <c>Warning</c>.
/// </summary>
[JsonPropertyName("Warning")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonCondition Warning { get; set; }

/// <summary>
/// Condition entry at <c>NORMAL</c> level, or <see langword="null"/>
/// when this wrapper carries a different level. Serializes as the
/// wire property <c>Normal</c>.
/// </summary>
[JsonPropertyName("Normal")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonCondition Normal { get; set; }

/// <summary>
/// Condition entry at <c>UNAVAILABLE</c> level, or <see langword="null"/>
/// when this wrapper carries a different level. Serializes as the
/// wire property <c>Unavailable</c>.
/// </summary>
[JsonPropertyName("Unavailable")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonCondition Unavailable { get; set; }

/// <summary>
/// The single non-null condition carried by this wrapper, or
/// <see langword="null"/> when the wrapper is empty. Precedence
/// order on multi-populated wrappers is
/// Fault → Warning → Normal → Unavailable, matching
/// <see cref="Level"/>. Never serialized.
/// </summary>
[JsonIgnore]
public JsonCondition Value => Fault ?? Warning ?? Normal ?? Unavailable;

/// <summary>
/// The wire property name of the single non-null level carried by
/// this wrapper (<c>"Fault"</c>, <c>"Warning"</c>, <c>"Normal"</c>,
/// or <c>"Unavailable"</c>), or <see langword="null"/> when the
/// wrapper is empty. Never serialized.
/// </summary>
[JsonIgnore]
public string Level =>
Fault != null ? "Fault" :
Warning != null ? "Warning" :
Normal != null ? "Normal" :
Unavailable != null ? "Unavailable" : null;

/// <summary>
/// Constructs a wrapper carrying the given condition at
/// <c>FAULT</c> level.
/// </summary>
public static JsonConditionWrapper OfFault(JsonCondition condition) => new JsonConditionWrapper { Fault = condition };

/// <summary>
/// Constructs a wrapper carrying the given condition at
/// <c>WARNING</c> level.
/// </summary>
public static JsonConditionWrapper OfWarning(JsonCondition condition) => new JsonConditionWrapper { Warning = condition };

/// <summary>
/// Constructs a wrapper carrying the given condition at
/// <c>NORMAL</c> level.
/// </summary>
public static JsonConditionWrapper OfNormal(JsonCondition condition) => new JsonConditionWrapper { Normal = condition };

/// <summary>
/// Constructs a wrapper carrying the given condition at
/// <c>UNAVAILABLE</c> level.
/// </summary>
public static JsonConditionWrapper OfUnavailable(JsonCondition condition) => new JsonConditionWrapper { Unavailable = condition };

/// <summary>
/// Materializes this wrapper's condition into a strongly-typed
/// <see cref="IConditionObservation"/> at the level indicated by
/// which property is non-null, or <see langword="null"/> when the
/// wrapper is empty. Precedence on multi-populated wrappers
/// matches <see cref="Level"/>.
/// </summary>
public IConditionObservation ToObservation()
{
if (Fault != null) return Fault.ToCondition(ConditionLevel.FAULT);
if (Warning != null) return Warning.ToCondition(ConditionLevel.WARNING);
if (Normal != null) return Normal.ToCondition(ConditionLevel.NORMAL);
if (Unavailable != null) return Unavailable.ToCondition(ConditionLevel.UNAVAILABLE);
return null;
}
}
}
212 changes: 74 additions & 138 deletions libraries/MTConnect.NET-JSON-cppagent/Streams/JsonConditions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.

using MTConnect.Observations;
Expand All @@ -10,163 +10,99 @@
namespace MTConnect.Streams.Json
{
/// <summary>
/// Typed representation of a Condition list on a Component stream,
/// bucketed by Condition level (Fault, Warning, Normal, Unavailable).
/// Sequence of Condition observations in the cppagent JSON v2 shape:
/// a flat JSON array of single-key <see cref="JsonConditionWrapper"/>
/// objects, one wrapper per observation, in insertion order.
/// </summary>
/// <remarks>
/// Serialized via <see cref="JsonConditionsConverter"/> in the
/// cppagent JSON v2 wire shape: an array of single-key wrapper
/// objects, one per Condition entry
/// (e.g. <c>[{"Normal": {...}}, {"Warning": {...}}]</c>).
/// The level order on the wire is fixed at Fault, Warning, Normal,
/// Unavailable; mixed-level interleaving is not round-trip preserved
/// through this typed model. The legacy MTConnect JSON v1
/// object-keyed shape (<c>{"Fault": [...], "Warning": [...], ...}</c>)
/// is still accepted on the read path for back-compat.
/// The type shape IS the wire shape — <see cref="JsonConditions"/>
/// inherits <see cref="List{T}"/> of <see cref="JsonConditionWrapper"/>
/// so System.Text.Json's default serializer handles both directions
/// with no custom converter. Emitting bytes:
/// <code>[{"Normal":{...}},{"Warning":{...}},{"Fault":{...}}]</code>
/// requires only that each wrapper carries exactly one non-null
/// property (enforced by convention, not by runtime validation) and
/// that the four properties on <see cref="JsonConditionWrapper"/> are
/// annotated with
/// <c>[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]</c>.
/// <para>
/// Element ordering on the wire follows list insertion order. The
/// <see cref="JsonConditions(IEnumerable{IObservationOutput})"/> ctor
/// preserves the historical level-order emission
/// (<c>FAULT</c> → <c>WARNING</c> → <c>NORMAL</c> → <c>UNAVAILABLE</c>,
/// source order within each level) so this rewrite is byte-identical
/// on the wire to the pre-refactor converter output for the same
/// observation input; callers constructing via the copy ctor or
/// direct <see cref="List{T}.Add(T)"/> control ordering explicitly.
/// </para>
/// </remarks>
[System.Text.Json.Serialization.JsonConverter(typeof(JsonConditionsConverter))]
public class JsonConditions
public sealed class JsonConditions : List<JsonConditionWrapper>
{
/// <summary>
/// Materializes every level bucket into a flat list of
/// <see cref="IObservation"/> instances, tagged with the
/// corresponding <see cref="ConditionLevel"/>. Enumeration order
/// matches the wire-emission order: Fault, then Warning, then
/// Normal, then Unavailable.
/// Initializes an empty container for JSON deserialization or
/// programmatic construction via <see cref="List{T}.Add(T)"/>.
/// </summary>
[JsonIgnore]
public List<IObservation> Observations
{
get
{
var l = new List<IObservation>();

if (!Fault.IsNullOrEmpty())
{
foreach (var x in Fault) l.Add(x.ToCondition(ConditionLevel.FAULT));
}

if (!Warning.IsNullOrEmpty())
{
foreach (var x in Warning) l.Add(x.ToCondition(ConditionLevel.WARNING));
}

if (!Normal.IsNullOrEmpty())
{
foreach (var x in Normal) l.Add(x.ToCondition(ConditionLevel.NORMAL));
}

if (!Unavailable.IsNullOrEmpty())
{
foreach (var x in Unavailable) l.Add(x.ToCondition(ConditionLevel.UNAVAILABLE));
}

return l;
}
}

/// <summary>
/// Condition entries at <c>FAULT</c> level. Source order is
/// preserved within the bucket; entries are emitted on the wire
/// as <c>{"Fault": {...}}</c> wrapper objects, ahead of every
/// other level.
/// </summary>
[JsonPropertyName("Fault")]
public IEnumerable<JsonCondition> Fault { get; set; }

/// <summary>
/// Condition entries at <c>WARNING</c> level. Source order is
/// preserved within the bucket; entries are emitted on the wire
/// as <c>{"Warning": {...}}</c> wrapper objects, after Fault
/// and before Normal.
/// </summary>
[JsonPropertyName("Warning")]
public IEnumerable<JsonCondition> Warning { get; set; }
public JsonConditions() { }

/// <summary>
/// Condition entries at <c>NORMAL</c> level. Source order is
/// preserved within the bucket; entries are emitted on the wire
/// as <c>{"Normal": {...}}</c> wrapper objects, after Warning
/// and before Unavailable.
/// Initializes the container with a pre-built sequence of
/// wrappers. Order is preserved from
/// <paramref name="wrappers"/>.
/// </summary>
[JsonPropertyName("Normal")]
public IEnumerable<JsonCondition> Normal { get; set; }
public JsonConditions(IEnumerable<JsonConditionWrapper> wrappers)
: base(wrappers ?? Enumerable.Empty<JsonConditionWrapper>())
{
}

/// <summary>
/// Condition entries at <c>UNAVAILABLE</c> level. Source order
/// is preserved within the bucket; entries are emitted on the
/// wire as <c>{"Unavailable": {...}}</c> wrapper objects, after
/// every other level.
/// Initializes the container from an observation-output
/// sequence, wrapping each observation in the single-key wrapper
/// for its level. Preserves the historical level-order emission
/// (<c>FAULT</c> → <c>WARNING</c> → <c>NORMAL</c> → <c>UNAVAILABLE</c>,
/// source order within each level) so the wire output is
/// byte-identical to the pre-refactor converter for the same
/// input.
/// </summary>
[JsonPropertyName("Unavailable")]
public IEnumerable<JsonCondition> Unavailable { get; set; }

public JsonConditions(IEnumerable<IObservationOutput> observations)
{
if (observations == null) return;

/// <summary>
/// Initializes an empty instance for JSON deserialization.
/// </summary>
public JsonConditions() { }
AppendLevel(observations, ConditionLevel.FAULT, JsonConditionWrapper.OfFault);
AppendLevel(observations, ConditionLevel.WARNING, JsonConditionWrapper.OfWarning);
AppendLevel(observations, ConditionLevel.NORMAL, JsonConditionWrapper.OfNormal);
AppendLevel(observations, ConditionLevel.UNAVAILABLE, JsonConditionWrapper.OfUnavailable);
}

/// <summary>
/// Initializes the container from an observation-output
/// sequence, partitioning each observation into the
/// Fault/Warning/Normal/Unavailable bucket indicated by its
/// <c>Level</c> value-bag entry.
/// Materializes every wrapper into a flat
/// <see cref="List{T}"/> of <see cref="IObservation"/>, in list
/// insertion order. Wrappers whose four level properties are all
/// <see langword="null"/> are skipped. Not serialized.
/// </summary>
public JsonConditions(IEnumerable<IObservationOutput> observations)
[JsonIgnore]
public List<IObservation> Observations
{
if (observations != null)
get
{
if (!observations.IsNullOrEmpty())
var result = new List<IObservation>(Count);
foreach (var wrapper in this)
{
// Add Fault
var levelObservations = observations.Where(o => o.GetValue(ValueKeys.Level) == ConditionLevel.FAULT.ToString());
if (!levelObservations.IsNullOrEmpty())
{
var jsonObservations = new List<JsonCondition>();
foreach (var observation in levelObservations)
{
jsonObservations.Add(new JsonCondition(observation));
}
Fault = jsonObservations;
}

// Add Warning
levelObservations = observations.Where(o => o.GetValue(ValueKeys.Level) == ConditionLevel.WARNING.ToString());
if (!levelObservations.IsNullOrEmpty())
{
var jsonObservations = new List<JsonCondition>();
foreach (var observation in levelObservations)
{
jsonObservations.Add(new JsonCondition(observation));
}
Warning = jsonObservations;
}

// Add Normal
levelObservations = observations.Where(o => o.GetValue(ValueKeys.Level) == ConditionLevel.NORMAL.ToString());
if (!levelObservations.IsNullOrEmpty())
{
var jsonObservations = new List<JsonCondition>();
foreach (var observation in levelObservations)
{
jsonObservations.Add(new JsonCondition(observation));
}
Normal = jsonObservations;
}

// Add Unavailable
levelObservations = observations.Where(o => o.GetValue(ValueKeys.Level) == ConditionLevel.UNAVAILABLE.ToString());
if (!levelObservations.IsNullOrEmpty())
{
var jsonObservations = new List<JsonCondition>();
foreach (var observation in levelObservations)
{
jsonObservations.Add(new JsonCondition(observation));
}
Unavailable = jsonObservations;
}
var observation = wrapper?.ToObservation();
if (observation != null) result.Add(observation);
}
return result;
}
}

private void AppendLevel(
IEnumerable<IObservationOutput> observations,
ConditionLevel level,
System.Func<JsonCondition, JsonConditionWrapper> factory)
{
var levelName = level.ToString();
foreach (var observation in observations.Where(o => o != null && o.GetValue(ValueKeys.Level) == levelName))
{
Add(factory(new JsonCondition(observation)));
}
}
}
Expand Down
Loading