feat(scripts): add typed Parameters.Get<T>() helpers for script API
Replace raw dictionary casting with ScriptParameters wrapper that provides Get<T>, Get<T?>, Get<T[]>, and Get<List<T>> with clear error messages, numeric conversion, and JsonElement support for Inbound API parameters.
This commit is contained in:
@@ -156,6 +156,10 @@ Inbound API scripts **cannot** call shared scripts directly — shared scripts a
|
||||
- **Input parameters** are available as defined in the method definition.
|
||||
- **Return value** construction matching the defined return structure.
|
||||
|
||||
#### Parameter Access
|
||||
- `Parameters["key"]` — Raw dictionary access.
|
||||
- `Parameters.Get<T>("key")` — Typed access (same API as site runtime scripts). See Site Runtime component for full type support.
|
||||
|
||||
#### Database Access
|
||||
- `Database.Connection("connectionName")` — Obtain a raw MS SQL client connection for querying the configuration or machine data databases directly from central.
|
||||
|
||||
|
||||
@@ -258,6 +258,14 @@ Available to all Script Execution Actors and Alarm Execution Actors:
|
||||
- `Database.Connection("connectionName")` — Obtain a raw MS SQL client connection (ADO.NET) for synchronous read/write.
|
||||
- `Database.CachedWrite("connectionName", "sql", parameters)` — Submit a write operation for store-and-forward delivery.
|
||||
|
||||
### Parameter Access
|
||||
- `Parameters["key"]` — Raw dictionary access (returns `object?`, requires manual casting).
|
||||
- `Parameters.Get<T>("key")` — Typed access with descriptive error messages. Throws `ScriptParameterException` if parameter is missing, null, or cannot be converted to `T`.
|
||||
- `Parameters.Get<T?>("key")` — Nullable typed access. Returns `null` if parameter is missing, null, or cannot be converted.
|
||||
- `Parameters.Get<T[]>("key")` — Array access. Converts a list parameter to a typed array. Throws on first unconvertible element.
|
||||
- `Parameters.Get<List<T>>("key")` — List access. Converts a list parameter to a typed `List<T>`.
|
||||
- Supported types: `bool`, `int`, `long`, `float`, `double`, `string`, `DateTime`.
|
||||
|
||||
### Recursion Limit
|
||||
- Every script call (`Instance.CallScript` and `Scripts.CallShared`) increments a call depth counter.
|
||||
- If the counter exceeds the maximum recursion depth (default: 10), the call fails with an error.
|
||||
|
||||
242
src/ScadaLink.Commons/Types/ScriptParameters.cs
Normal file
242
src/ScadaLink.Commons/Types/ScriptParameters.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ScadaLink.Commons.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Typed wrapper around script parameters. Implements IReadOnlyDictionary for backward
|
||||
/// compatibility (Parameters["key"]) and adds Get<T>() for typed access with
|
||||
/// clear error messages.
|
||||
/// </summary>
|
||||
public class ScriptParameters : IReadOnlyDictionary<string, object?>
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, object?> _inner;
|
||||
|
||||
public ScriptParameters(IReadOnlyDictionary<string, object?> parameters)
|
||||
{
|
||||
_inner = parameters ?? throw new ArgumentNullException(nameof(parameters));
|
||||
}
|
||||
|
||||
public ScriptParameters() : this(new Dictionary<string, object?>()) { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a parameter value with typed conversion.
|
||||
/// <list type="bullet">
|
||||
/// <item><c>Get<int>("key")</c> — throws if missing, null, or unconvertible.</item>
|
||||
/// <item><c>Get<int?>("key")</c> — returns null if missing, null, or unconvertible.</item>
|
||||
/// <item><c>Get<int[]>("key")</c> — converts list to typed array; throws on first bad element.</item>
|
||||
/// <item><c>Get<List<int>>("key")</c> — converts list to typed List; throws on first bad element.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public T Get<T>(string key)
|
||||
{
|
||||
var targetType = typeof(T);
|
||||
|
||||
// Array types: int[], string[], etc.
|
||||
if (targetType.IsArray && targetType != typeof(string))
|
||||
{
|
||||
var elementType = targetType.GetElementType()!;
|
||||
return (T)(object)ConvertToArray(key, elementType);
|
||||
}
|
||||
|
||||
// List<T> types
|
||||
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
var elementType = targetType.GetGenericArguments()[0];
|
||||
return (T)ConvertToList(key, elementType);
|
||||
}
|
||||
|
||||
// Nullable<T> types: int?, bool?, etc.
|
||||
var underlyingType = Nullable.GetUnderlyingType(targetType);
|
||||
if (underlyingType != null)
|
||||
{
|
||||
return GetNullable<T>(key, underlyingType);
|
||||
}
|
||||
|
||||
// Non-nullable scalar types
|
||||
return GetRequired<T>(key, targetType);
|
||||
}
|
||||
|
||||
private T GetRequired<T>(string key, Type targetType)
|
||||
{
|
||||
if (!_inner.TryGetValue(key, out var value))
|
||||
throw new ScriptParameterException($"Parameter '{key}' not found");
|
||||
|
||||
if (value is null)
|
||||
throw new ScriptParameterException($"Parameter '{key}' value is null");
|
||||
|
||||
return (T)ConvertScalar(value, targetType, key);
|
||||
}
|
||||
|
||||
private T GetNullable<T>(string key, Type underlyingType)
|
||||
{
|
||||
if (!_inner.TryGetValue(key, out var value) || value is null)
|
||||
return default!; // null for Nullable<T>
|
||||
|
||||
try
|
||||
{
|
||||
var converted = ConvertScalar(value, underlyingType, key);
|
||||
return (T)converted;
|
||||
}
|
||||
catch (ScriptParameterException)
|
||||
{
|
||||
return default!; // null on conversion failure for nullable
|
||||
}
|
||||
}
|
||||
|
||||
private Array ConvertToArray(string key, Type elementType)
|
||||
{
|
||||
var list = GetSourceList(key);
|
||||
var array = Array.CreateInstance(elementType, list.Count);
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
var item = list[i];
|
||||
if (item is null)
|
||||
throw new ScriptParameterException(
|
||||
$"Parameter '{key}' element at index {i} is null");
|
||||
|
||||
try
|
||||
{
|
||||
array.SetValue(ConvertScalar(item, elementType, key), i);
|
||||
}
|
||||
catch (ScriptParameterException)
|
||||
{
|
||||
throw new ScriptParameterException(
|
||||
$"Parameter '{key}' element at index {i} with value '{item}' could not be parsed as {elementType.Name}");
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private object ConvertToList(string key, Type elementType)
|
||||
{
|
||||
var list = GetSourceList(key);
|
||||
var listType = typeof(List<>).MakeGenericType(elementType);
|
||||
var result = (IList)Activator.CreateInstance(listType, list.Count)!;
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
var item = list[i];
|
||||
if (item is null)
|
||||
throw new ScriptParameterException(
|
||||
$"Parameter '{key}' element at index {i} is null");
|
||||
|
||||
try
|
||||
{
|
||||
result.Add(ConvertScalar(item, elementType, key));
|
||||
}
|
||||
catch (ScriptParameterException)
|
||||
{
|
||||
throw new ScriptParameterException(
|
||||
$"Parameter '{key}' element at index {i} with value '{item}' could not be parsed as {elementType.Name}");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private IList GetSourceList(string key)
|
||||
{
|
||||
if (!_inner.TryGetValue(key, out var value))
|
||||
throw new ScriptParameterException($"Parameter '{key}' not found");
|
||||
|
||||
if (value is null)
|
||||
throw new ScriptParameterException($"Parameter '{key}' value is null");
|
||||
|
||||
if (value is IList list)
|
||||
return list;
|
||||
|
||||
throw new ScriptParameterException($"Parameter '{key}' is not a list or array");
|
||||
}
|
||||
|
||||
private static object ConvertScalar(object value, Type targetType, string key)
|
||||
{
|
||||
// Direct type match
|
||||
if (targetType.IsInstanceOfType(value))
|
||||
return value;
|
||||
|
||||
// Unwrap JsonElement (from JSON deserialization of List<object?> / Dictionary<string, object?>)
|
||||
if (value is JsonElement je)
|
||||
return ConvertJsonElement(je, targetType, key);
|
||||
|
||||
// string target — use ToString
|
||||
if (targetType == typeof(string))
|
||||
return value.ToString() ?? string.Empty;
|
||||
|
||||
// DateTime from string
|
||||
if (targetType == typeof(DateTime) && value is string dateStr)
|
||||
{
|
||||
if (DateTime.TryParse(dateStr, CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.RoundtripKind, out var dt))
|
||||
return dt;
|
||||
|
||||
throw new ScriptParameterException(
|
||||
$"Parameter '{key}' with value '{value}' could not be parsed as DateTime");
|
||||
}
|
||||
|
||||
// Numeric and other IConvertible conversions
|
||||
try
|
||||
{
|
||||
return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidCastException or FormatException or OverflowException)
|
||||
{
|
||||
throw new ScriptParameterException(
|
||||
$"Parameter '{key}' with value '{value}' could not be parsed as {targetType.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object ConvertJsonElement(JsonElement element, Type targetType, string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (targetType == typeof(bool) && element.ValueKind is JsonValueKind.True or JsonValueKind.False)
|
||||
return element.GetBoolean();
|
||||
if (targetType == typeof(int) && element.ValueKind == JsonValueKind.Number)
|
||||
return element.GetInt32();
|
||||
if (targetType == typeof(long) && element.ValueKind == JsonValueKind.Number)
|
||||
return element.GetInt64();
|
||||
if (targetType == typeof(float) && element.ValueKind == JsonValueKind.Number)
|
||||
return element.GetSingle();
|
||||
if (targetType == typeof(double) && element.ValueKind == JsonValueKind.Number)
|
||||
return element.GetDouble();
|
||||
if (targetType == typeof(string) && element.ValueKind == JsonValueKind.String)
|
||||
return element.GetString()!;
|
||||
if (targetType == typeof(string))
|
||||
return element.ToString();
|
||||
if (targetType == typeof(DateTime) && element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
if (DateTime.TryParse(element.GetString(), CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.RoundtripKind, out var dt))
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is FormatException or OverflowException or InvalidOperationException)
|
||||
{
|
||||
// Fall through to error
|
||||
}
|
||||
|
||||
throw new ScriptParameterException(
|
||||
$"Parameter '{key}' with value '{element}' could not be parsed as {targetType.Name}");
|
||||
}
|
||||
|
||||
// IReadOnlyDictionary<string, object?> implementation
|
||||
public object? this[string key] => _inner[key];
|
||||
public IEnumerable<string> Keys => _inner.Keys;
|
||||
public IEnumerable<object?> Values => _inner.Values;
|
||||
public int Count => _inner.Count;
|
||||
public bool ContainsKey(string key) => _inner.ContainsKey(key);
|
||||
public bool TryGetValue(string key, out object? value) => _inner.TryGetValue(key, out value);
|
||||
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() => _inner.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown by <see cref="ScriptParameters.Get{T}"/> when a parameter
|
||||
/// cannot be retrieved or converted to the requested type.
|
||||
/// </summary>
|
||||
public class ScriptParameterException : Exception
|
||||
{
|
||||
public ScriptParameterException(string message) : base(message) { }
|
||||
public ScriptParameterException(string message, Exception innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Entities.InboundApi;
|
||||
using ScadaLink.Commons.Types;
|
||||
|
||||
namespace ScadaLink.InboundAPI;
|
||||
|
||||
@@ -58,6 +59,7 @@ public class InboundScriptExecutor
|
||||
typeof(Enumerable).Assembly,
|
||||
typeof(Dictionary<,>).Assembly,
|
||||
typeof(RouteHelper).Assembly,
|
||||
typeof(ScriptParameters).Assembly,
|
||||
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly)
|
||||
.WithImports(
|
||||
"System",
|
||||
@@ -152,7 +154,7 @@ public class InboundScriptExecutor
|
||||
/// </summary>
|
||||
public class InboundScriptContext
|
||||
{
|
||||
public IReadOnlyDictionary<string, object?> Parameters { get; }
|
||||
public ScriptParameters Parameters { get; }
|
||||
public RouteHelper Route { get; }
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
@@ -161,7 +163,7 @@ public class InboundScriptContext
|
||||
RouteHelper route,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Parameters = parameters;
|
||||
Parameters = new ScriptParameters(parameters);
|
||||
Route = route;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Types;
|
||||
using ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
namespace ScadaLink.SiteRuntime.Actors;
|
||||
@@ -64,7 +65,7 @@ public class AlarmExecutionActor : ReceiveActor
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
Instance = context,
|
||||
Parameters = new Dictionary<string, object?>(),
|
||||
Parameters = new ScriptParameters(),
|
||||
CancellationToken = cts.Token
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Interfaces.Services;
|
||||
using ScadaLink.Commons.Messages.ScriptExecution;
|
||||
using ScadaLink.Commons.Types;
|
||||
using ScadaLink.HealthMonitoring;
|
||||
using ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
@@ -100,7 +101,7 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
Instance = context,
|
||||
Parameters = parameters ?? new Dictionary<string, object?>(),
|
||||
Parameters = new ScriptParameters(parameters ?? new Dictionary<string, object?>()),
|
||||
CancellationToken = cts.Token
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Types;
|
||||
|
||||
namespace ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
@@ -177,8 +178,7 @@ public class ScriptCompilationResult
|
||||
public class ScriptGlobals
|
||||
{
|
||||
public ScriptRuntimeContext Instance { get; set; } = null!;
|
||||
public IReadOnlyDictionary<string, object?> Parameters { get; set; } =
|
||||
new Dictionary<string, object?>();
|
||||
public ScriptParameters Parameters { get; set; } = new ScriptParameters();
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Types;
|
||||
|
||||
namespace ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
@@ -82,7 +83,7 @@ public class SharedScriptLibrary
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
Instance = context,
|
||||
Parameters = parameters ?? new Dictionary<string, object?>(),
|
||||
Parameters = new ScriptParameters(parameters ?? new Dictionary<string, object?>()),
|
||||
CancellationToken = cancellationToken
|
||||
};
|
||||
|
||||
|
||||
403
tests/ScadaLink.Commons.Tests/Types/ScriptParametersTests.cs
Normal file
403
tests/ScadaLink.Commons.Tests/Types/ScriptParametersTests.cs
Normal file
@@ -0,0 +1,403 @@
|
||||
using System.Text.Json;
|
||||
using ScadaLink.Commons.Types;
|
||||
|
||||
namespace ScadaLink.Commons.Tests.Types;
|
||||
|
||||
public class ScriptParametersTests
|
||||
{
|
||||
// ── Non-nullable scalar Get<T> ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Get_Int_ExactTypeMatch()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = 42 });
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Int_FromLong()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = 42L });
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Int_FromString()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = "42" });
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Int_MissingKey_Throws()
|
||||
{
|
||||
var p = new ScriptParameters();
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int>("x"));
|
||||
Assert.Contains("Parameter 'x' not found", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Int_NullValue_Throws()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = null });
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int>("x"));
|
||||
Assert.Contains("Parameter 'x' value is null", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Int_Unparsable_Throws()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = "abc" });
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int>("x"));
|
||||
Assert.Contains("could not be parsed as Int32", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_String_DirectReturn()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["s"] = "hello" });
|
||||
Assert.Equal("hello", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_String_NullValue_Throws()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["s"] = null });
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<string>("s"));
|
||||
Assert.Contains("value is null", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_String_FromInt()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["s"] = 42 });
|
||||
Assert.Equal("42", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Bool_True()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["b"] = true });
|
||||
Assert.True(p.Get<bool>("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Double_FromLong()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = 42L });
|
||||
Assert.Equal(42.0, p.Get<double>("d"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Double_FromFloat()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = 3.14f });
|
||||
Assert.Equal(3.14, p.Get<double>("d"), 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Long_FromInt()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["l"] = 42 });
|
||||
Assert.Equal(42L, p.Get<long>("l"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Float_FromDouble()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["f"] = 3.14 });
|
||||
Assert.Equal(3.14f, p.Get<float>("f"), 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_DateTime_FromString()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["dt"] = "2026-03-22T10:30:00Z" });
|
||||
var result = p.Get<DateTime>("dt");
|
||||
Assert.Equal(new DateTime(2026, 3, 22, 10, 30, 0, DateTimeKind.Utc), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_DateTime_ExactType()
|
||||
{
|
||||
var dt = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["dt"] = dt });
|
||||
Assert.Equal(dt, p.Get<DateTime>("dt"));
|
||||
}
|
||||
|
||||
// ── Nullable scalar Get<T?> ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Get_NullableInt_MissingKey_ReturnsNull()
|
||||
{
|
||||
var p = new ScriptParameters();
|
||||
Assert.Null(p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_NullableInt_NullValue_ReturnsNull()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = null });
|
||||
Assert.Null(p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_NullableInt_ValidValue_ReturnsValue()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = 42L });
|
||||
Assert.Equal(42, p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_NullableInt_Unparsable_ReturnsNull()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = "abc" });
|
||||
Assert.Null(p.Get<int?>("x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_NullableDouble_ValidValue()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = 3.14 });
|
||||
Assert.Equal(3.14, p.Get<double?>("d"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_NullableBool_ValidValue()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["b"] = true });
|
||||
Assert.True(p.Get<bool?>("b"));
|
||||
}
|
||||
|
||||
// ── Array Get<T[]> ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Get_IntArray_FromListOfLongs()
|
||||
{
|
||||
var list = new List<object?> { 1L, 2L, 3L };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
|
||||
Assert.Equal(new[] { 1, 2, 3 }, p.Get<int[]>("arr"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_IntArray_EmptyList()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = new List<object?>() });
|
||||
Assert.Empty(p.Get<int[]>("arr"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_IntArray_NonList_Throws()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = "not a list" });
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int[]>("arr"));
|
||||
Assert.Contains("is not a list or array", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_IntArray_UnparsableElement_ThrowsWithIndex()
|
||||
{
|
||||
var list = new List<object?> { 1L, 2L, "bad" };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int[]>("arr"));
|
||||
Assert.Contains("element at index 2", ex.Message);
|
||||
Assert.Contains("'bad'", ex.Message);
|
||||
Assert.Contains("Int32", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_IntArray_NullElement_Throws()
|
||||
{
|
||||
var list = new List<object?> { 1L, null, 3L };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int[]>("arr"));
|
||||
Assert.Contains("element at index 1 is null", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_StringArray_FromListOfStrings()
|
||||
{
|
||||
var list = new List<object?> { "a", "b", "c" };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
|
||||
Assert.Equal(new[] { "a", "b", "c" }, p.Get<string[]>("arr"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_DoubleArray_FromListOfNumbers()
|
||||
{
|
||||
var list = new List<object?> { 1.1, 2.2, 3.3 };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
|
||||
Assert.Equal(new[] { 1.1, 2.2, 3.3 }, p.Get<double[]>("arr"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_IntArray_MissingKey_Throws()
|
||||
{
|
||||
var p = new ScriptParameters();
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int[]>("arr"));
|
||||
Assert.Contains("not found", ex.Message);
|
||||
}
|
||||
|
||||
// ── List<T> Get<List<T>> ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Get_ListInt_FromListOfLongs()
|
||||
{
|
||||
var list = new List<object?> { 10L, 20L, 30L };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
|
||||
var result = p.Get<List<int>>("items");
|
||||
Assert.Equal(new List<int> { 10, 20, 30 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_ListString_FromListOfStrings()
|
||||
{
|
||||
var list = new List<object?> { "x", "y" };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
|
||||
Assert.Equal(new List<string> { "x", "y" }, p.Get<List<string>>("items"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_ListInt_UnparsableElement_ThrowsWithIndex()
|
||||
{
|
||||
var list = new List<object?> { 1L, "oops" };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<List<int>>("items"));
|
||||
Assert.Contains("element at index 1", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_ListDouble_FromMixedNumbers()
|
||||
{
|
||||
var list = new List<object?> { 1L, 2.5, 3 };
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
|
||||
var result = p.Get<List<double>>("items");
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Equal(1.0, result[0]);
|
||||
Assert.Equal(2.5, result[1]);
|
||||
Assert.Equal(3.0, result[2]);
|
||||
}
|
||||
|
||||
// ── Backward compatibility ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Indexer_Works()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["key"] = "val" });
|
||||
Assert.Equal("val", p["key"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsKey_Works()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["key"] = 1 });
|
||||
Assert.True(p.ContainsKey("key"));
|
||||
Assert.False(p.ContainsKey("missing"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetValue_Works()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["key"] = 42 });
|
||||
Assert.True(p.TryGetValue("key", out var val));
|
||||
Assert.Equal(42, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Count_Works()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["a"] = 1, ["b"] = 2 });
|
||||
Assert.Equal(2, p.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumeration_Works()
|
||||
{
|
||||
var dict = new Dictionary<string, object?> { ["a"] = 1, ["b"] = 2 };
|
||||
var p = new ScriptParameters(dict);
|
||||
var keys = p.Select(kv => kv.Key).OrderBy(k => k).ToList();
|
||||
Assert.Equal(new[] { "a", "b" }, keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyConstructor_ProducesEmptyDictionary()
|
||||
{
|
||||
var p = new ScriptParameters();
|
||||
Assert.Empty(p);
|
||||
Assert.False(p.ContainsKey("anything"));
|
||||
}
|
||||
|
||||
// ── Edge cases ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Get_Double_FromInt()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = 42 });
|
||||
Assert.Equal(42.0, p.Get<double>("d"));
|
||||
}
|
||||
|
||||
// ── JsonElement values (from JSON deserialization) ─────────────
|
||||
|
||||
[Fact]
|
||||
public void Get_Int_FromJsonElement()
|
||||
{
|
||||
using var doc = JsonDocument.Parse("{\"x\": 42}");
|
||||
var element = doc.RootElement.GetProperty("x").Clone();
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = element });
|
||||
Assert.Equal(42, p.Get<int>("x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_IntArray_FromJsonElementList()
|
||||
{
|
||||
// Simulates what JsonSerializer.Deserialize<List<object?>> produces
|
||||
using var doc = JsonDocument.Parse("[10, 20, 30]");
|
||||
var list = JsonSerializer.Deserialize<List<object?>>(doc.RootElement.GetRawText())!;
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
|
||||
Assert.Equal(new[] { 10, 20, 30 }, p.Get<int[]>("items"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_ListInt_FromJsonElementList()
|
||||
{
|
||||
using var doc = JsonDocument.Parse("[1, 2, 3]");
|
||||
var list = JsonSerializer.Deserialize<List<object?>>(doc.RootElement.GetRawText())!;
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
|
||||
Assert.Equal(new List<int> { 1, 2, 3 }, p.Get<List<int>>("items"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_String_FromJsonElement()
|
||||
{
|
||||
using var doc = JsonDocument.Parse("{\"s\": \"hello\"}");
|
||||
var element = doc.RootElement.GetProperty("s").Clone();
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["s"] = element });
|
||||
Assert.Equal("hello", p.Get<string>("s"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Double_FromJsonElement()
|
||||
{
|
||||
using var doc = JsonDocument.Parse("{\"d\": 3.14}");
|
||||
var element = doc.RootElement.GetProperty("d").Clone();
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = element });
|
||||
Assert.Equal(3.14, p.Get<double>("d"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Bool_FromJsonElement()
|
||||
{
|
||||
using var doc = JsonDocument.Parse("{\"b\": true}");
|
||||
var element = doc.RootElement.GetProperty("b").Clone();
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["b"] = element });
|
||||
Assert.True(p.Get<bool>("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_Int_OverflowFromLong_Throws()
|
||||
{
|
||||
var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = long.MaxValue });
|
||||
var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int>("x"));
|
||||
Assert.Contains("could not be parsed as Int32", ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ScadaLink.Commons.Types;
|
||||
using ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
namespace ScadaLink.SiteRuntime.Tests.Scripts;
|
||||
@@ -236,7 +237,7 @@ public class SandboxTests
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
Instance = null!,
|
||||
Parameters = new Dictionary<string, object?>(),
|
||||
Parameters = new ScriptParameters(),
|
||||
CancellationToken = cts.Token
|
||||
};
|
||||
|
||||
@@ -266,7 +267,7 @@ public class SandboxTests
|
||||
var globals = new ScriptGlobals
|
||||
{
|
||||
Instance = null!,
|
||||
Parameters = new Dictionary<string, object?>(),
|
||||
Parameters = new ScriptParameters(),
|
||||
CancellationToken = cts.Token
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user