efba01d10a
Phases 1+2 of the design at
docs/plans/2026-05-12-script-scope-access-design.md.
Adds ergonomic scope-aware accessors to compiled scripts. A script
on a composed TempSensor reads its own attribute via
Attributes["Temperature"]; reaches up to the parent via
Parent.Attributes["SpeedRPM"]; invokes a child script via
Children["TempSensor"].CallScript("Sample"). All resolve to the
existing flat Instance.GetAttribute / SetAttribute / CallScript
delegates by prepending the script's canonical path prefix.
Runtime types (SiteRuntime.Scripts.ScopeAccessors):
AttributeAccessor sync indexer + GetAsync / SetAsync
CompositionAccessor Attributes + CallScript
ChildrenAccessor Children["name"] => CompositionAccessor
ScriptGlobals gains Scope, Attributes, Children, Parent properties.
Sync indexer blocks on the Instance Actor Ask; explicit GetAsync /
SetAsync are also available for callers that want to await.
Plumbing:
- Commons.Types.Scripts.ScriptScope record (SelfPath / ParentPath).
- ResolvedScript.Scope (defaults to ScriptScope.Root for back-compat).
- FlatteningService emits new ScriptScope(prefix, "") for each
composed script so a script defined on TempSensor composed under
a parent gets SelfPath = "TempSensor".
- ScriptActor reads the Scope from its ResolvedScript and forwards
it through ScriptExecutionActor into ScriptGlobals on each call.
RevisionHashService not touched: the per-script canonical name
already encodes the composition path, so any structural change
already flips the hash.
10 new unit tests on the path arithmetic. Site/Template engine
suites stay green (129 + 199).
Editor surface (Phase 3: metadata fetch, Phase 4: completion +
SCADA006 / SCADA007 diagnostics) follows in the next commits.
106 lines
3.4 KiB
C#
106 lines
3.4 KiB
C#
namespace ScadaLink.SiteRuntime.Scripts;
|
|
|
|
/// <summary>
|
|
/// Scope-aware view onto the instance's attributes, anchored at a path prefix.
|
|
/// <c>Attributes["X"]</c> on the root scope resolves to canonical name "X";
|
|
/// on a composition with prefix "TempSensor" it resolves to "TempSensor.X".
|
|
/// Reads block on the actor Ask; async variants are provided for callers
|
|
/// that prefer to await explicitly.
|
|
/// </summary>
|
|
public class AttributeAccessor
|
|
{
|
|
private readonly ScriptRuntimeContext _ctx;
|
|
|
|
/// <summary>Canonical-name prefix, e.g. "" for root or "TempSensor" for a composition.</summary>
|
|
public string ScopePrefix { get; }
|
|
|
|
public AttributeAccessor(ScriptRuntimeContext ctx, string prefix)
|
|
{
|
|
_ctx = ctx;
|
|
ScopePrefix = prefix;
|
|
}
|
|
|
|
public string Resolve(string key) =>
|
|
ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key;
|
|
|
|
public object? this[string key]
|
|
{
|
|
get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult();
|
|
set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
|
|
}
|
|
|
|
public Task<object?> GetAsync(string key) => _ctx.GetAttribute(Resolve(key));
|
|
|
|
public Task SetAsync(string key, object? value)
|
|
{
|
|
_ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A view of one composition at a path. Exposes its attributes via
|
|
/// <see cref="AttributeAccessor"/> and an invokable <c>CallScript</c>.
|
|
/// </summary>
|
|
public class CompositionAccessor
|
|
{
|
|
private readonly ScriptRuntimeContext _ctx;
|
|
|
|
/// <summary>Canonical-name path this composition is rooted at.</summary>
|
|
public string Path { get; }
|
|
|
|
public AttributeAccessor Attributes { get; }
|
|
|
|
public CompositionAccessor(ScriptRuntimeContext ctx, string path)
|
|
{
|
|
_ctx = ctx;
|
|
Path = path;
|
|
Attributes = new AttributeAccessor(ctx, path);
|
|
}
|
|
|
|
public string ResolveScript(string scriptName) =>
|
|
Path.Length == 0 ? scriptName : Path + "." + scriptName;
|
|
|
|
public Task<object?> CallScript(string scriptName, IReadOnlyDictionary<string, object?>? parameters = null)
|
|
=> _ctx.CallScript(ResolveScript(scriptName), parameters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dictionary-style accessor for the script's child compositions. Indexing
|
|
/// returns a <see cref="CompositionAccessor"/> rooted at the child's path.
|
|
/// </summary>
|
|
public class ChildrenAccessor
|
|
{
|
|
private readonly ScriptRuntimeContext _ctx;
|
|
private readonly string _selfPath;
|
|
|
|
public ChildrenAccessor(ScriptRuntimeContext ctx, string selfPath)
|
|
{
|
|
_ctx = ctx;
|
|
_selfPath = selfPath;
|
|
}
|
|
|
|
public CompositionAccessor this[string compositionName]
|
|
{
|
|
get
|
|
{
|
|
var path = _selfPath.Length == 0
|
|
? compositionName
|
|
: _selfPath + "." + compositionName;
|
|
return new CompositionAccessor(_ctx, path);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class ScopeAccessorFactory
|
|
{
|
|
public static AttributeAccessor AttributesFor(ScriptRuntimeContext ctx, string selfPath)
|
|
=> new(ctx, selfPath);
|
|
|
|
public static ChildrenAccessor ChildrenFor(ScriptRuntimeContext ctx, string selfPath)
|
|
=> new(ctx, selfPath);
|
|
|
|
public static CompositionAccessor? ParentFor(ScriptRuntimeContext ctx, string? parentPath)
|
|
=> parentPath == null ? null : new CompositionAccessor(ctx, parentPath);
|
|
}
|