using System.Text.Json; using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Deployment; /// /// Pure, thread-safe deploy-time gate: given a flattened-configuration JSON, it /// compiles every script, every alarm on-trigger script, and every Expression /// trigger just as does at /// runtime — but WITHOUT touching any actor state. It returns the collected /// compile errors so the Deployment Manager can reject a deployment whose scripts /// do not compile on the site (design spec: site-side compile failure rejects the /// deployment; no partial state is applied). /// /// Because it holds no actor state and only calls the (stateless) /// , it is pure and thread-safe; the Deployment /// Manager nevertheless invokes it synchronously on the singleton's actor thread /// as a pure prefix step, preserving mailbox-FIFO deploy ordering (see /// ). Compiles of unchanged /// script bodies are deduped by the process-wide compile cache. /// public sealed class DeployCompileValidator { private readonly ScriptCompilationService _compilationService; /// Initializes a new instance of the . /// The same compilation service the Instance Actor uses at runtime. public DeployCompileValidator(ScriptCompilationService compilationService) => _compilationService = compilationService; /// /// Validates every compilable artifact in the flattened configuration. /// Returns an empty list when the whole configuration compiles; otherwise /// one human-readable error per failing artifact. Malformed JSON is itself a /// single error (this also fronts the poison-config case for startup load). /// /// The flattened-configuration JSON to validate. /// A list of compile-error messages; empty if the configuration is clean. public IReadOnlyList Validate(string configJson) { FlattenedConfiguration? config; try { config = JsonSerializer.Deserialize(configJson); } catch (JsonException ex) { return [$"Configuration JSON failed to deserialize: {ex.Message}"]; } if (config == null) return ["Configuration JSON failed to deserialize: result was null."]; var errors = new List(); // Scripts: full execution-path compile against ScriptGlobals. foreach (var script in config.Scripts) { var result = _compilationService.Compile(script.CanonicalName, script.Code); if (!result.IsSuccess) errors.Add($"Script '{script.CanonicalName}': {string.Join("; ", result.Errors)}"); // Expression-triggered scripts also compile their trigger expression. AddTriggerExpressionError(errors, script.TriggerType, script.TriggerConfiguration, $"script-trigger-{script.CanonicalName}"); } // Alarms: on-trigger script reference must resolve + compile, and any // Expression trigger must compile — mirrors InstanceActor.CreateChildActors. foreach (var alarm in config.Alarms) { if (!string.IsNullOrEmpty(alarm.OnTriggerScriptCanonicalName)) { var triggerScriptDef = config.Scripts .FirstOrDefault(s => s.CanonicalName == alarm.OnTriggerScriptCanonicalName); if (triggerScriptDef == null) { errors.Add($"Alarm '{alarm.CanonicalName}': on-trigger script " + $"'{alarm.OnTriggerScriptCanonicalName}' is not defined."); } else { var result = _compilationService.Compile( $"alarm-trigger-{alarm.CanonicalName}", triggerScriptDef.Code); if (!result.IsSuccess) errors.Add($"Alarm '{alarm.CanonicalName}' on-trigger script " + $"'{alarm.OnTriggerScriptCanonicalName}': {string.Join("; ", result.Errors)}"); } } AddTriggerExpressionError(errors, alarm.TriggerType, alarm.TriggerConfiguration, $"alarm-trigger-expr-{alarm.CanonicalName}"); } return errors; } /// /// Compiles the boolean trigger expression for an Expression-triggered script /// or alarm and records any failure. No-op for non-Expression triggers or a /// blank/malformed expression config — mirroring /// InstanceActor.CompileTriggerExpression, where those cases yield an /// inert trigger rather than a deployment error. /// private void AddTriggerExpressionError( List errors, string? triggerType, string? triggerConfigJson, string compileName) { if (!string.Equals(triggerType, "Expression", StringComparison.OrdinalIgnoreCase)) return; var expression = TriggerExpressionGlobals.ExtractExpression(triggerConfigJson); if (expression == null) return; var result = _compilationService.CompileTriggerExpression(compileName, expression); if (!result.IsSuccess) errors.Add($"Trigger expression '{compileName}': {string.Join("; ", result.Errors)}"); } }