Files
git.stella-ops.org/src/Policy/StellaOps.PolicyDsl/PolicyIrSerializer.cs
2026-02-01 21:37:40 +02:00

420 lines
14 KiB
C#

using System.Buffers;
using System.Collections.Immutable;
using System.Text.Json;
namespace StellaOps.PolicyDsl;
/// <summary>
/// Serializes policy IR documents to a canonical JSON representation for hashing.
/// </summary>
public static class PolicyIrSerializer
{
public static ImmutableArray<byte> Serialize(PolicyIrDocument document)
{
var buffer = new ArrayBufferWriter<byte>();
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<PolicyIrAction> 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<string> 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<string, PolicyIrLiteral> 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;
}
}
}