9cff87fe85
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
282 lines
13 KiB
C#
282 lines
13 KiB
C#
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
|
|
|
/// <summary>
|
|
/// Which kind of trigger a template script has. <see cref="None"/> is no
|
|
/// trigger; <see cref="Unknown"/> is a stored trigger-type string the runtime
|
|
/// does not recognize (preserved as-is by the editor).
|
|
/// </summary>
|
|
internal enum ScriptTriggerKind { None, Interval, ValueChange, Conditional, Call, Expression, Unknown }
|
|
|
|
/// <summary>
|
|
/// When a Conditional/Expression trigger fires. <see cref="OnTrue"/> fires once
|
|
/// as the condition becomes true; <see cref="WhileTrue"/> re-fires on a timer
|
|
/// (cadence = the script's MinTimeBetweenRuns) while the condition stays true.
|
|
/// </summary>
|
|
internal enum ScriptTriggerMode { OnTrue, WhileTrue }
|
|
|
|
/// <summary>A script's trigger as the editor emits it: a type string + config JSON.</summary>
|
|
public sealed record ScriptTriggerValue(string? TriggerType, string? Config);
|
|
|
|
/// <summary>Parsed/editable view of a script trigger's configuration.</summary>
|
|
internal sealed class ScriptTriggerModel
|
|
{
|
|
/// <summary>Interval period in milliseconds.</summary>
|
|
public long? IntervalMs { get; set; }
|
|
|
|
/// <summary>Monitored attribute (ValueChange + Conditional).</summary>
|
|
public string? AttributeName { get; set; }
|
|
|
|
/// <summary>Comparison operator (Conditional) — one of <see cref="ScriptTriggerConfigCodec.Operators"/>.</summary>
|
|
public string Operator { get; set; } = ">";
|
|
|
|
/// <summary>Comparison threshold (Conditional).</summary>
|
|
public double? Threshold { get; set; }
|
|
|
|
/// <summary>Boolean C# expression (Expression).</summary>
|
|
public string? Expression { get; set; }
|
|
|
|
/// <summary>Fire mode (Conditional + Expression). Defaults to <see cref="ScriptTriggerMode.OnTrue"/>.</summary>
|
|
public ScriptTriggerMode Mode { get; set; } = ScriptTriggerMode.OnTrue;
|
|
|
|
// Per-trigger analysis kind (Expression only). When true the
|
|
// codec serializes "analysisKind":"Strict"; when false (Advisory, the
|
|
// default) the key is omitted. Matches ValidationService.IsStrictAnalysis.
|
|
/// <summary>
|
|
/// When <see langword="true"/>, the trigger config carries
|
|
/// <c>"analysisKind":"Strict"</c> so ValidationService escalates the
|
|
/// blank-expression advisory to a deploy-blocking error.
|
|
/// </summary>
|
|
public bool IsStrictAnalysisKind { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round-trip codec for a template script's <c>TriggerType</c> +
|
|
/// <c>TriggerConfiguration</c>, shared by <see cref="ScriptTriggerEditor"/> (UI
|
|
/// editing) and consumed at the site by <c>ScriptActor.ParseTriggerConfig</c>.
|
|
/// Serialized config shapes:
|
|
/// Interval { intervalMs }
|
|
/// ValueChange { attributeName }
|
|
/// Conditional { attributeName, operator, threshold, mode }
|
|
/// Call { }
|
|
/// Expression { expression, mode }
|
|
///
|
|
/// <c>mode</c> (Conditional + Expression) is <c>OnTrue</c> or <c>WhileTrue</c>;
|
|
/// an absent or unrecognized value parses as <c>OnTrue</c>.
|
|
///
|
|
/// Parsing also accepts the legacy aliases <c>attribute</c> and <c>value</c> so
|
|
/// older configs survive a round-trip through the editor.
|
|
/// </summary>
|
|
internal static class ScriptTriggerConfigCodec
|
|
{
|
|
/// <summary>The six comparison operators <c>ScriptActor.EvaluateCondition</c> accepts.</summary>
|
|
internal static readonly string[] Operators = { ">", ">=", "<", "<=", "==", "!=" };
|
|
|
|
/// <summary>Classifies a raw <c>TriggerType</c> string (case-insensitive).</summary>
|
|
/// <param name="triggerType">The raw trigger type string from the template script entity.</param>
|
|
/// <returns>The matching <see cref="ScriptTriggerKind"/>, or <see cref="ScriptTriggerKind.None"/> for null/empty.</returns>
|
|
internal static ScriptTriggerKind ParseKind(string? triggerType)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(triggerType)) return ScriptTriggerKind.None;
|
|
return triggerType.Trim().ToLowerInvariant() switch
|
|
{
|
|
"interval" => ScriptTriggerKind.Interval,
|
|
"valuechange" => ScriptTriggerKind.ValueChange,
|
|
"conditional" => ScriptTriggerKind.Conditional,
|
|
"call" => ScriptTriggerKind.Call,
|
|
"expression" => ScriptTriggerKind.Expression,
|
|
_ => ScriptTriggerKind.Unknown
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether a trigger type honours the script's <c>MinTimeBetweenRuns</c>.
|
|
/// True for the auto-firing triggers it throttles — ValueChange, Conditional,
|
|
/// Expression. False for Interval (its own period is the cadence), Call
|
|
/// (invoked explicitly, never throttled), and None/Unknown.
|
|
/// </summary>
|
|
/// <param name="triggerType">The raw trigger type string to classify.</param>
|
|
/// <returns><see langword="true"/> if the trigger honours <c>MinTimeBetweenRuns</c>; otherwise <see langword="false"/>.</returns>
|
|
internal static bool SupportsMinTimeBetweenRuns(string? triggerType) =>
|
|
ParseKind(triggerType) is ScriptTriggerKind.ValueChange
|
|
or ScriptTriggerKind.Conditional
|
|
or ScriptTriggerKind.Expression;
|
|
|
|
/// <summary>Canonical <c>TriggerType</c> string for a kind; null for None/Unknown.</summary>
|
|
/// <param name="kind">The trigger kind to convert.</param>
|
|
/// <returns>The canonical trigger-type string, or null for <see cref="ScriptTriggerKind.None"/>/<see cref="ScriptTriggerKind.Unknown"/>.</returns>
|
|
internal static string? KindToString(ScriptTriggerKind kind) => kind switch
|
|
{
|
|
ScriptTriggerKind.Interval => "Interval",
|
|
ScriptTriggerKind.ValueChange => "ValueChange",
|
|
ScriptTriggerKind.Conditional => "Conditional",
|
|
ScriptTriggerKind.Call => "Call",
|
|
ScriptTriggerKind.Expression => "Expression",
|
|
_ => null
|
|
};
|
|
|
|
/// <summary>
|
|
/// Parses a trigger configuration JSON in the context of the given kind.
|
|
/// Returns a model with default values on null/empty/malformed input or for
|
|
/// missing keys — never throws.
|
|
/// </summary>
|
|
/// <param name="json">The raw JSON trigger configuration string.</param>
|
|
/// <param name="kind">The trigger kind, used to determine which fields to parse.</param>
|
|
/// <returns>A <see cref="ScriptTriggerModel"/> populated from the JSON; defaults are used for absent or malformed fields.</returns>
|
|
internal static ScriptTriggerModel Parse(string? json, ScriptTriggerKind kind)
|
|
{
|
|
var model = new ScriptTriggerModel();
|
|
if (string.IsNullOrWhiteSpace(json)) return model;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(json);
|
|
var root = doc.RootElement;
|
|
|
|
switch (kind)
|
|
{
|
|
case ScriptTriggerKind.Interval:
|
|
model.IntervalMs = TryReadLong(root, "intervalMs");
|
|
break;
|
|
|
|
case ScriptTriggerKind.ValueChange:
|
|
model.AttributeName = TryReadAttributeName(root);
|
|
break;
|
|
|
|
case ScriptTriggerKind.Conditional:
|
|
model.AttributeName = TryReadAttributeName(root);
|
|
var op = root.TryGetProperty("operator", out var o) ? o.GetString() : null;
|
|
model.Operator = NormalizeOperator(op);
|
|
model.Threshold = TryReadDouble(root, "threshold") ?? TryReadDouble(root, "value");
|
|
model.Mode = ReadMode(root);
|
|
break;
|
|
|
|
case ScriptTriggerKind.Expression:
|
|
model.Expression = root.TryGetProperty("expression", out var e) ? e.GetString() : null;
|
|
model.Mode = ReadMode(root);
|
|
// Read optional analysisKind discriminator (matches
|
|
// ValidationService.IsStrictAnalysis — "Strict" case-insensitive → strict;
|
|
// absent/"Advisory"/anything else → Advisory default).
|
|
if (root.TryGetProperty("analysisKind", out var ak)
|
|
&& ak.ValueKind == JsonValueKind.String)
|
|
{
|
|
model.IsStrictAnalysisKind = string.Equals(
|
|
ak.GetString(), "Strict", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Malformed JSON — fall through with default model.
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serializes the model to the JSON shape <c>ScriptActor.ParseTriggerConfig</c>
|
|
/// expects. Returns null for None/Unknown (no structured config to emit).
|
|
/// </summary>
|
|
/// <param name="model">The trigger model to serialize.</param>
|
|
/// <param name="kind">The trigger kind, used to determine which fields to emit.</param>
|
|
/// <returns>The JSON configuration string, or null for <see cref="ScriptTriggerKind.None"/>/<see cref="ScriptTriggerKind.Unknown"/>.</returns>
|
|
internal static string? Serialize(ScriptTriggerModel model, ScriptTriggerKind kind)
|
|
{
|
|
if (kind is ScriptTriggerKind.None or ScriptTriggerKind.Unknown) return null;
|
|
|
|
using var stream = new MemoryStream();
|
|
using (var w = new Utf8JsonWriter(stream))
|
|
{
|
|
w.WriteStartObject();
|
|
switch (kind)
|
|
{
|
|
case ScriptTriggerKind.Interval:
|
|
if (model.IntervalMs.HasValue)
|
|
w.WriteNumber("intervalMs", model.IntervalMs.Value);
|
|
break;
|
|
|
|
case ScriptTriggerKind.ValueChange:
|
|
w.WriteString("attributeName", model.AttributeName ?? "");
|
|
break;
|
|
|
|
case ScriptTriggerKind.Conditional:
|
|
w.WriteString("attributeName", model.AttributeName ?? "");
|
|
w.WriteString("operator", model.Operator);
|
|
if (model.Threshold.HasValue)
|
|
w.WriteNumber("threshold", model.Threshold.Value);
|
|
w.WriteString("mode", model.Mode.ToString());
|
|
break;
|
|
|
|
case ScriptTriggerKind.Expression:
|
|
w.WriteString("expression", model.Expression ?? "");
|
|
w.WriteString("mode", model.Mode.ToString());
|
|
// Emit "analysisKind":"Strict" only when explicitly set;
|
|
// Advisory is the default so the key is omitted to stay backward-compatible.
|
|
if (model.IsStrictAnalysisKind)
|
|
w.WriteString("analysisKind", "Strict");
|
|
break;
|
|
|
|
// Call → empty object.
|
|
}
|
|
w.WriteEndObject();
|
|
}
|
|
return Encoding.UTF8.GetString(stream.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the optional <c>mode</c> property; an absent or unrecognized value
|
|
/// (case-insensitive) yields <see cref="ScriptTriggerMode.OnTrue"/>.
|
|
/// </summary>
|
|
private static ScriptTriggerMode ReadMode(JsonElement root)
|
|
{
|
|
var raw = root.TryGetProperty("mode", out var m) ? m.GetString() : null;
|
|
return string.Equals(raw?.Trim(), "WhileTrue", StringComparison.OrdinalIgnoreCase)
|
|
? ScriptTriggerMode.WhileTrue
|
|
: ScriptTriggerMode.OnTrue;
|
|
}
|
|
|
|
/// <summary>Returns <paramref name="raw"/> if it is a recognized operator, else ">".</summary>
|
|
/// <param name="raw">The raw operator string to normalize.</param>
|
|
/// <returns>A valid operator string from <see cref="Operators"/>; defaults to ">" for unrecognized input.</returns>
|
|
internal static string NormalizeOperator(string? raw)
|
|
{
|
|
var op = raw?.Trim();
|
|
return op != null && Array.IndexOf(Operators, op) >= 0 ? op : ">";
|
|
}
|
|
|
|
private static string? TryReadAttributeName(JsonElement root) =>
|
|
root.TryGetProperty("attributeName", out var a) ? a.GetString()
|
|
: root.TryGetProperty("attribute", out var a2) ? a2.GetString()
|
|
: null;
|
|
|
|
private static long? TryReadLong(JsonElement el, string name)
|
|
{
|
|
if (!el.TryGetProperty(name, out var p)) return null;
|
|
return p.ValueKind switch
|
|
{
|
|
JsonValueKind.Number when p.TryGetInt64(out var i) => i,
|
|
JsonValueKind.Number => (long)p.GetDouble(),
|
|
JsonValueKind.String when long.TryParse(
|
|
p.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) => v,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private static double? TryReadDouble(JsonElement el, string name)
|
|
{
|
|
if (!el.TryGetProperty(name, out var p)) return null;
|
|
return p.ValueKind switch
|
|
{
|
|
JsonValueKind.Number => p.GetDouble(),
|
|
JsonValueKind.String when double.TryParse(
|
|
p.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var v) => v,
|
|
_ => null
|
|
};
|
|
}
|
|
}
|