refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Indirection so ScriptAnalysisService can be unit-tested without standing
|
||||
/// up SharedScriptService and its EF Core repository chain.
|
||||
/// </summary>
|
||||
public interface ISharedScriptCatalog
|
||||
{
|
||||
/// <summary>Returns the parameter and return shapes for all registered shared scripts.</summary>
|
||||
Task<IReadOnlyList<ScriptShape>> GetShapesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the source code and metadata for a named shared script, or
|
||||
/// null if no shared script with that name exists. Used by Test Run to
|
||||
/// compile and execute nested CallShared invocations.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the shared script to retrieve.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the async lookup.</param>
|
||||
Task<SharedScriptSource?> GetByNameAsync(string name, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public record SharedScriptSource(string Name, string Code, string? ParameterDefinitions, string? ReturnDefinition);
|
||||
|
||||
public class SharedScriptCatalog : ISharedScriptCatalog
|
||||
{
|
||||
private readonly SharedScriptService _service;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SharedScriptCatalog"/> backed by the given service.
|
||||
/// </summary>
|
||||
/// <param name="service">Service providing access to shared script definitions.</param>
|
||||
public SharedScriptCatalog(SharedScriptService service) => _service = service;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ScriptShape>> GetShapesAsync()
|
||||
{
|
||||
var scripts = await _service.GetAllSharedScriptsAsync();
|
||||
return scripts
|
||||
.Select(s => ScriptShapeParser.Parse(s.Name, s.ParameterDefinitions, s.ReturnDefinition))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SharedScriptSource?> GetByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return null;
|
||||
var scripts = await _service.GetAllSharedScriptsAsync(cancellationToken);
|
||||
var s = scripts.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.Ordinal));
|
||||
return s == null ? null : new SharedScriptSource(s.Name, s.Code, s.ParameterDefinitions, s.ReturnDefinition);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Globals type seen by inbound API method scripts during analysis. Mirrors
|
||||
/// the surface the runtime exposes (see ZB.MOM.WW.ScadaBridge.InboundAPI.InboundScriptContext
|
||||
/// and RouteHelper). The methods here are never invoked — Roslyn only reads
|
||||
/// their signatures to type-check API method scripts and offer completions.
|
||||
/// </summary>
|
||||
public class InboundScriptHost
|
||||
{
|
||||
/// <summary>
|
||||
/// The request parameters passed to the inbound API method.
|
||||
/// </summary>
|
||||
public ScriptParameters Parameters { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The route helper for accessing target instances.
|
||||
/// </summary>
|
||||
public RouteHelper Route { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The cancellation token for the operation.
|
||||
/// </summary>
|
||||
public System.Threading.CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>Editor mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.RouteHelper.</summary>
|
||||
public class RouteHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Targets a specific instance for method invocation.
|
||||
/// </summary>
|
||||
/// <param name="instanceCode">The instance code to target.</param>
|
||||
public RouteTarget To(string instanceCode) => new();
|
||||
}
|
||||
|
||||
/// <summary>Editor mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.RouteTarget.</summary>
|
||||
public class RouteTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Calls a script on the target instance.
|
||||
/// </summary>
|
||||
/// <param name="scriptName">The name of the script to call.</param>
|
||||
/// <param name="parameters">Optional parameters to pass to the script.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public System.Threading.Tasks.Task<object?> Call(
|
||||
string scriptName,
|
||||
object? parameters = null,
|
||||
System.Threading.CancellationToken cancellationToken = default) =>
|
||||
System.Threading.Tasks.Task.FromResult<object?>(null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an attribute value from the target instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">The name of the attribute to retrieve.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public System.Threading.Tasks.Task<object?> GetAttribute(
|
||||
string attributeName,
|
||||
System.Threading.CancellationToken cancellationToken = default) =>
|
||||
System.Threading.Tasks.Task.FromResult<object?>(null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets multiple attribute values from the target instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeNames">The names of the attributes to retrieve.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public System.Threading.Tasks.Task<IReadOnlyDictionary<string, object?>> GetAttributes(
|
||||
IEnumerable<string> attributeNames,
|
||||
System.Threading.CancellationToken cancellationToken = default) =>
|
||||
System.Threading.Tasks.Task.FromResult<IReadOnlyDictionary<string, object?>>(
|
||||
new Dictionary<string, object?>());
|
||||
|
||||
/// <summary>
|
||||
/// Sets a single attribute value on the target instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">The name of the attribute to set.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public System.Threading.Tasks.Task SetAttribute(
|
||||
string attributeName,
|
||||
string value,
|
||||
System.Threading.CancellationToken cancellationToken = default) =>
|
||||
System.Threading.Tasks.Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Sets multiple attribute values on the target instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeValues">Dictionary of attribute names to values.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public System.Threading.Tasks.Task SetAttributes(
|
||||
IReadOnlyDictionary<string, string> attributeValues,
|
||||
System.Threading.CancellationToken cancellationToken = default) =>
|
||||
System.Threading.Tasks.Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Translates JSON Schema documents stored in
|
||||
/// <c>TemplateScript.ParameterDefinitions</c> / <c>ReturnDefinition</c> into
|
||||
/// the flat <see cref="ParameterShape"/> / type-name vocabulary used by the
|
||||
/// rest of the script-analysis pipeline (completions, inlay hints, signature
|
||||
/// help, hover).
|
||||
///
|
||||
/// Lenient: malformed JSON yields an empty result, never an exception.
|
||||
///
|
||||
/// Also accepts the legacy pre-migration flat shape
|
||||
/// (<c>[{name,type,required,itemType?}]</c> for parameters,
|
||||
/// <c>{type,itemType?}</c> for return) so partially migrated rows don't crash
|
||||
/// the editor.
|
||||
/// </summary>
|
||||
public static class JsonSchemaShapeParser
|
||||
{
|
||||
/// <summary>Parses a JSON Schema or legacy flat-array parameters definition and returns the resulting parameter shapes.</summary>
|
||||
/// <param name="json">The JSON string to parse; <c>null</c> or whitespace returns an empty list.</param>
|
||||
public static IReadOnlyList<ParameterShape> ParseParameters(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<ParameterShape>();
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return doc.RootElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Array => ParseLegacyParameterArray(doc.RootElement),
|
||||
JsonValueKind.Object => ParseJsonSchemaObject(doc.RootElement),
|
||||
_ => Array.Empty<ParameterShape>(),
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<ParameterShape>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parses a JSON Schema or legacy return-type definition and returns the normalised type name, or <c>null</c> if absent or unrecognised.</summary>
|
||||
/// <param name="json">The JSON string to parse; <c>null</c> or whitespace returns <c>null</c>.</param>
|
||||
public static string? ParseReturnType(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
||||
return ParseReturnSchema(doc.RootElement);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- JSON Schema branch -------------------------------------------------
|
||||
|
||||
private static IReadOnlyList<ParameterShape> ParseJsonSchemaObject(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("properties", out var props) || props.ValueKind != JsonValueKind.Object)
|
||||
return Array.Empty<ParameterShape>();
|
||||
|
||||
var requiredSet = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (root.TryGetProperty("required", out var req) && req.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in req.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var s = item.GetString();
|
||||
if (!string.IsNullOrEmpty(s)) requiredSet.Add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<ParameterShape>();
|
||||
foreach (var prop in props.EnumerateObject())
|
||||
{
|
||||
var name = prop.Name;
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
var type = MapJsonSchemaType(prop.Value);
|
||||
result.Add(new ParameterShape(name, type, requiredSet.Contains(name)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? ParseReturnSchema(JsonElement schema)
|
||||
{
|
||||
if (!schema.TryGetProperty("type", out var typeEl)) return null;
|
||||
if (typeEl.ValueKind != JsonValueKind.String) return null;
|
||||
var type = typeEl.GetString();
|
||||
if (string.IsNullOrEmpty(type)) return null;
|
||||
|
||||
// Legacy form: `{type:"List", itemType:"Integer"}` (post-migration this
|
||||
// should be `{type:"array", items:{type:"integer"}}`, handled below).
|
||||
if (type.Equals("List", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (schema.TryGetProperty("itemType", out var it) && it.ValueKind == JsonValueKind.String)
|
||||
return $"List<{NormalizeLegacyType(it.GetString())}>";
|
||||
return "List<Object>";
|
||||
}
|
||||
|
||||
if (type.Equals("array", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (schema.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var inner = MapJsonSchemaType(items);
|
||||
return $"List<{inner}>";
|
||||
}
|
||||
return "List<Object>";
|
||||
}
|
||||
|
||||
return MapJsonSchemaTypeName(type);
|
||||
}
|
||||
|
||||
private static string MapJsonSchemaType(JsonElement schema)
|
||||
{
|
||||
if (schema.ValueKind != JsonValueKind.Object) return "Object";
|
||||
if (!schema.TryGetProperty("type", out var typeEl) || typeEl.ValueKind != JsonValueKind.String)
|
||||
return "Object";
|
||||
|
||||
var type = typeEl.GetString() ?? "";
|
||||
if (type.Equals("array", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (schema.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Object)
|
||||
return $"List<{MapJsonSchemaType(items)}>";
|
||||
return "List<Object>";
|
||||
}
|
||||
return MapJsonSchemaTypeName(type);
|
||||
}
|
||||
|
||||
private static string MapJsonSchemaTypeName(string type) => type.ToLowerInvariant() switch
|
||||
{
|
||||
"boolean" => "Boolean",
|
||||
"integer" => "Integer",
|
||||
"number" => "Float",
|
||||
"string" => "String",
|
||||
"object" => "Object",
|
||||
"array" => "List",
|
||||
// Legacy aliases (in case a row's been edited by hand pre-migration):
|
||||
"bool" => "Boolean",
|
||||
"int" or "int32" or "int64" => "Integer",
|
||||
"float" or "double" or "decimal" => "Float",
|
||||
_ => type,
|
||||
};
|
||||
|
||||
// ---- Legacy flat-array branch ------------------------------------------
|
||||
|
||||
private static IReadOnlyList<ParameterShape> ParseLegacyParameterArray(JsonElement root)
|
||||
{
|
||||
var result = new List<ParameterShape>();
|
||||
foreach (var el in root.EnumerateArray())
|
||||
{
|
||||
if (el.ValueKind != JsonValueKind.Object) continue;
|
||||
var name = el.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
var rawType = el.TryGetProperty("type", out var t) ? t.GetString() ?? "String" : "String";
|
||||
var required = !el.TryGetProperty("required", out var rq) || rq.ValueKind != JsonValueKind.False;
|
||||
result.Add(new ParameterShape(name, NormalizeLegacyType(rawType), required));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string NormalizeLegacyType(string? raw)
|
||||
{
|
||||
if (string.IsNullOrEmpty(raw)) return "String";
|
||||
return raw.ToLowerInvariant() switch
|
||||
{
|
||||
"boolean" or "bool" => "Boolean",
|
||||
"integer" or "int" or "int32" or "int64" => "Integer",
|
||||
"float" or "double" or "decimal" or "number" => "Float",
|
||||
"string" or "datetime" => "String",
|
||||
"object" => "Object",
|
||||
"list" or "array" => "List",
|
||||
_ => raw,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System.Text;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Per-call console capture for the Test Run sandbox.
|
||||
/// <para>
|
||||
/// Sandbox scripts use <c>System.Console.WriteLine</c> for ad-hoc output. The
|
||||
/// sandbox needs to capture that output per execution. <c>Console.Out</c> is,
|
||||
/// however, <b>process-global</b>: redirecting it with <c>Console.SetOut</c> for
|
||||
/// the duration of one run corrupts any other run executing concurrently —
|
||||
/// outputs interleave, and whichever run finishes first restores
|
||||
/// <c>Console.Out</c> while the others are still writing (CentralUI-003).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This writer is installed into <c>Console.Out</c>/<c>Console.Error</c>
|
||||
/// <b>exactly once</b> (see <see cref="Install"/>) and never removed. Each
|
||||
/// concurrent run pushes its own buffer onto an <see cref="AsyncLocal{T}"/>
|
||||
/// scope via <see cref="BeginCapture"/>; writes on that run's logical call-tree
|
||||
/// land in that run's buffer only. Writes made on threads with no active
|
||||
/// capture scope (i.e. genuine host-process console output) fall through to the
|
||||
/// original writer. No process-global mutation happens per run.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal sealed class SandboxConsoleCapture : TextWriter
|
||||
{
|
||||
private static readonly object InstallLock = new();
|
||||
private static SandboxConsoleCapture? _outInstance;
|
||||
private static SandboxConsoleCapture? _errorInstance;
|
||||
|
||||
private readonly TextWriter _fallback;
|
||||
private readonly AsyncLocal<StringWriter?> _current = new();
|
||||
|
||||
private SandboxConsoleCapture(TextWriter fallback) => _fallback = fallback;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Encoding Encoding => _fallback.Encoding;
|
||||
|
||||
/// <summary>
|
||||
/// Installs the routing writers into <see cref="Console.Out"/> and
|
||||
/// <see cref="Console.Error"/> once for the process. Idempotent and
|
||||
/// thread-safe. Subsequent calls return the already-installed instances.
|
||||
/// </summary>
|
||||
public static (SandboxConsoleCapture Out, SandboxConsoleCapture Error) Install()
|
||||
{
|
||||
if (_outInstance != null && _errorInstance != null)
|
||||
return (_outInstance, _errorInstance);
|
||||
|
||||
lock (InstallLock)
|
||||
{
|
||||
if (_outInstance == null)
|
||||
{
|
||||
_outInstance = new SandboxConsoleCapture(Console.Out);
|
||||
Console.SetOut(_outInstance);
|
||||
}
|
||||
|
||||
if (_errorInstance == null)
|
||||
{
|
||||
_errorInstance = new SandboxConsoleCapture(Console.Error);
|
||||
Console.SetError(_errorInstance);
|
||||
}
|
||||
}
|
||||
|
||||
return (_outInstance, _errorInstance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins a capture scope on the current logical (async) call-tree. All
|
||||
/// console writes from this point until the returned scope is disposed are
|
||||
/// routed into <paramref name="buffer"/> instead of the original writer.
|
||||
/// The scope is restored on dispose, so nesting and concurrent scopes on
|
||||
/// other call-trees are unaffected.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The writer that receives console output for this scope.</param>
|
||||
public CaptureScope BeginCapture(StringWriter buffer)
|
||||
{
|
||||
var previous = _current.Value;
|
||||
_current.Value = buffer;
|
||||
return new CaptureScope(this, previous);
|
||||
}
|
||||
|
||||
// CentralUI-030: intra-script concurrency hardening. A sandboxed script
|
||||
// can fan out work with `Task.WhenAll` / `Task.Run`; `AsyncLocal` flows
|
||||
// the capture `StringWriter` into every child task, so two tasks can
|
||||
// race the *same* buffer. `StringWriter` is not thread-safe — concurrent
|
||||
// `Write`/`WriteLine` calls can corrupt the underlying `StringBuilder`
|
||||
// and either throw or interleave at the character level. We lock on the
|
||||
// captured writer itself so writes from one capture scope serialise;
|
||||
// fall-through to the original `_fallback` (host-process console) is
|
||||
// unlocked because the BCL's process-wide `Console.Out` is already
|
||||
// synchronised by its TextWriter wrapper.
|
||||
/// <inheritdoc />
|
||||
public override void Write(char value) => WriteSynchronized(t => t.Write(value));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(string? value) => WriteSynchronized(t => t.Write(value));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(char[] buffer, int index, int count) =>
|
||||
WriteSynchronized(t => t.Write(buffer, index, count));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteLine() => WriteSynchronized(t => t.WriteLine());
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteLine(string? value) => WriteSynchronized(t => t.WriteLine(value));
|
||||
|
||||
/// <summary>
|
||||
/// Routes a single write through the currently-active capture buffer
|
||||
/// under a lock on that buffer, or to the unwrapped fallback writer when
|
||||
/// no capture scope is active. The lock target is the `StringWriter`
|
||||
/// instance itself — different capture scopes have different writers,
|
||||
/// so two unrelated scopes never block each other.
|
||||
/// </summary>
|
||||
private void WriteSynchronized(Action<TextWriter> write)
|
||||
{
|
||||
var captured = _current.Value;
|
||||
if (captured is null)
|
||||
{
|
||||
write(_fallback);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (captured)
|
||||
{
|
||||
write(captured);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct CaptureScope : IDisposable
|
||||
{
|
||||
private readonly SandboxConsoleCapture _owner;
|
||||
private readonly StringWriter? _previous;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a capture scope that restores the previous writer on dispose.
|
||||
/// </summary>
|
||||
/// <param name="owner">The owning <see cref="SandboxConsoleCapture"/> instance.</param>
|
||||
/// <param name="previous">The writer that was active before this scope was opened.</param>
|
||||
internal CaptureScope(SandboxConsoleCapture owner, StringWriter? previous)
|
||||
{
|
||||
_owner = owner;
|
||||
_previous = previous;
|
||||
}
|
||||
|
||||
/// <summary>Restores the previous capture scope.</summary>
|
||||
public void Dispose() => _owner._current.Value = _previous;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System.Data.Common;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// User-facing surface for <c>ExternalSystem.Call</c> /
|
||||
/// <c>ExternalSystem.CachedCall</c> inside a Test Run. Mirrors
|
||||
/// ExternalSystemHelper in ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptRuntimeContext
|
||||
/// so the same user code compiles against both. When constructed with a null
|
||||
/// client (the editor's metadata-only analysis pass) every call throws
|
||||
/// <see cref="ScriptSandboxException"/>; with a real client wired in (a Test
|
||||
/// Run) calls hit the live HTTP path.
|
||||
/// </summary>
|
||||
public class SandboxExternalHelper
|
||||
{
|
||||
private readonly IExternalSystemClient? _client;
|
||||
private readonly string _instanceName;
|
||||
|
||||
/// <summary>Initializes a new instance of the SandboxExternalHelper class.</summary>
|
||||
/// <param name="client">Optional external system client for test runs.</param>
|
||||
/// <param name="instanceName">The instance name context.</param>
|
||||
public SandboxExternalHelper(IExternalSystemClient? client, string instanceName)
|
||||
{
|
||||
_client = client;
|
||||
_instanceName = instanceName;
|
||||
}
|
||||
|
||||
/// <summary>Invokes a synchronous external system call.</summary>
|
||||
/// <param name="systemName">The external system name.</param>
|
||||
/// <param name="methodName">The method name to invoke.</param>
|
||||
/// <param name="parameters">Optional method parameters.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public Task<ExternalCallResult> Call(
|
||||
string systemName,
|
||||
string methodName,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_client == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"External.Call(\"{systemName}\", \"{methodName}\") — external system client not configured for Test Run.");
|
||||
return _client.CallAsync(systemName, methodName, parameters, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Invokes a cached external system call.</summary>
|
||||
/// <param name="systemName">The external system name.</param>
|
||||
/// <param name="methodName">The method name to invoke.</param>
|
||||
/// <param name="parameters">Optional method parameters.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public Task<ExternalCallResult> CachedCall(
|
||||
string systemName,
|
||||
string methodName,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_client == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"External.CachedCall(\"{systemName}\", \"{methodName}\") — external system client not configured for Test Run.");
|
||||
return _client.CachedCallAsync(systemName, methodName, parameters, _instanceName, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sandbox database helper for script analysis.</summary>
|
||||
public class SandboxDatabaseHelper
|
||||
{
|
||||
private readonly IDatabaseGateway? _gateway;
|
||||
private readonly string _instanceName;
|
||||
|
||||
/// <summary>Initializes a new instance of the SandboxDatabaseHelper class.</summary>
|
||||
/// <param name="gateway">Optional database gateway for test runs.</param>
|
||||
/// <param name="instanceName">The instance name context.</param>
|
||||
public SandboxDatabaseHelper(IDatabaseGateway? gateway, string instanceName)
|
||||
{
|
||||
_gateway = gateway;
|
||||
_instanceName = instanceName;
|
||||
}
|
||||
|
||||
/// <summary>Gets a database connection by name.</summary>
|
||||
/// <param name="name">The database connection name.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public Task<DbConnection> Connection(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_gateway == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"Database.Connection(\"{name}\") — database gateway not configured for Test Run.");
|
||||
return _gateway.GetConnectionAsync(name, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Executes a cached database write operation.</summary>
|
||||
/// <param name="name">The database connection name.</param>
|
||||
/// <param name="sql">The SQL statement to execute.</param>
|
||||
/// <param name="parameters">Optional SQL parameters.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public Task CachedWrite(
|
||||
string name,
|
||||
string sql,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_gateway == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"Database.CachedWrite(\"{name}\") — database gateway not configured for Test Run.");
|
||||
return _gateway.CachedWriteAsync(name, sql, parameters, _instanceName, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.NotifyHelper</c> — the
|
||||
/// <c>Notify</c> global. Signature-faithful to production so the same user code
|
||||
/// (<c>Notify.To(...).Send(...)</c> / <c>Notify.Status(...)</c>) compiles
|
||||
/// identically against both surfaces.
|
||||
///
|
||||
/// In the Notification Outbox design production no longer delivers notification
|
||||
/// email inline — <c>Notify.Send</c> enqueues into the site Store-and-Forward
|
||||
/// Engine and returns a <c>NotificationId</c>. The sandbox has no S&F engine
|
||||
/// and no central, so it is a pure no-op fake: <c>Send</c> returns a generated
|
||||
/// fake id and <c>Status</c> returns a placeholder <see cref="NotificationDeliveryStatus"/>.
|
||||
/// Nothing is delivered.
|
||||
/// </summary>
|
||||
public class SandboxNotifyHelper
|
||||
{
|
||||
/// <summary>Selects the notification list to send to.</summary>
|
||||
/// <param name="listName">The notification list name.</param>
|
||||
public SandboxNotifyTarget To(string listName) =>
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// Queries the delivery status of a previously-sent notification. The
|
||||
/// sandbox never delivers, so this always reports the placeholder
|
||||
/// <c>Unknown</c> status — it exists for signature fidelity with
|
||||
/// <c>NotifyHelper.Status</c>.
|
||||
/// </summary>
|
||||
/// <param name="notificationId">The notification ID to check status for.</param>
|
||||
public Task<NotificationDeliveryStatus> Status(string notificationId) =>
|
||||
Task.FromResult(new NotificationDeliveryStatus("Unknown", 0, null, null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.NotifyTarget</c> — the
|
||||
/// target of <c>Notify.To("listName")</c>.
|
||||
/// </summary>
|
||||
public class SandboxNotifyTarget
|
||||
{
|
||||
/// <summary>Initializes a new instance of the SandboxNotifyTarget class.</summary>
|
||||
internal SandboxNotifyTarget()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors <c>NotifyTarget.Send</c> — returns a <c>NotificationId</c>. In
|
||||
/// the sandbox nothing is enqueued or delivered; a fake id is returned so
|
||||
/// the call type-checks identically to production.
|
||||
/// </summary>
|
||||
/// <param name="subject">The notification subject.</param>
|
||||
/// <param name="message">The notification message.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public Task<string> Send(string subject, string message, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(Guid.NewGuid().ToString("N"));
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime globals for an inbound API method Test Run. Mirrors
|
||||
/// <see cref="InboundScriptHost"/>'s public surface so the same user code that
|
||||
/// compiles for diagnostics also compiles against this type — but every
|
||||
/// <c>Route</c> accessor throws <see cref="ScriptSandboxException"/> instead of
|
||||
/// reaching a deployed site. Cross-site routing needs the cluster transport and
|
||||
/// a live instance, neither of which exists in a central Test Run; pure logic
|
||||
/// and <c>Parameters</c> still work, matching how <see cref="SandboxScriptHost"/>
|
||||
/// throws on <c>Attributes</c> for shared scripts.
|
||||
/// </summary>
|
||||
public class SandboxInboundScriptHost
|
||||
{
|
||||
/// <summary>Gets or initializes the script input parameters.</summary>
|
||||
public ScriptParameters Parameters { get; init; } = new();
|
||||
|
||||
/// <summary>Gets or initializes the cancellation token for the test run.</summary>
|
||||
public CancellationToken CancellationToken { get; init; }
|
||||
|
||||
/// <summary>Gets the route accessor; every call throws <see cref="ScriptSandboxException"/> in a test run.</summary>
|
||||
public RouteAccessor Route { get; } = new();
|
||||
|
||||
/// <summary>Mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.RouteHelper.</summary>
|
||||
public class RouteAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a sandbox route target that throws on every operation.
|
||||
/// </summary>
|
||||
/// <param name="instanceCode">The instance code (used only in the exception message).</param>
|
||||
public RouteTarget To(string instanceCode) => new(instanceCode);
|
||||
}
|
||||
|
||||
/// <summary>Mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.RouteTarget — every call throws.</summary>
|
||||
public class RouteTarget
|
||||
{
|
||||
private readonly string _instanceCode;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the sandbox route target for the given instance code.
|
||||
/// </summary>
|
||||
/// <param name="instanceCode">The instance code referenced by the routing expression (used in the exception message).</param>
|
||||
internal RouteTarget(string instanceCode) => _instanceCode = instanceCode;
|
||||
|
||||
/// <summary>
|
||||
/// Always throws <see cref="ScriptSandboxException"/>; cross-site routing is unavailable in a Test Run.
|
||||
/// </summary>
|
||||
/// <param name="scriptName">Script name (included in the exception message).</param>
|
||||
/// <param name="parameters">Unused parameters.</param>
|
||||
/// <param name="cancellationToken">Unused token.</param>
|
||||
public Task<object?> Call(
|
||||
string scriptName,
|
||||
object? parameters = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw Unavailable($"Call(\"{scriptName}\")");
|
||||
|
||||
/// <summary>
|
||||
/// Always throws <see cref="ScriptSandboxException"/>; cross-site routing is unavailable in a Test Run.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">Attribute name (included in the exception message).</param>
|
||||
/// <param name="cancellationToken">Unused token.</param>
|
||||
public Task<object?> GetAttribute(
|
||||
string attributeName,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw Unavailable($"GetAttribute(\"{attributeName}\")");
|
||||
|
||||
/// <summary>
|
||||
/// Always throws <see cref="ScriptSandboxException"/>; cross-site routing is unavailable in a Test Run.
|
||||
/// </summary>
|
||||
/// <param name="attributeNames">Attribute names (unused).</param>
|
||||
/// <param name="cancellationToken">Unused token.</param>
|
||||
public Task<IReadOnlyDictionary<string, object?>> GetAttributes(
|
||||
IEnumerable<string> attributeNames,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw Unavailable("GetAttributes(...)");
|
||||
|
||||
/// <summary>
|
||||
/// Always throws <see cref="ScriptSandboxException"/>; cross-site routing is unavailable in a Test Run.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">Attribute name (included in the exception message).</param>
|
||||
/// <param name="value">Unused value.</param>
|
||||
/// <param name="cancellationToken">Unused token.</param>
|
||||
public Task SetAttribute(
|
||||
string attributeName,
|
||||
string value,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw Unavailable($"SetAttribute(\"{attributeName}\")");
|
||||
|
||||
/// <summary>
|
||||
/// Always throws <see cref="ScriptSandboxException"/>; cross-site routing is unavailable in a Test Run.
|
||||
/// </summary>
|
||||
/// <param name="attributeValues">Unused attribute values.</param>
|
||||
/// <param name="cancellationToken">Unused token.</param>
|
||||
public Task SetAttributes(
|
||||
IReadOnlyDictionary<string, string> attributeValues,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw Unavailable("SetAttributes(...)");
|
||||
|
||||
private ScriptSandboxException Unavailable(string operation) =>
|
||||
new($"Route.To(\"{_instanceCode}\").{operation} is not available in Test Run — " +
|
||||
"cross-site routing needs a deployed site reachable over the cluster transport.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Backs the Test Run sandbox <c>Instance</c> when the run is bound to a real
|
||||
/// deployed instance. Routes attribute reads/writes and sibling-script calls to
|
||||
/// the instance cross-site via <see cref="CommunicationService"/> — the same
|
||||
/// transport the inbound API's <c>Route.To()</c> uses. All calls run under the
|
||||
/// Test Run's cancellation token, so the sandbox timeout still applies.
|
||||
/// </summary>
|
||||
public sealed class SandboxInstanceGateway : ISandboxInstanceGateway
|
||||
{
|
||||
private readonly CommunicationService _comms;
|
||||
private readonly string _siteId;
|
||||
private readonly string _instanceUniqueName;
|
||||
private readonly CancellationToken _runToken;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="SandboxInstanceGateway"/> bound to a specific deployed instance.
|
||||
/// </summary>
|
||||
/// <param name="comms">Communication service used to route requests to the site.</param>
|
||||
/// <param name="siteId">String identifier of the site hosting the bound instance.</param>
|
||||
/// <param name="instanceUniqueName">Unique name of the instance to route calls to.</param>
|
||||
/// <param name="runToken">Cancellation token for the test run; applied to all cross-site calls.</param>
|
||||
public SandboxInstanceGateway(
|
||||
CommunicationService comms,
|
||||
string siteId,
|
||||
string instanceUniqueName,
|
||||
CancellationToken runToken)
|
||||
{
|
||||
_comms = comms;
|
||||
_siteId = siteId;
|
||||
_instanceUniqueName = instanceUniqueName;
|
||||
_runToken = runToken;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object?> GetAttributeAsync(string canonicalName, CancellationToken ct)
|
||||
{
|
||||
var request = new RouteToGetAttributesRequest(
|
||||
Guid.NewGuid().ToString(), _instanceUniqueName,
|
||||
new[] { canonicalName }, DateTimeOffset.UtcNow);
|
||||
var response = await _comms.RouteToGetAttributesAsync(_siteId, request, _runToken);
|
||||
if (!response.Success)
|
||||
throw new ScriptSandboxException(
|
||||
$"GetAttribute(\"{canonicalName}\") on bound instance '{_instanceUniqueName}' failed: {response.ErrorMessage}");
|
||||
return response.Values.TryGetValue(canonicalName, out var value) ? value : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct)
|
||||
{
|
||||
var request = new RouteToSetAttributesRequest(
|
||||
Guid.NewGuid().ToString(), _instanceUniqueName,
|
||||
new Dictionary<string, string> { [canonicalName] = value }, DateTimeOffset.UtcNow);
|
||||
var response = await _comms.RouteToSetAttributesAsync(_siteId, request, _runToken);
|
||||
if (!response.Success)
|
||||
throw new ScriptSandboxException(
|
||||
$"SetAttribute(\"{canonicalName}\") on bound instance '{_instanceUniqueName}' failed: {response.ErrorMessage}");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object?> CallScriptAsync(
|
||||
string canonicalScriptName, IReadOnlyDictionary<string, object?>? parameters, CancellationToken ct)
|
||||
{
|
||||
var request = new RouteToCallRequest(
|
||||
Guid.NewGuid().ToString(), _instanceUniqueName,
|
||||
canonicalScriptName, parameters, DateTimeOffset.UtcNow);
|
||||
var response = await _comms.RouteToCallAsync(_siteId, request, _runToken);
|
||||
if (!response.Success)
|
||||
throw new ScriptSandboxException(
|
||||
$"CallScript(\"{canonicalScriptName}\") on bound instance '{_instanceUniqueName}' failed: {response.ErrorMessage}");
|
||||
return response.ReturnValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Request from the UI to execute a script in the central sandbox.
|
||||
/// Parameters arrive as JSON values and are converted to .NET primitives
|
||||
/// before being placed in the Parameters dictionary supplied to the script.
|
||||
/// <see cref="Kind"/> selects which globals surface the script is compiled
|
||||
/// and run against — template/shared scripts see <see cref="SandboxScriptHost"/>,
|
||||
/// inbound API method scripts see <see cref="SandboxInboundScriptHost"/>.
|
||||
/// <see cref="BindInstanceUniqueName"/>, when set, binds the run to a deployed
|
||||
/// instance so <c>Instance</c>/<c>Attributes</c> access routes to it cross-site
|
||||
/// instead of throwing. Ignored for inbound API scripts.
|
||||
/// </summary>
|
||||
public record SandboxRunRequest(
|
||||
string Code,
|
||||
Dictionary<string, JsonElement>? Parameters,
|
||||
int? TimeoutSeconds,
|
||||
ScriptKind Kind = ScriptKind.Template,
|
||||
string? BindInstanceUniqueName = null);
|
||||
|
||||
public enum SandboxErrorKind
|
||||
{
|
||||
None,
|
||||
CompileError,
|
||||
SandboxLimitation,
|
||||
RuntimeError,
|
||||
Timeout
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a Test Run. <see cref="Markers"/> carries Roslyn diagnostics
|
||||
/// when <see cref="ErrorKind"/> is CompileError so the UI can display them
|
||||
/// the same way it does for the editor's live problems panel.
|
||||
/// </summary>
|
||||
public record SandboxRunResult(
|
||||
bool Success,
|
||||
string? ReturnValueJson,
|
||||
string? ReturnTypeName,
|
||||
string ConsoleOutput,
|
||||
string? Error,
|
||||
SandboxErrorKind ErrorKind,
|
||||
long DurationMs,
|
||||
IReadOnlyList<DiagnosticMarker>? Markers);
|
||||
@@ -0,0 +1,413 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime globals for the Test Run sandbox. Mirrors the real site-runtime
|
||||
/// <c>ScriptGlobals</c> surface (ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts) member-for-member
|
||||
/// so the same user code that runs at a site also compiles and runs here.
|
||||
///
|
||||
/// Instance-context members — <c>Instance.GetAttribute/SetAttribute/CallScript</c>,
|
||||
/// <c>Attributes</c>, <c>Children</c>, <c>Parent</c> — need a live deployed
|
||||
/// instance. With no instance bound they throw <see cref="ScriptSandboxException"/>;
|
||||
/// with one bound (see <see cref="SandboxInstanceContext"/>) they route to it.
|
||||
///
|
||||
/// <c>ExternalSystem</c>, <c>Database</c>, and <c>Scripts.CallShared</c> run
|
||||
/// against central's real services and fire for real; <c>Notify</c> is a
|
||||
/// signature-faithful no-op fake. None of them depend on a bound instance.
|
||||
/// </summary>
|
||||
public class SandboxScriptHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Script parameters passed to the sandbox.
|
||||
/// </summary>
|
||||
public ScriptParameters Parameters { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token for the sandbox execution.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Alarm context for the sandbox.
|
||||
/// </summary>
|
||||
public AlarmContext? Alarm { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Script scope defining the execution context.
|
||||
/// </summary>
|
||||
public ScriptScope Scope { get; init; } = ScriptScope.Root;
|
||||
|
||||
/// <summary>
|
||||
/// Instance context providing access to deployed instance data.
|
||||
/// </summary>
|
||||
public SandboxInstanceContext Instance { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Helper for external system calls.
|
||||
/// </summary>
|
||||
public SandboxExternalHelper ExternalSystem => Instance.ExternalSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for database operations.
|
||||
/// </summary>
|
||||
public SandboxDatabaseHelper Database => Instance.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for sending notifications.
|
||||
/// </summary>
|
||||
public SandboxNotifyHelper Notify => Instance.Notify;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for calling scripts.
|
||||
/// </summary>
|
||||
public SandboxScriptCallHelper Scripts => Instance.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// Accessor for attributes scoped to the current instance.
|
||||
/// </summary>
|
||||
public SandboxAttributeAccessor Attributes => new(Instance, Scope.SelfPath);
|
||||
|
||||
/// <summary>
|
||||
/// Accessor for child compositions.
|
||||
/// </summary>
|
||||
public SandboxChildrenAccessor Children => new(Instance, Scope.SelfPath);
|
||||
|
||||
/// <summary>
|
||||
/// Accessor for the parent composition, or null if at root.
|
||||
/// </summary>
|
||||
public SandboxCompositionAccessor? Parent =>
|
||||
Scope.ParentPath == null ? null : new SandboxCompositionAccessor(Instance, Scope.ParentPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backs the sandbox <c>Instance</c> when a Test Run is bound to a real
|
||||
/// deployed instance. Null when unbound. The implementation routes to the
|
||||
/// instance cross-site over the cluster transport.
|
||||
/// </summary>
|
||||
public interface ISandboxInstanceGateway
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value of an attribute with the specified canonical name.
|
||||
/// </summary>
|
||||
/// <param name="canonicalName">The canonical name of the attribute.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The attribute value, or null if not found.</returns>
|
||||
Task<object?> GetAttributeAsync(string canonicalName, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of an attribute with the specified canonical name.
|
||||
/// </summary>
|
||||
/// <param name="canonicalName">The canonical name of the attribute.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Calls a script with the specified canonical name.
|
||||
/// </summary>
|
||||
/// <param name="canonicalScriptName">The canonical name of the script.</param>
|
||||
/// <param name="parameters">Script parameters, or null if none.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The script result, or null if none.</returns>
|
||||
Task<object?> CallScriptAsync(
|
||||
string canonicalScriptName, IReadOnlyDictionary<string, object?>? parameters, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptRuntimeContext</c> —
|
||||
/// the <c>Instance</c> global. Attribute and sibling-script access needs a real
|
||||
/// deployed instance: with no gateway wired it throws; with one (a bound
|
||||
/// instance) it routes cross-site. <c>ExternalSystem</c>/<c>Database</c>/
|
||||
/// <c>Scripts</c> run against central's real services regardless of binding;
|
||||
/// <c>Notify</c> is a signature-faithful no-op fake.
|
||||
/// </summary>
|
||||
public class SandboxInstanceContext
|
||||
{
|
||||
private readonly ISandboxInstanceGateway? _gateway;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for external system calls.
|
||||
/// </summary>
|
||||
public SandboxExternalHelper ExternalSystem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Helper for database operations.
|
||||
/// </summary>
|
||||
public SandboxDatabaseHelper Database { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Helper for sending notifications.
|
||||
/// </summary>
|
||||
public SandboxNotifyHelper Notify { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Helper for calling scripts.
|
||||
/// </summary>
|
||||
public SandboxScriptCallHelper Scripts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SandboxInstanceContext.
|
||||
/// </summary>
|
||||
/// <param name="gateway">Gateway for accessing deployed instance data, or null if unbound.</param>
|
||||
/// <param name="external">External system helper, or null to create a default.</param>
|
||||
/// <param name="database">Database helper, or null to create a default.</param>
|
||||
/// <param name="notify">Notification helper, or null to create a default.</param>
|
||||
/// <param name="scripts">Script call helper, or null to create a default.</param>
|
||||
public SandboxInstanceContext(
|
||||
ISandboxInstanceGateway? gateway = null,
|
||||
SandboxExternalHelper? external = null,
|
||||
SandboxDatabaseHelper? database = null,
|
||||
SandboxNotifyHelper? notify = null,
|
||||
SandboxScriptCallHelper? scripts = null)
|
||||
{
|
||||
_gateway = gateway;
|
||||
ExternalSystem = external ?? new SandboxExternalHelper(null, "<sandbox>");
|
||||
Database = database ?? new SandboxDatabaseHelper(null, "<sandbox>");
|
||||
Notify = notify ?? new SandboxNotifyHelper();
|
||||
Scripts = scripts ?? new SandboxScriptCallHelper(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of an attribute.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">The name of the attribute.</param>
|
||||
/// <returns>The attribute value, or null if not found.</returns>
|
||||
public Task<object?> GetAttribute(string attributeName)
|
||||
{
|
||||
if (_gateway == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"GetAttribute(\"{attributeName}\") needs a deployed instance — " +
|
||||
"bind one in Test Run to read live attribute values.");
|
||||
return _gateway.GetAttributeAsync(attributeName, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of an attribute.
|
||||
/// </summary>
|
||||
/// <param name="attributeName">The name of the attribute.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
public void SetAttribute(string attributeName, string value)
|
||||
{
|
||||
if (_gateway == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"SetAttribute(\"{attributeName}\") needs a deployed instance — " +
|
||||
"bind one in Test Run to write attribute values.");
|
||||
_gateway.SetAttributeAsync(attributeName, value, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls a sibling script.
|
||||
/// </summary>
|
||||
/// <param name="scriptName">The name of the script.</param>
|
||||
/// <param name="parameters">Script parameters, or null if none.</param>
|
||||
/// <returns>The script result, or null if none.</returns>
|
||||
public Task<object?> CallScript(string scriptName, object? parameters = null)
|
||||
{
|
||||
if (_gateway == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"CallScript(\"{scriptName}\") needs a deployed instance — " +
|
||||
"bind one in Test Run to call sibling scripts.");
|
||||
return _gateway.CallScriptAsync(scriptName, ScriptArgs.Normalize(parameters), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ScriptRuntimeContext.ScriptCallHelper</c> —
|
||||
/// <c>Scripts.CallShared(...)</c>. Compiles and runs the named shared script in
|
||||
/// the same sandbox via the wired delegate.
|
||||
/// </summary>
|
||||
public class SandboxScriptCallHelper
|
||||
{
|
||||
private readonly Func<string, IReadOnlyDictionary<string, object?>?, CancellationToken, Task<object?>>? _callShared;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SandboxScriptCallHelper.
|
||||
/// </summary>
|
||||
/// <param name="callShared">Delegate for calling shared scripts, or null if not available.</param>
|
||||
public SandboxScriptCallHelper(
|
||||
Func<string, IReadOnlyDictionary<string, object?>?, CancellationToken, Task<object?>>? callShared)
|
||||
{
|
||||
_callShared = callShared;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls a shared script.
|
||||
/// </summary>
|
||||
/// <param name="scriptName">The name of the shared script.</param>
|
||||
/// <param name="parameters">Script parameters, or null if none.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The script result, or null if none.</returns>
|
||||
public Task<object?> CallShared(
|
||||
string scriptName,
|
||||
object? parameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_callShared == null)
|
||||
throw new ScriptSandboxException(
|
||||
$"Scripts.CallShared(\"{scriptName}\") — shared-script catalog not configured for Test Run.");
|
||||
return _callShared(scriptName, ScriptArgs.Normalize(parameters), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.AttributeAccessor</c> —
|
||||
/// scope-aware <c>Attributes["X"]</c> access anchored at a canonical-name prefix.
|
||||
/// </summary>
|
||||
public class SandboxAttributeAccessor
|
||||
{
|
||||
private readonly SandboxInstanceContext _ctx;
|
||||
|
||||
/// <summary>
|
||||
/// The scope prefix for attribute resolution.
|
||||
/// </summary>
|
||||
public string ScopePrefix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SandboxAttributeAccessor.
|
||||
/// </summary>
|
||||
/// <param name="ctx">The sandbox instance context.</param>
|
||||
/// <param name="prefix">The scope prefix for attribute names.</param>
|
||||
public SandboxAttributeAccessor(SandboxInstanceContext ctx, string prefix)
|
||||
{
|
||||
_ctx = ctx;
|
||||
ScopePrefix = prefix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a key to its fully qualified name within the current scope.
|
||||
/// </summary>
|
||||
/// <param name="key">The attribute key.</param>
|
||||
/// <returns>The fully qualified attribute name.</returns>
|
||||
public string Resolve(string key) =>
|
||||
ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an attribute value by key.
|
||||
/// </summary>
|
||||
/// <param name="key">The attribute key.</param>
|
||||
/// <returns>The attribute value, or null if not found.</returns>
|
||||
public object? this[string key]
|
||||
{
|
||||
get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult();
|
||||
set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an attribute value asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="key">The attribute key.</param>
|
||||
/// <returns>The attribute value, or null if not found.</returns>
|
||||
public Task<object?> GetAsync(string key) => _ctx.GetAttribute(Resolve(key));
|
||||
|
||||
/// <summary>
|
||||
/// Sets an attribute value asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="key">The attribute key.</param>
|
||||
/// <param name="value">The value to set, or null.</param>
|
||||
/// <returns>A task representing the operation.</returns>
|
||||
public Task SetAsync(string key, object? value)
|
||||
{
|
||||
_ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.CompositionAccessor</c> —
|
||||
/// a view of one composition: its attributes plus an invokable <c>CallScript</c>.
|
||||
/// </summary>
|
||||
public class SandboxCompositionAccessor
|
||||
{
|
||||
private readonly SandboxInstanceContext _ctx;
|
||||
|
||||
/// <summary>
|
||||
/// The path to the composition within the instance hierarchy.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Accessor for attributes within the composition.
|
||||
/// </summary>
|
||||
public SandboxAttributeAccessor Attributes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SandboxCompositionAccessor.
|
||||
/// </summary>
|
||||
/// <param name="ctx">The sandbox instance context.</param>
|
||||
/// <param name="path">The path to the composition within the instance hierarchy.</param>
|
||||
public SandboxCompositionAccessor(SandboxInstanceContext ctx, string path)
|
||||
{
|
||||
_ctx = ctx;
|
||||
Path = path;
|
||||
Attributes = new SandboxAttributeAccessor(ctx, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a script name to its fully qualified name within the composition.
|
||||
/// </summary>
|
||||
/// <param name="scriptName">The script name.</param>
|
||||
/// <returns>The fully qualified script name.</returns>
|
||||
public string ResolveScript(string scriptName) =>
|
||||
Path.Length == 0 ? scriptName : Path + "." + scriptName;
|
||||
|
||||
/// <summary>
|
||||
/// Calls a script within the composition.
|
||||
/// </summary>
|
||||
/// <param name="scriptName">The name of the script.</param>
|
||||
/// <param name="parameters">Script parameters, or null if none.</param>
|
||||
/// <returns>The script result, or null if none.</returns>
|
||||
public Task<object?> CallScript(string scriptName, object? parameters = null)
|
||||
=> _ctx.CallScript(ResolveScript(scriptName), parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ChildrenAccessor</c> —
|
||||
/// dictionary-style access to child compositions.
|
||||
/// </summary>
|
||||
public class SandboxChildrenAccessor
|
||||
{
|
||||
private readonly SandboxInstanceContext _ctx;
|
||||
private readonly string _selfPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SandboxChildrenAccessor.
|
||||
/// </summary>
|
||||
/// <param name="ctx">The sandbox instance context.</param>
|
||||
/// <param name="selfPath">The path to the parent composition.</param>
|
||||
public SandboxChildrenAccessor(SandboxInstanceContext ctx, string selfPath)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_selfPath = selfPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a child composition by name.
|
||||
/// </summary>
|
||||
/// <param name="compositionName">The name of the child composition.</param>
|
||||
/// <returns>An accessor for the child composition.</returns>
|
||||
public SandboxCompositionAccessor this[string compositionName]
|
||||
{
|
||||
get
|
||||
{
|
||||
var path = _selfPath.Length == 0
|
||||
? compositionName
|
||||
: _selfPath + "." + compositionName;
|
||||
return new SandboxCompositionAccessor(_ctx, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Distinct exception so the Test Run pipeline can label sandbox-only
|
||||
/// limitations differently from genuine runtime errors in user code.
|
||||
/// </summary>
|
||||
public class ScriptSandboxException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ScriptSandboxException.
|
||||
/// </summary>
|
||||
/// <param name="message">The exception message.</param>
|
||||
public ScriptSandboxException(string message) : base(message) { }
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Which runtime globals surface a script is analyzed against. Template and
|
||||
/// shared scripts see <see cref="SandboxScriptHost"/> (mirroring the site
|
||||
/// runtime's ScriptGlobals); inbound API method scripts see
|
||||
/// <see cref="InboundScriptHost"/> (with <c>Route</c> and <c>Parameters</c>).
|
||||
/// </summary>
|
||||
public enum ScriptKind
|
||||
{
|
||||
Template,
|
||||
InboundApi
|
||||
}
|
||||
|
||||
public record DiagnoseRequest(
|
||||
string Code,
|
||||
IReadOnlyList<string>? DeclaredParameters = null,
|
||||
IReadOnlyList<ScriptShape>? SiblingScripts = null,
|
||||
IReadOnlyList<AttributeShape>? SelfAttributes = null,
|
||||
IReadOnlyList<CompositionContext>? Children = null,
|
||||
CompositionContext? Parent = null,
|
||||
ScriptKind Kind = ScriptKind.Template);
|
||||
|
||||
public record DiagnoseResponse(IReadOnlyList<DiagnosticMarker> Markers);
|
||||
|
||||
/// <summary>
|
||||
/// Shape Monaco's setModelMarkers expects (with severity mapped to Monaco's
|
||||
/// MarkerSeverity enum: 1=Hint, 2=Info, 4=Warning, 8=Error).
|
||||
/// </summary>
|
||||
public record DiagnosticMarker(
|
||||
int Severity,
|
||||
int StartLineNumber,
|
||||
int StartColumn,
|
||||
int EndLineNumber,
|
||||
int EndColumn,
|
||||
string Message,
|
||||
string Code);
|
||||
|
||||
public record CompletionsRequest(
|
||||
string CodeText,
|
||||
int Line,
|
||||
int Column,
|
||||
IReadOnlyList<string>? DeclaredParameters = null,
|
||||
IReadOnlyList<ScriptShape>? SiblingScripts = null,
|
||||
IReadOnlyList<AttributeShape>? SelfAttributes = null,
|
||||
IReadOnlyList<CompositionContext>? Children = null,
|
||||
CompositionContext? Parent = null,
|
||||
ScriptKind Kind = ScriptKind.Template);
|
||||
|
||||
public record CompletionsResponse(IReadOnlyList<CompletionItem> Items);
|
||||
|
||||
public record CompletionItem(
|
||||
string Label,
|
||||
string InsertText,
|
||||
string Detail,
|
||||
string Kind,
|
||||
/// <summary>Monaco CompletionItemInsertTextRule. 4 = InsertAsSnippet.</summary>
|
||||
int InsertTextRules = 0);
|
||||
|
||||
public record HoverRequest(
|
||||
string CodeText,
|
||||
int Line,
|
||||
int Column,
|
||||
IReadOnlyList<ScriptShape>? SiblingScripts = null,
|
||||
IReadOnlyList<ParameterShape>? DeclaredParameters = null,
|
||||
IReadOnlyList<AttributeShape>? SelfAttributes = null,
|
||||
IReadOnlyList<CompositionContext>? Children = null,
|
||||
CompositionContext? Parent = null);
|
||||
|
||||
public record HoverResponse(string? Markdown);
|
||||
|
||||
public record SignatureHelpRequest(
|
||||
string CodeText,
|
||||
int Line,
|
||||
int Column,
|
||||
IReadOnlyList<ScriptShape>? SiblingScripts = null,
|
||||
IReadOnlyList<CompositionContext>? Children = null,
|
||||
CompositionContext? Parent = null);
|
||||
|
||||
public record SignatureHelpResponse(
|
||||
string? Label,
|
||||
IReadOnlyList<SignatureHelpParameter>? Parameters,
|
||||
int ActiveParameter);
|
||||
|
||||
public record SignatureHelpParameter(string Label, string? Documentation);
|
||||
|
||||
/// <summary>
|
||||
/// Shape metadata for a script. Captured from the form's ParameterListEditor
|
||||
/// and ReturnTypeEditor (for siblings) or from SharedScriptCatalog (for shared
|
||||
/// scripts). Used by hover, signature-help, snippet expansion, and the
|
||||
/// argument-count diagnostic.
|
||||
/// </summary>
|
||||
public record ScriptShape(
|
||||
string Name,
|
||||
IReadOnlyList<ParameterShape> Parameters,
|
||||
string? ReturnType);
|
||||
|
||||
public record ParameterShape(string Name, string Type, bool Required);
|
||||
|
||||
/// <summary>
|
||||
/// Attribute declared on a template: name + canonical SCADA type (Boolean,
|
||||
/// Integer, Float, String, Object, List).
|
||||
/// </summary>
|
||||
public record AttributeShape(string Name, string Type);
|
||||
|
||||
/// <summary>
|
||||
/// One end of a composition relationship — either a child (referenced by
|
||||
/// composition instance name) or the parent (referenced by template name).
|
||||
/// The shape carries the attributes and scripts at that scope so the editor
|
||||
/// can complete <c>Children["X"].Attributes["Y"]</c> and
|
||||
/// <c>Children["X"].CallScript("Z")</c> with the right metadata.
|
||||
/// </summary>
|
||||
public record CompositionContext(
|
||||
string Name,
|
||||
IReadOnlyList<AttributeShape> Attributes,
|
||||
IReadOnlyList<ScriptShape> Scripts);
|
||||
|
||||
public record FormatRequest(string Code);
|
||||
public record FormatResponse(string Code);
|
||||
|
||||
public record InlayHintsRequest(
|
||||
string Code,
|
||||
IReadOnlyList<ScriptShape>? SiblingScripts = null);
|
||||
|
||||
public record InlayHintsResponse(IReadOnlyList<InlayHint> Hints);
|
||||
|
||||
public record InlayHint(int Line, int Column, string Label);
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal-API endpoint group for Roslyn-backed script analysis (diagnostics, completions, hover, etc.).
|
||||
/// </summary>
|
||||
public static class ScriptAnalysisEndpoints
|
||||
{
|
||||
/// <summary>Registers all script analysis endpoints under <c>/api/script-analysis</c>.</summary>
|
||||
/// <param name="endpoints">The endpoint route builder to register against.</param>
|
||||
/// <returns>The same <paramref name="endpoints"/> instance for chaining.</returns>
|
||||
public static IEndpointRouteBuilder MapScriptAnalysisEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/script-analysis")
|
||||
.RequireAuthorization(AuthorizationPolicies.RequireDesign);
|
||||
|
||||
group.MapPost("/diagnostics", (DiagnoseRequest req, ScriptAnalysisService svc) =>
|
||||
Results.Ok(svc.Diagnose(req)));
|
||||
|
||||
group.MapPost("/completions", async (CompletionsRequest req, ScriptAnalysisService svc) =>
|
||||
Results.Ok(await svc.CompleteAsync(req)));
|
||||
|
||||
group.MapPost("/hover", async (HoverRequest req, ScriptAnalysisService svc) =>
|
||||
Results.Ok(await svc.Hover(req)));
|
||||
|
||||
group.MapPost("/signature-help", async (SignatureHelpRequest req, ScriptAnalysisService svc) =>
|
||||
Results.Ok(await svc.SignatureHelp(req)));
|
||||
|
||||
group.MapPost("/format", (FormatRequest req, ScriptAnalysisService svc) =>
|
||||
Results.Ok(svc.Format(req)));
|
||||
|
||||
group.MapPost("/inlay-hints", (InlayHintsRequest req, ScriptAnalysisService svc) =>
|
||||
Results.Ok(svc.InlayHints(req)));
|
||||
|
||||
group.MapPost("/run", async (SandboxRunRequest req, ScriptAnalysisService svc, HttpContext http) =>
|
||||
Results.Ok(await svc.RunInSandboxAsync(req, http.RequestAborted)));
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Parses the parameter-definitions and return-definition JSON Schema written
|
||||
/// by SchemaBuilder into a <see cref="ScriptShape"/>. Delegates to
|
||||
/// <see cref="JsonSchemaShapeParser"/>, which also handles legacy flat-shape
|
||||
/// rows during the transition window.
|
||||
/// </summary>
|
||||
public static class ScriptShapeParser
|
||||
{
|
||||
/// <summary>Parses the parameter and return-type JSON schemas for a script and returns a <see cref="ScriptShape"/> describing its signature.</summary>
|
||||
/// <param name="name">The canonical script name.</param>
|
||||
/// <param name="parametersJson">The JSON Schema or legacy flat-array parameters definition, or <c>null</c> for parameterless scripts.</param>
|
||||
/// <param name="returnJson">The JSON Schema or legacy return-type definition, or <c>null</c> for void scripts.</param>
|
||||
public static ScriptShape Parse(string name, string? parametersJson, string? returnJson)
|
||||
{
|
||||
var parameters = JsonSchemaShapeParser.ParseParameters(parametersJson);
|
||||
var returnType = JsonSchemaShapeParser.ParseReturnType(returnJson);
|
||||
return new ScriptShape(name, parameters, returnType);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user