up
This commit is contained in:
213
src/Policy/StellaOps.PolicyDsl/PolicyEngineFactory.cs
Normal file
213
src/Policy/StellaOps.PolicyDsl/PolicyEngineFactory.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
namespace StellaOps.PolicyDsl;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating policy evaluation engines from compiled policy documents.
|
||||
/// </summary>
|
||||
public sealed class PolicyEngineFactory
|
||||
{
|
||||
private readonly PolicyCompiler _compiler = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a policy engine from source code.
|
||||
/// </summary>
|
||||
/// <param name="source">The policy DSL source code.</param>
|
||||
/// <returns>A policy engine if compilation succeeds, otherwise null with diagnostics.</returns>
|
||||
public PolicyEngineResult CreateFromSource(string source)
|
||||
{
|
||||
var compilation = _compiler.Compile(source);
|
||||
if (!compilation.Success || compilation.Document is null)
|
||||
{
|
||||
return new PolicyEngineResult(null, compilation.Diagnostics);
|
||||
}
|
||||
|
||||
var engine = new PolicyEngine(compilation.Document, compilation.Checksum!);
|
||||
return new PolicyEngineResult(engine, compilation.Diagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a policy engine from a pre-compiled IR document.
|
||||
/// </summary>
|
||||
/// <param name="document">The compiled policy IR document.</param>
|
||||
/// <param name="checksum">The policy checksum.</param>
|
||||
/// <returns>A policy engine.</returns>
|
||||
public PolicyEngine CreateFromDocument(PolicyIrDocument document, string checksum)
|
||||
{
|
||||
return new PolicyEngine(document, checksum);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of creating a policy engine.
|
||||
/// </summary>
|
||||
public sealed record PolicyEngineResult(
|
||||
PolicyEngine? Engine,
|
||||
System.Collections.Immutable.ImmutableArray<StellaOps.Policy.PolicyIssue> Diagnostics);
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight policy evaluation engine.
|
||||
/// </summary>
|
||||
public sealed class PolicyEngine
|
||||
{
|
||||
internal PolicyEngine(PolicyIrDocument document, string checksum)
|
||||
{
|
||||
Document = document;
|
||||
Checksum = checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the compiled policy document.
|
||||
/// </summary>
|
||||
public PolicyIrDocument Document { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the policy checksum (SHA-256 of canonical representation).
|
||||
/// </summary>
|
||||
public string Checksum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the policy name.
|
||||
/// </summary>
|
||||
public string Name => Document.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the policy syntax version.
|
||||
/// </summary>
|
||||
public string Syntax => Document.Syntax;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of rules in the policy.
|
||||
/// </summary>
|
||||
public int RuleCount => Document.Rules.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the policy against the given signal context.
|
||||
/// </summary>
|
||||
/// <param name="context">The signal context to evaluate against.</param>
|
||||
/// <returns>The evaluation result.</returns>
|
||||
public PolicyEvaluationResult Evaluate(SignalContext context)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var matchedRules = new List<string>();
|
||||
var actions = new List<EvaluatedAction>();
|
||||
|
||||
foreach (var rule in Document.Rules.OrderByDescending(r => r.Priority))
|
||||
{
|
||||
var matched = EvaluateExpression(rule.When, context);
|
||||
if (matched)
|
||||
{
|
||||
matchedRules.Add(rule.Name);
|
||||
foreach (var action in rule.ThenActions)
|
||||
{
|
||||
actions.Add(new EvaluatedAction(rule.Name, action, WasElseBranch: false));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var action in rule.ElseActions)
|
||||
{
|
||||
actions.Add(new EvaluatedAction(rule.Name, action, WasElseBranch: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new PolicyEvaluationResult(
|
||||
PolicyName: Name,
|
||||
PolicyChecksum: Checksum,
|
||||
MatchedRules: matchedRules.ToArray(),
|
||||
Actions: actions.ToArray());
|
||||
}
|
||||
|
||||
private static bool EvaluateExpression(PolicyExpression expression, SignalContext context)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
PolicyBinaryExpression binary => EvaluateBinary(binary, context),
|
||||
PolicyUnaryExpression unary => EvaluateUnary(unary, context),
|
||||
PolicyLiteralExpression literal => literal.Value is bool b && b,
|
||||
PolicyIdentifierExpression identifier => context.HasSignal(identifier.Name),
|
||||
PolicyMemberAccessExpression member => EvaluateMemberAccess(member, context),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool EvaluateBinary(PolicyBinaryExpression binary, SignalContext context)
|
||||
{
|
||||
return binary.Operator switch
|
||||
{
|
||||
PolicyBinaryOperator.And => EvaluateExpression(binary.Left, context) && EvaluateExpression(binary.Right, context),
|
||||
PolicyBinaryOperator.Or => EvaluateExpression(binary.Left, context) || EvaluateExpression(binary.Right, context),
|
||||
PolicyBinaryOperator.Equal => EvaluateEquality(binary.Left, binary.Right, context, negate: false),
|
||||
PolicyBinaryOperator.NotEqual => EvaluateEquality(binary.Left, binary.Right, context, negate: true),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool EvaluateUnary(PolicyUnaryExpression unary, SignalContext context)
|
||||
{
|
||||
return unary.Operator switch
|
||||
{
|
||||
PolicyUnaryOperator.Not => !EvaluateExpression(unary.Operand, context),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool EvaluateMemberAccess(PolicyMemberAccessExpression member, SignalContext context)
|
||||
{
|
||||
var value = ResolveValue(member.Target, context);
|
||||
if (value is IDictionary<string, object?> dict)
|
||||
{
|
||||
return dict.TryGetValue(member.Member, out var v) && v is bool b && b;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool EvaluateEquality(PolicyExpression left, PolicyExpression right, SignalContext context, bool negate)
|
||||
{
|
||||
var leftValue = ResolveValue(left, context);
|
||||
var rightValue = ResolveValue(right, context);
|
||||
var equal = Equals(leftValue, rightValue);
|
||||
return negate ? !equal : equal;
|
||||
}
|
||||
|
||||
private static object? ResolveValue(PolicyExpression expression, SignalContext context)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
PolicyLiteralExpression literal => literal.Value,
|
||||
PolicyIdentifierExpression identifier => context.GetSignal(identifier.Name),
|
||||
PolicyMemberAccessExpression member => ResolveMemberValue(member, context),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static object? ResolveMemberValue(PolicyMemberAccessExpression member, SignalContext context)
|
||||
{
|
||||
var target = ResolveValue(member.Target, context);
|
||||
if (target is IDictionary<string, object?> dict)
|
||||
{
|
||||
return dict.TryGetValue(member.Member, out var v) ? v : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of evaluating a policy.
|
||||
/// </summary>
|
||||
public sealed record PolicyEvaluationResult(
|
||||
string PolicyName,
|
||||
string PolicyChecksum,
|
||||
string[] MatchedRules,
|
||||
EvaluatedAction[] Actions);
|
||||
|
||||
/// <summary>
|
||||
/// An action that was evaluated as part of policy execution.
|
||||
/// </summary>
|
||||
public sealed record EvaluatedAction(
|
||||
string RuleName,
|
||||
PolicyIrAction Action,
|
||||
bool WasElseBranch);
|
||||
Reference in New Issue
Block a user