Skip to content

Commit b3a05a2

Browse files
committed
Feat: policy composition and decision composer
1 parent 9cfb37d commit b3a05a2

3 files changed

Lines changed: 499 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using ModularityKit.Mutator.Abstractions.Engine;
2+
3+
namespace ModularityKit.Mutator.Abstractions.Policies;
4+
5+
/// <summary>
6+
/// Represents one composed policy built from multiple child policies.
7+
/// </summary>
8+
/// <remarks>
9+
/// The type is intentionally internal because callers are expected to use the
10+
/// factory methods on <see cref="PolicyComposition"/> instead of constructing
11+
/// composed policies directly. That keeps the composition surface explicit and
12+
/// allows the implementation to enforce validation and ordering rules in one
13+
/// place.
14+
/// </remarks>
15+
internal sealed class ComposedMutationPolicy<TState> : IMutationPolicy<TState>
16+
{
17+
private readonly IReadOnlyList<IMutationPolicy<TState>> _policies;
18+
19+
/// <summary>
20+
/// Creates a composed policy from a validated set of child policies.
21+
/// </summary>
22+
/// <param name="name">The composed policy name.</param>
23+
/// <param name="priority">The priority of the composed policy.</param>
24+
/// <param name="description">An optional human-readable description.</param>
25+
/// <param name="mode">The composition mode that controls decision merging.</param>
26+
/// <param name="policies">The child policies to evaluate.</param>
27+
public ComposedMutationPolicy(
28+
string name,
29+
int priority,
30+
string? description,
31+
PolicyCompositionMode mode,
32+
IEnumerable<IMutationPolicy<TState>> policies)
33+
{
34+
if (string.IsNullOrWhiteSpace(name))
35+
throw new ArgumentException("A composed policy name is required.", nameof(name));
36+
37+
ArgumentNullException.ThrowIfNull(policies);
38+
39+
Name = name;
40+
Priority = priority;
41+
Description = description;
42+
Mode = mode;
43+
_policies = ValidatePolicies(policies);
44+
}
45+
46+
public string Name { get; }
47+
48+
public int Priority { get; }
49+
50+
public string? Description { get; }
51+
52+
private PolicyCompositionMode Mode { get; }
53+
54+
public PolicyDecision Evaluate(IMutation<TState> mutation, TState state)
55+
=> EvaluateAsync(mutation, state, CancellationToken.None).GetAwaiter().GetResult();
56+
57+
public Task<PolicyDecision> EvaluateAsync(
58+
IMutation<TState> mutation,
59+
TState state,
60+
CancellationToken cancellationToken = default)
61+
{
62+
ArgumentNullException.ThrowIfNull(mutation);
63+
64+
return PolicyDecisionComposer.ComposeAsync(
65+
Name,
66+
Mode,
67+
_policies,
68+
mutation,
69+
state,
70+
cancellationToken);
71+
}
72+
73+
/// <summary>
74+
/// Validates that the composed policy contains at least one non-null child policy.
75+
/// </summary>
76+
/// <param name="policies">The candidate child policies.</param>
77+
/// <returns>The validated list of child policies.</returns>
78+
private static IReadOnlyList<IMutationPolicy<TState>> ValidatePolicies(IEnumerable<IMutationPolicy<TState>> policies)
79+
{
80+
var validated = new List<IMutationPolicy<TState>>();
81+
var index = 0;
82+
83+
foreach (var policy in policies)
84+
{
85+
if (policy is null)
86+
throw new ArgumentException($"Child policy at index {index} is null.", nameof(policies));
87+
88+
validated.Add(policy);
89+
index++;
90+
}
91+
92+
if (validated.Count == 0)
93+
throw new ArgumentException("At least one child policy is required.", nameof(policies));
94+
95+
return validated;
96+
}
97+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
namespace ModularityKit.Mutator.Abstractions.Policies;
2+
3+
/// <summary>
4+
/// Factory methods for composing multiple mutation policies into one deterministic policy.
5+
/// </summary>
6+
public static class PolicyComposition
7+
{
8+
/// <summary>
9+
/// Creates composed policy that requires every child policy to allow the mutation.
10+
/// </summary>
11+
/// <typeparam name="TState">The state type handled by the composed policy.</typeparam>
12+
/// <param name="name">The composed policy name.</param>
13+
/// <param name="policies">The child policies to evaluate.</param>
14+
/// <param name="priority">The priority of the composed policy.</param>
15+
/// <param name="description">An optional human readable description.</param>
16+
/// <returns>A composed policy that merges all child decisions.</returns>
17+
public static IMutationPolicy<TState> AllOf<TState>(
18+
string name,
19+
IEnumerable<IMutationPolicy<TState>> policies,
20+
int priority = 0,
21+
string? description = null)
22+
=> new ComposedMutationPolicy<TState>(
23+
name,
24+
priority,
25+
description,
26+
PolicyCompositionMode.AllOf,
27+
policies);
28+
29+
/// <summary>
30+
/// Creates composed policy that succeeds when at least one child policy allows the mutation.
31+
/// </summary>
32+
/// <typeparam name="TState">The state type handled by the composed policy.</typeparam>
33+
/// <param name="name">The composed policy name.</param>
34+
/// <param name="policies">The child policies to evaluate.</param>
35+
/// <param name="priority">The priority of the composed policy.</param>
36+
/// <param name="description">An optional human readable description.</param>
37+
/// <returns>A composed policy that selects the allowed branch when one exists.</returns>
38+
public static IMutationPolicy<TState> AnyOf<TState>(
39+
string name,
40+
IEnumerable<IMutationPolicy<TState>> policies,
41+
int priority = 0,
42+
string? description = null)
43+
=> new ComposedMutationPolicy<TState>(
44+
name,
45+
priority,
46+
description,
47+
PolicyCompositionMode.AnyOf,
48+
policies);
49+
50+
/// <summary>
51+
/// Creates a composed policy that evaluates children in deterministic priority order and
52+
/// returns the first decisive result.
53+
/// </summary>
54+
/// <typeparam name="TState">The state type handled by the composed policy.</typeparam>
55+
/// <param name="name">The composed policy name.</param>
56+
/// <param name="policies">The child policies to evaluate.</param>
57+
/// <param name="priority">The priority of the composed policy.</param>
58+
/// <param name="description">An optional human readable description.</param>
59+
/// <returns>A composed policy that stops at the first decisive child decision.</returns>
60+
public static IMutationPolicy<TState> Priority<TState>(
61+
string name,
62+
IEnumerable<IMutationPolicy<TState>> policies,
63+
int priority = 0,
64+
string? description = null)
65+
=> new ComposedMutationPolicy<TState>(
66+
name,
67+
priority,
68+
description,
69+
PolicyCompositionMode.Priority,
70+
policies);
71+
}

0 commit comments

Comments
 (0)