using System.Text.Json; namespace ScadaLink.CentralUI.Components.Shared; /// /// In-memory JSON Schema tree used by . The editor /// mutates this graph directly; handles /// parse / serialize round-tripping to the canonical JSON Schema text stored /// in TemplateScript / SharedScript / ApiMethod columns. /// internal sealed class SchemaNode { /// One of: string · integer · number · boolean · object · array. public string Type { get; set; } = "string"; /// For type=array: the schema of the array's items. public SchemaNode? Items { get; set; } /// For type=object: ordered list of named properties. public List Properties { get; } = new(); } internal sealed class SchemaProperty { /// Stable identity for Blazor @key across renames. 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" }; /// /// Parse a JSON Schema string into a tree. /// Returns the supplied when the input is /// empty or malformed. Also accepts the legacy flat-array parameter /// shape ([{name,type,required,itemType?}]) for safety during the /// transition window — translates it into an equivalent object schema. /// 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; } } /// Default empty object schema (parameters mode default). public static SchemaNode NewObject() => new() { Type = "object" }; /// Default scalar schema (return mode default). 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(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(); } }