Skip to content

Commit 41b5913

Browse files
committed
Feat: centralize Redis governance keyspace conventions (#32)
1 parent df0a0d3 commit 41b5913

9 files changed

Lines changed: 264 additions & 0 deletions

ModularityKit.Mutator.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<Project Path="Examples/Governance/GovernedExecution/GovernedExecution.csproj" />
1414
<Project Path="Examples/Governance/DecisionTaxonomy/DecisionTaxonomy.csproj" />
1515
<Project Path="Examples/Governance/Queries/Queries.csproj" />
16+
<Project Path="Examples/Governance/RedisQueries/RedisQueries.csproj" />
1617
<Project Path="Examples/Governance/RequestLifecycle/RequestLifecycle.csproj" />
1718
<Project Path="Examples/Governance/VersionedResolution/VersionedResolution.csproj" />
1819
</Folder>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using ModularityKit.Mutator.Governance.Redis;
2+
using ModularityKit.Mutator.Governance.Redis.Configuration;
3+
using Xunit;
4+
5+
namespace ModularityKit.Mutator.Governance.Redis.Tests.Configuration;
6+
7+
public sealed class RedisMutationRequestStoreOptionsTests
8+
{
9+
[Fact]
10+
public void Uses_expected_default_key_prefix()
11+
{
12+
var options = new RedisMutationRequestStoreOptions();
13+
14+
Assert.Equal("modularitykit:governance", options.KeyPrefix);
15+
}
16+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
2+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
3+
using ModularityKit.Mutator.Governance.Redis;
4+
using ModularityKit.Mutator.Governance.Redis.Configuration;
5+
using ModularityKit.Mutator.Governance.Redis.Keys;
6+
using Xunit;
7+
8+
namespace ModularityKit.Mutator.Governance.Redis.Tests.Keys;
9+
10+
public sealed class RedisMutationRequestKeyspaceTests
11+
{
12+
[Fact]
13+
public void Builds_expected_request_keys_from_prefix()
14+
{
15+
var keyspace = new RedisMutationRequestKeyspace(new RedisMutationRequestStoreOptions
16+
{
17+
KeyPrefix = "mk:gov"
18+
});
19+
20+
Assert.Equal("mk:gov:requests:ids", keyspace.RequestIds().ToString());
21+
Assert.Equal("mk:gov:requests:req-42:data", keyspace.RequestData("req-42").ToString());
22+
Assert.Equal("mk:gov:requests:req-42:revision", keyspace.RequestRevision("req-42").ToString());
23+
}
24+
25+
[Fact]
26+
public void Builds_expected_index_keys_for_state_status_and_pending_reason()
27+
{
28+
var keyspace = new RedisMutationRequestKeyspace(new RedisMutationRequestStoreOptions
29+
{
30+
KeyPrefix = "mk:gov"
31+
});
32+
33+
Assert.Equal("mk:gov:states:tenant-42:requests", keyspace.RequestsByStateId("tenant-42").ToString());
34+
Assert.Equal("mk:gov:status:pending:requests", keyspace.RequestsByStatus(MutationRequestStatus.Pending).ToString());
35+
Assert.Equal("mk:gov:pending:requests", keyspace.PendingRequestIds().ToString());
36+
Assert.Equal(
37+
"mk:gov:pending:approval:requests",
38+
keyspace.PendingRequestIds(PendingMutationReason.Approval).ToString());
39+
}
40+
41+
[Fact]
42+
public void Enumerate_indexes_includes_pending_indexes_only_for_pending_requests()
43+
{
44+
var keyspace = new RedisMutationRequestKeyspace(new RedisMutationRequestStoreOptions
45+
{
46+
KeyPrefix = "mk:gov"
47+
});
48+
49+
var request = new MutationRequest
50+
{
51+
RequestId = "req-42",
52+
StateId = "tenant-42",
53+
StateType = "IamRoleState",
54+
MutationType = "GrantRoleMutation",
55+
Status = MutationRequestStatus.Pending,
56+
PendingReason = PendingMutationReason.Approval
57+
};
58+
59+
var keys = keyspace.EnumerateIndexes(request).Select(key => key.ToString()).ToArray();
60+
61+
Assert.Contains("mk:gov:requests:ids", keys);
62+
Assert.Contains("mk:gov:states:tenant-42:requests", keys);
63+
Assert.Contains("mk:gov:status:pending:requests", keys);
64+
Assert.Contains("mk:gov:pending:requests", keys);
65+
Assert.Contains("mk:gov:pending:approval:requests", keys);
66+
}
67+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
12+
<PackageReference Include="xunit" Version="2.9.3" />
13+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
<PrivateAssets>all</PrivateAssets>
16+
</PackageReference>
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\..\src\Redis\ModularityKit.Mutator.Governance.Redis.csproj" />
21+
<ProjectReference Include="..\..\src\ModularityKit.Mutator.Governance.csproj" />
22+
</ItemGroup>
23+
24+
</Project>

src/ModularityKit.Mutator.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
<ItemGroup>
2424
<Compile Remove="Governance/**/*.cs" />
25+
<Compile Remove="Redis/**/*.cs" />
2526
</ItemGroup>
2627

2728
<ItemGroup>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace ModularityKit.Mutator.Governance.Redis.Configuration;
2+
3+
/// <summary>
4+
/// Configuration for Redis-backed governance request storage.
5+
/// </summary>
6+
public sealed class RedisMutationRequestStoreOptions
7+
{
8+
/// <summary>
9+
/// Key prefix used by the provider.
10+
/// </summary>
11+
public string KeyPrefix { get; set; } = "modularitykit:governance";
12+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
2+
using ModularityKit.Mutator.Governance.Abstractions.Requests.Model;
3+
using ModularityKit.Mutator.Governance.Redis.Configuration;
4+
using StackExchange.Redis;
5+
6+
namespace ModularityKit.Mutator.Governance.Redis.Keys;
7+
8+
/// <summary>
9+
/// Centralizes Redis key naming for governed mutation requests.
10+
/// </summary>
11+
public sealed class RedisMutationRequestKeyspace
12+
{
13+
private readonly string _keyPrefix;
14+
15+
/// <summary>
16+
/// Initializes a new keyspace instance for the configured Redis prefix.
17+
/// </summary>
18+
/// <param name="options">The Redis provider options.</param>
19+
public RedisMutationRequestKeyspace(RedisMutationRequestStoreOptions options)
20+
{
21+
ArgumentNullException.ThrowIfNull(options);
22+
23+
if (string.IsNullOrWhiteSpace(options.KeyPrefix))
24+
throw new ArgumentException("Redis key prefix cannot be empty.", nameof(options));
25+
26+
_keyPrefix = options.KeyPrefix;
27+
}
28+
29+
/// <summary>
30+
/// Gets the Redis key that stores all known request identifiers.
31+
/// </summary>
32+
/// <returns>The Redis key for the global request-id set.</returns>
33+
public RedisKey RequestIds() => $"{_keyPrefix}:requests:ids";
34+
35+
/// <summary>
36+
/// Gets the Redis key that stores the serialized document for a request.
37+
/// </summary>
38+
/// <param name="requestId">The request identifier.</param>
39+
/// <returns>The Redis key for the request document.</returns>
40+
public RedisKey RequestData(string requestId) => $"{_keyPrefix}:requests:{requestId}:data";
41+
42+
/// <summary>
43+
/// Gets the Redis key that stores the optimistic-concurrency revision for a request.
44+
/// </summary>
45+
/// <param name="requestId">The request identifier.</param>
46+
/// <returns>The Redis key for the request revision.</returns>
47+
public RedisKey RequestRevision(string requestId) => $"{_keyPrefix}:requests:{requestId}:revision";
48+
49+
/// <summary>
50+
/// Gets the Redis key for requests grouped by state identifier.
51+
/// </summary>
52+
/// <param name="stateId">The state identifier.</param>
53+
/// <returns>The Redis key for requests targeting the supplied state.</returns>
54+
public RedisKey RequestsByStateId(string stateId) => $"{_keyPrefix}:states:{stateId}:requests";
55+
56+
/// <summary>
57+
/// Gets the Redis key for requests grouped by governance status.
58+
/// </summary>
59+
/// <param name="status">The request status.</param>
60+
/// <returns>The Redis key for requests in the supplied status.</returns>
61+
public RedisKey RequestsByStatus(MutationRequestStatus status)
62+
=> $"{_keyPrefix}:status:{status.ToString().ToLowerInvariant()}:requests";
63+
64+
/// <summary>
65+
/// Gets the Redis key for all pending requests.
66+
/// </summary>
67+
/// <returns>The Redis key for the global pending-request set.</returns>
68+
public RedisKey PendingRequestIds() => $"{_keyPrefix}:pending:requests";
69+
70+
/// <summary>
71+
/// Gets the Redis key for pending requests grouped by pending reason.
72+
/// </summary>
73+
/// <param name="reason">The pending reason.</param>
74+
/// <returns>The Redis key for the pending-request set of the supplied reason.</returns>
75+
public RedisKey PendingRequestIds(PendingMutationReason reason)
76+
=> $"{_keyPrefix}:pending:{reason.ToString().ToLowerInvariant()}:requests";
77+
78+
/// <summary>
79+
/// Enumerates the secondary-index keys that should contain the supplied request.
80+
/// </summary>
81+
/// <param name="request">The request to index.</param>
82+
/// <returns>The Redis keys representing all indexes for the request.</returns>
83+
internal IReadOnlyList<RedisKey> EnumerateIndexes(MutationRequest request)
84+
{
85+
ArgumentNullException.ThrowIfNull(request);
86+
87+
var keys = new List<RedisKey>
88+
{
89+
RequestIds(),
90+
RequestsByStateId(request.StateId),
91+
RequestsByStatus(request.Status)
92+
};
93+
94+
if (request.Status == MutationRequestStatus.Pending)
95+
{
96+
keys.Add(PendingRequestIds());
97+
98+
if (request.PendingReason.HasValue)
99+
keys.Add(PendingRequestIds(request.PendingReason.Value));
100+
}
101+
102+
return keys;
103+
}
104+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<PackageId>ModularityKit.Mutator.Governance.Redis</PackageId>
8+
<Version>0.1.0</Version>
9+
<Authors>ModularityKit</Authors>
10+
<Company>ModularityKit</Company>
11+
<Description>Redis provider for ModularityKit.Mutator.Governance.</Description>
12+
<PackageProjectUrl>https://github.com/ModularityKit/ModularityKit.Mutator</PackageProjectUrl>
13+
<RepositoryUrl>https://github.com/ModularityKit/ModularityKit.Mutator.git</RepositoryUrl>
14+
<RepositoryType>git</RepositoryType>
15+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
16+
<PackageTags>governance;redis;requests;query;provider</PackageTags>
17+
<PackageReadmeFile>README.md</PackageReadmeFile>
18+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
19+
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
20+
</PropertyGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\ModularityKit.Mutator.Governance.csproj" />
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
28+
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
29+
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
30+
</ItemGroup>
31+
32+
<ItemGroup>
33+
<None Include="README.md" Pack="true" PackagePath="" Visible="false" />
34+
</ItemGroup>
35+
36+
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("ModularityKit.Mutator.Governance.Redis.Tests")]

0 commit comments

Comments
 (0)