Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CLI/AlarmTriggerConfigJson.cs
T
Joseph Doherty 5a878b78d4 docs(xml): fill missing XML doc comments + strip task-tracking refs across src (fixdocs)
Add missing <summary>/<param>/<returns>/<typeparam> tags and switch
interface implementations to <inheritdoc/> across 106 files; strip
project bookkeeping identifiers (Task NN, #05-TNN, PLAN-04, StoreAndForward-0NN)
from shipped code comments while preserving the descriptive rationale.
Comment-only: zero code-logic lines changed; solution builds 0/0.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
2026-07-10 08:23:56 -04:00

110 lines
5.7 KiB
C#

using System.Text;
using System.Text.Json;
namespace ZB.MOM.WW.ScadaBridge.CLI;
/// <summary>
/// Serializes typed alarm-setpoint CLI flags into the trigger-config JSON the
/// server expects. Key names MUST stay in lockstep with the canonical codec at
/// src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs
/// (that codec is internal to CentralUI, so this is a deliberate CLI-side mirror;
/// the round-trip test verifies the JSON against the live server — the real contract).
/// </summary>
internal static class AlarmTriggerConfigJson
{
/// <summary>
/// Builds the trigger-config JSON for <paramref name="triggerType"/> from the typed
/// flags, or returns null when none are supplied (so the alarm is created without a
/// trigger config). Unknown/blank trigger types yield null.
/// </summary>
/// <param name="triggerType">The trigger type string (case-insensitive).</param>
/// <param name="attribute">Attribute name (non-Expression trigger types).</param>
/// <param name="matchValue">ValueMatch: value to compare against.</param>
/// <param name="notEquals">ValueMatch: invert the comparison.</param>
/// <param name="min">RangeViolation: minimum allowed value.</param>
/// <param name="max">RangeViolation: maximum allowed value.</param>
/// <param name="thresholdPerSecond">RateOfChange: rate threshold per second.</param>
/// <param name="windowSeconds">RateOfChange: sliding window in seconds.</param>
/// <param name="direction">RateOfChange: direction (rising|falling|either).</param>
/// <param name="loLo">HiLo: low-low setpoint.</param>
/// <param name="lo">HiLo: low setpoint.</param>
/// <param name="hi">HiLo: high setpoint.</param>
/// <param name="hiHi">HiLo: high-high setpoint.</param>
/// <param name="expression">Expression: boolean trigger expression.</param>
/// <param name="analysisKind">
/// Optional analysis kind for Expression triggers ("strict" → emits
/// <c>"analysisKind":"Strict"</c>; null/"advisory"/anything else → Advisory default,
/// key omitted). Ignored for non-Expression trigger types.
/// </param>
/// <returns>The trigger-config JSON string, or <c>null</c> when no typed flags were supplied or the trigger type is unrecognized.</returns>
internal static string? Build(
string triggerType, string? attribute,
string? matchValue, bool notEquals,
double? min, double? max,
double? thresholdPerSecond, double? windowSeconds, string? direction,
double? loLo, double? lo, double? hi, double? hiHi,
string? expression,
string? analysisKind = null)
{
var type = triggerType?.Trim();
var anyTyped = attribute is not null || matchValue is not null || notEquals
|| min.HasValue || max.HasValue || thresholdPerSecond.HasValue || windowSeconds.HasValue
|| direction is not null || loLo.HasValue || lo.HasValue || hi.HasValue || hiHi.HasValue
|| expression is not null;
if (!anyTyped) return null;
using var stream = new MemoryStream();
using (var w = new Utf8JsonWriter(stream))
{
w.WriteStartObject();
if (!string.Equals(type, "Expression", StringComparison.OrdinalIgnoreCase))
w.WriteString("attributeName", attribute ?? "");
switch (type?.ToLowerInvariant())
{
case "valuematch":
var mv = matchValue ?? "";
if (notEquals) mv = "!=" + mv;
w.WriteString("matchValue", mv);
break;
case "rangeviolation":
if (min.HasValue) w.WriteNumber("min", min.Value);
if (max.HasValue) w.WriteNumber("max", max.Value);
break;
case "rateofchange":
if (thresholdPerSecond.HasValue) w.WriteNumber("thresholdPerSecond", thresholdPerSecond.Value);
if (windowSeconds.HasValue) w.WriteNumber("windowSeconds", windowSeconds.Value);
w.WriteString("direction", NormalizeDirection(direction));
break;
case "hilo":
// Only the four setpoints are exposed as flags. The codec also accepts
// per-setpoint priorities/deadbands/messages — intentionally omitted here;
// use raw --trigger-config for those (see the YAGNI scope guard in the plan).
if (loLo.HasValue) w.WriteNumber("loLo", loLo.Value);
if (lo.HasValue) w.WriteNumber("lo", lo.Value);
if (hi.HasValue) w.WriteNumber("hi", hi.Value);
if (hiHi.HasValue) w.WriteNumber("hiHi", hiHi.Value);
break;
case "expression":
w.WriteString("expression", expression ?? "");
// Emit "analysisKind":"Strict" only when the caller passes
// --trigger-kind strict (case-insensitive); Advisory (the default) is
// expressed by omitting the key, matching ValidationService.IsStrictAnalysis.
if (string.Equals(analysisKind?.Trim(), "Strict", StringComparison.OrdinalIgnoreCase))
w.WriteString("analysisKind", "Strict");
break;
}
w.WriteEndObject();
}
return Encoding.UTF8.GetString(stream.ToArray());
}
// Mirrors AlarmTriggerConfigCodec.NormalizeDirection.
private static string NormalizeDirection(string? raw) => raw?.ToLowerInvariant() switch
{
"rising" or "up" or "positive" => "rising",
"falling" or "down" or "negative" => "falling",
_ => "either",
};
}