using System.Buffers; using System.Collections.Immutable; using System.Text.Json; namespace StellaOps.PolicyDsl; /// /// Serializes policy IR documents to a canonical JSON representation for hashing. /// public static class PolicyIrSerializer { public static ImmutableArray Serialize(PolicyIrDocument document) { var buffer = new ArrayBufferWriter(); using var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Indented = false, SkipValidation = false }); WriteDocument(writer, document); writer.Flush(); return buffer.WrittenSpan.ToArray().ToImmutableArray(); } private static void WriteDocument(Utf8JsonWriter writer, PolicyIrDocument document) { writer.WriteStartObject(); writer.WriteString("name", document.Name); writer.WriteString("syntax", document.Syntax); writer.WritePropertyName("metadata"); WriteLiteralDictionary(writer, document.Metadata); writer.WritePropertyName("profiles"); writer.WriteStartArray(); foreach (var profile in document.Profiles) { WriteProfile(writer, profile); } writer.WriteEndArray(); writer.WritePropertyName("settings"); WriteLiteralDictionary(writer, document.Settings); writer.WritePropertyName("rules"); writer.WriteStartArray(); foreach (var rule in document.Rules) { WriteRule(writer, rule); } writer.WriteEndArray(); writer.WriteEndObject(); } private static void WriteProfile(Utf8JsonWriter writer, PolicyIrProfile profile) { writer.WriteStartObject(); writer.WriteString("name", profile.Name); writer.WritePropertyName("maps"); writer.WriteStartArray(); foreach (var map in profile.Maps) { writer.WriteStartObject(); writer.WriteString("name", map.Name); writer.WritePropertyName("entries"); writer.WriteStartArray(); foreach (var entry in map.Entries) { writer.WriteStartObject(); writer.WriteString("source", entry.Source); writer.WriteNumber("weight", entry.Weight); writer.WriteEndObject(); } writer.WriteEndArray(); writer.WriteEndObject(); } writer.WriteEndArray(); writer.WritePropertyName("env"); writer.WriteStartArray(); foreach (var env in profile.Environments) { writer.WriteStartObject(); writer.WriteString("name", env.Name); writer.WritePropertyName("entries"); writer.WriteStartArray(); foreach (var entry in env.Entries) { writer.WriteStartObject(); writer.WritePropertyName("condition"); WriteExpression(writer, entry.Condition); writer.WriteNumber("weight", entry.Weight); writer.WriteEndObject(); } writer.WriteEndArray(); writer.WriteEndObject(); } writer.WriteEndArray(); writer.WritePropertyName("scalars"); writer.WriteStartArray(); foreach (var scalar in profile.Scalars) { writer.WriteStartObject(); writer.WriteString("name", scalar.Name); writer.WritePropertyName("value"); WriteLiteral(writer, scalar.Value); writer.WriteEndObject(); } writer.WriteEndArray(); writer.WriteEndObject(); } private static void WriteRule(Utf8JsonWriter writer, PolicyIrRule rule) { writer.WriteStartObject(); writer.WriteString("name", rule.Name); writer.WriteNumber("priority", rule.Priority); writer.WritePropertyName("when"); WriteExpression(writer, rule.When); writer.WritePropertyName("then"); WriteActions(writer, rule.ThenActions); writer.WritePropertyName("else"); WriteActions(writer, rule.ElseActions); writer.WriteString("because", rule.Because); writer.WriteEndObject(); } private static void WriteActions(Utf8JsonWriter writer, ImmutableArray actions) { writer.WriteStartArray(); foreach (var action in actions) { WriteAction(writer, action); } writer.WriteEndArray(); } private static void WriteAction(Utf8JsonWriter writer, PolicyIrAction action) { switch (action) { case PolicyIrAssignmentAction assign: writer.WriteStartObject(); writer.WriteString("type", "assign"); WriteReference(writer, assign.Target); writer.WritePropertyName("value"); WriteExpression(writer, assign.Value); writer.WriteEndObject(); break; case PolicyIrAnnotateAction annotate: writer.WriteStartObject(); writer.WriteString("type", "annotate"); WriteReference(writer, annotate.Target); writer.WritePropertyName("value"); WriteExpression(writer, annotate.Value); writer.WriteEndObject(); break; case PolicyIrIgnoreAction ignore: writer.WriteStartObject(); writer.WriteString("type", "ignore"); writer.WritePropertyName("until"); WriteOptionalExpression(writer, ignore.Until); writer.WriteString("because", ignore.Because ?? string.Empty); writer.WriteEndObject(); break; case PolicyIrEscalateAction escalate: writer.WriteStartObject(); writer.WriteString("type", "escalate"); writer.WritePropertyName("to"); WriteOptionalExpression(writer, escalate.To); writer.WritePropertyName("when"); WriteOptionalExpression(writer, escalate.When); writer.WriteEndObject(); break; case PolicyIrRequireVexAction require: writer.WriteStartObject(); writer.WriteString("type", "requireVex"); writer.WritePropertyName("conditions"); writer.WriteStartObject(); foreach (var kvp in require.Conditions) { writer.WritePropertyName(kvp.Key); WriteExpression(writer, kvp.Value); } writer.WriteEndObject(); writer.WriteEndObject(); break; case PolicyIrWarnAction warn: writer.WriteStartObject(); writer.WriteString("type", "warn"); writer.WritePropertyName("message"); WriteOptionalExpression(writer, warn.Message); writer.WriteEndObject(); break; case PolicyIrDeferAction defer: writer.WriteStartObject(); writer.WriteString("type", "defer"); writer.WritePropertyName("until"); WriteOptionalExpression(writer, defer.Until); writer.WriteEndObject(); break; } } private static void WriteReference(Utf8JsonWriter writer, ImmutableArray segments) { writer.WritePropertyName("target"); writer.WriteStartArray(); foreach (var segment in segments) { writer.WriteStringValue(segment); } writer.WriteEndArray(); } private static void WriteOptionalExpression(Utf8JsonWriter writer, PolicyExpression? expression) { if (expression is null) { writer.WriteNullValue(); return; } WriteExpression(writer, expression); } private static void WriteExpression(Utf8JsonWriter writer, PolicyExpression expression) { switch (expression) { case PolicyLiteralExpression literal: writer.WriteStartObject(); writer.WriteString("type", "literal"); writer.WritePropertyName("value"); WriteLiteralValue(writer, literal.Value); writer.WriteEndObject(); break; case PolicyListExpression list: writer.WriteStartObject(); writer.WriteString("type", "list"); writer.WritePropertyName("items"); writer.WriteStartArray(); foreach (var item in list.Items) { WriteExpression(writer, item); } writer.WriteEndArray(); writer.WriteEndObject(); break; case PolicyIdentifierExpression identifier: writer.WriteStartObject(); writer.WriteString("type", "identifier"); writer.WriteString("name", identifier.Name); writer.WriteEndObject(); break; case PolicyMemberAccessExpression member: writer.WriteStartObject(); writer.WriteString("type", "member"); writer.WritePropertyName("target"); WriteExpression(writer, member.Target); writer.WriteString("member", member.Member); writer.WriteEndObject(); break; case PolicyInvocationExpression invocation: writer.WriteStartObject(); writer.WriteString("type", "call"); writer.WritePropertyName("target"); WriteExpression(writer, invocation.Target); writer.WritePropertyName("args"); writer.WriteStartArray(); foreach (var arg in invocation.Arguments) { WriteExpression(writer, arg); } writer.WriteEndArray(); writer.WriteEndObject(); break; case PolicyIndexerExpression indexer: writer.WriteStartObject(); writer.WriteString("type", "indexer"); writer.WritePropertyName("target"); WriteExpression(writer, indexer.Target); writer.WritePropertyName("index"); WriteExpression(writer, indexer.Index); writer.WriteEndObject(); break; case PolicyUnaryExpression unary: writer.WriteStartObject(); writer.WriteString("type", "unary"); writer.WriteString("op", unary.Operator switch { PolicyUnaryOperator.Not => "not", _ => unary.Operator.ToString().ToLowerInvariant(), }); writer.WritePropertyName("operand"); WriteExpression(writer, unary.Operand); writer.WriteEndObject(); break; case PolicyBinaryExpression binary: writer.WriteStartObject(); writer.WriteString("type", "binary"); writer.WriteString("op", GetBinaryOperator(binary.Operator)); writer.WritePropertyName("left"); WriteExpression(writer, binary.Left); writer.WritePropertyName("right"); WriteExpression(writer, binary.Right); writer.WriteEndObject(); break; default: writer.WriteStartObject(); writer.WriteString("type", "unknown"); writer.WriteEndObject(); break; } } private static string GetBinaryOperator(PolicyBinaryOperator op) => op switch { PolicyBinaryOperator.And => "and", PolicyBinaryOperator.Or => "or", PolicyBinaryOperator.Equal => "eq", PolicyBinaryOperator.NotEqual => "neq", PolicyBinaryOperator.LessThan => "lt", PolicyBinaryOperator.LessThanOrEqual => "lte", PolicyBinaryOperator.GreaterThan => "gt", PolicyBinaryOperator.GreaterThanOrEqual => "gte", PolicyBinaryOperator.In => "in", PolicyBinaryOperator.NotIn => "not_in", _ => op.ToString().ToLowerInvariant(), }; private static void WriteLiteralDictionary(Utf8JsonWriter writer, ImmutableSortedDictionary dictionary) { writer.WriteStartObject(); foreach (var kvp in dictionary) { writer.WritePropertyName(kvp.Key); WriteLiteral(writer, kvp.Value); } writer.WriteEndObject(); } private static void WriteLiteral(Utf8JsonWriter writer, PolicyIrLiteral literal) { switch (literal) { case PolicyIrStringLiteral s: writer.WriteStringValue(s.Value); break; case PolicyIrNumberLiteral n: writer.WriteNumberValue(n.Value); break; case PolicyIrBooleanLiteral b: writer.WriteBooleanValue(b.Value); break; case PolicyIrListLiteral list: writer.WriteStartArray(); foreach (var item in list.Items) { WriteLiteral(writer, item); } writer.WriteEndArray(); break; default: writer.WriteNullValue(); break; } } private static void WriteLiteralValue(Utf8JsonWriter writer, object? value) { switch (value) { case null: writer.WriteNullValue(); break; case string s: writer.WriteStringValue(s); break; case bool b: writer.WriteBooleanValue(b); break; case decimal dec: writer.WriteNumberValue(dec); break; case double dbl: writer.WriteNumberValue(dbl); break; case int i: writer.WriteNumberValue(i); break; default: writer.WriteStringValue(value.ToString()); break; } } }