Replace raw-JSON text inputs with rich UI: script parameter/return types use a JSON Schema builder (SchemaBuilder + JsonSchemaShapeParser, with a migration to convert existing definitions); alarm trigger config uses a type-aware editor with a flattened attribute picker (AlarmTriggerEditor). AlarmActor gains optional direction (rising/falling/either) on RateOfChange triggers.
205 lines
7.6 KiB
C#
205 lines
7.6 KiB
C#
using System.Text.Json;
|
|
|
|
namespace ScadaLink.CentralUI.Components.Shared;
|
|
|
|
/// <summary>
|
|
/// In-memory JSON Schema tree used by <see cref="SchemaBuilder"/>. The editor
|
|
/// mutates this graph directly; <see cref="SchemaBuilderModel"/> handles
|
|
/// parse / serialize round-tripping to the canonical JSON Schema text stored
|
|
/// in TemplateScript / SharedScript / ApiMethod columns.
|
|
/// </summary>
|
|
internal sealed class SchemaNode
|
|
{
|
|
/// <summary>One of: <c>string · integer · number · boolean · object · array</c>.</summary>
|
|
public string Type { get; set; } = "string";
|
|
|
|
/// <summary>For <c>type=array</c>: the schema of the array's items.</summary>
|
|
public SchemaNode? Items { get; set; }
|
|
|
|
/// <summary>For <c>type=object</c>: ordered list of named properties.</summary>
|
|
public List<SchemaProperty> Properties { get; } = new();
|
|
}
|
|
|
|
internal sealed class SchemaProperty
|
|
{
|
|
/// <summary>Stable identity for Blazor <c>@key</c> across renames.</summary>
|
|
public Guid Id { get; } = Guid.NewGuid();
|
|
public string Name { get; set; } = string.Empty;
|
|
public bool Required { get; set; } = true;
|
|
public SchemaNode Schema { get; set; } = new();
|
|
}
|
|
|
|
internal static class SchemaBuilderModel
|
|
{
|
|
public static readonly string[] PrimitiveTypes =
|
|
{ "string", "integer", "number", "boolean", "object", "array" };
|
|
|
|
/// <summary>
|
|
/// Parse a JSON Schema string into a <see cref="SchemaNode"/> tree.
|
|
/// Returns the supplied <paramref name="fallback"/> when the input is
|
|
/// empty or malformed. Also accepts the legacy flat-array parameter
|
|
/// shape (<c>[{name,type,required,itemType?}]</c>) for safety during the
|
|
/// transition window — translates it into an equivalent object schema.
|
|
/// </summary>
|
|
public static SchemaNode Parse(string? json, SchemaNode fallback)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json)) return fallback;
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(json);
|
|
return doc.RootElement.ValueKind switch
|
|
{
|
|
JsonValueKind.Object => ParseSchema(doc.RootElement),
|
|
JsonValueKind.Array => ParseLegacyArray(doc.RootElement),
|
|
_ => fallback,
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
/// <summary>Default empty object schema (parameters mode default).</summary>
|
|
public static SchemaNode NewObject() => new() { Type = "object" };
|
|
|
|
/// <summary>Default scalar schema (return mode default).</summary>
|
|
public static SchemaNode NewValue() => new() { Type = "string" };
|
|
|
|
public static string Serialize(SchemaNode node)
|
|
{
|
|
using var stream = new System.IO.MemoryStream();
|
|
using (var writer = new Utf8JsonWriter(stream))
|
|
{
|
|
WriteNode(writer, node);
|
|
}
|
|
return System.Text.Encoding.UTF8.GetString(stream.ToArray());
|
|
}
|
|
|
|
// ── Parse helpers ─────────────────────────────────────────────────────────
|
|
|
|
private static SchemaNode ParseSchema(JsonElement el)
|
|
{
|
|
var node = new SchemaNode { Type = "string" };
|
|
if (el.TryGetProperty("type", out var t) && t.ValueKind == JsonValueKind.String)
|
|
{
|
|
node.Type = NormalizeType(t.GetString());
|
|
}
|
|
|
|
if (node.Type == "array")
|
|
{
|
|
node.Items = el.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Object
|
|
? ParseSchema(items)
|
|
: new SchemaNode { Type = "string" };
|
|
}
|
|
else if (node.Type == "object")
|
|
{
|
|
var requiredSet = new HashSet<string>(StringComparer.Ordinal);
|
|
if (el.TryGetProperty("required", out var req) && req.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var r in req.EnumerateArray())
|
|
{
|
|
if (r.ValueKind == JsonValueKind.String)
|
|
{
|
|
var s = r.GetString();
|
|
if (!string.IsNullOrEmpty(s)) requiredSet.Add(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (el.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Object)
|
|
{
|
|
foreach (var prop in props.EnumerateObject())
|
|
{
|
|
node.Properties.Add(new SchemaProperty
|
|
{
|
|
Name = prop.Name,
|
|
Required = requiredSet.Contains(prop.Name),
|
|
Schema = prop.Value.ValueKind == JsonValueKind.Object
|
|
? ParseSchema(prop.Value)
|
|
: new SchemaNode { Type = "string" },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
private static SchemaNode ParseLegacyArray(JsonElement arr)
|
|
{
|
|
var root = new SchemaNode { Type = "object" };
|
|
foreach (var item in arr.EnumerateArray())
|
|
{
|
|
if (item.ValueKind != JsonValueKind.Object) continue;
|
|
var name = item.TryGetProperty("name", out var n) ? n.GetString() : null;
|
|
if (string.IsNullOrEmpty(name)) continue;
|
|
|
|
var rawType = item.TryGetProperty("type", out var t) ? t.GetString() : "string";
|
|
var required = !item.TryGetProperty("required", out var rq) || rq.ValueKind != JsonValueKind.False;
|
|
var schema = new SchemaNode { Type = NormalizeType(rawType) };
|
|
if (schema.Type == "array")
|
|
{
|
|
var inner = item.TryGetProperty("itemType", out var it) ? it.GetString() : "string";
|
|
schema.Items = new SchemaNode { Type = NormalizeType(inner) };
|
|
}
|
|
root.Properties.Add(new SchemaProperty
|
|
{
|
|
Name = name,
|
|
Required = required,
|
|
Schema = schema,
|
|
});
|
|
}
|
|
return root;
|
|
}
|
|
|
|
private static string NormalizeType(string? raw) => raw?.ToLowerInvariant() switch
|
|
{
|
|
"boolean" or "bool" => "boolean",
|
|
"integer" or "int" or "int32" or "int64" => "integer",
|
|
"number" or "float" or "double" or "decimal" => "number",
|
|
"string" or "datetime" => "string",
|
|
"object" => "object",
|
|
"array" or "list" => "array",
|
|
_ => "string",
|
|
};
|
|
|
|
// ── Serialize helpers ─────────────────────────────────────────────────────
|
|
|
|
private static void WriteNode(Utf8JsonWriter w, SchemaNode node)
|
|
{
|
|
w.WriteStartObject();
|
|
w.WriteString("type", node.Type);
|
|
|
|
if (node.Type == "array")
|
|
{
|
|
w.WritePropertyName("items");
|
|
WriteNode(w, node.Items ?? new SchemaNode { Type = "string" });
|
|
}
|
|
else if (node.Type == "object")
|
|
{
|
|
w.WritePropertyName("properties");
|
|
w.WriteStartObject();
|
|
foreach (var p in node.Properties.Where(p => !string.IsNullOrWhiteSpace(p.Name)))
|
|
{
|
|
w.WritePropertyName(p.Name);
|
|
WriteNode(w, p.Schema);
|
|
}
|
|
w.WriteEndObject();
|
|
|
|
var required = node.Properties
|
|
.Where(p => p.Required && !string.IsNullOrWhiteSpace(p.Name))
|
|
.Select(p => p.Name)
|
|
.ToArray();
|
|
if (required.Length > 0)
|
|
{
|
|
w.WritePropertyName("required");
|
|
w.WriteStartArray();
|
|
foreach (var r in required) w.WriteStringValue(r);
|
|
w.WriteEndArray();
|
|
}
|
|
}
|
|
|
|
w.WriteEndObject();
|
|
}
|
|
}
|