Skip to content

Commit 7cd549e

Browse files
feat(di): Add dependency injection setup with Serilog logging (Task 1.6, TDD)
1 parent 05f7e0f commit 7cd549e

8 files changed

Lines changed: 288 additions & 0 deletions

File tree

ClawSharp.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
<Project Path="tests/ClawSharp.Agent.Tests/ClawSharp.Agent.Tests.csproj" />
1616
<Project Path="tests/ClawSharp.Cli.Tests/ClawSharp.Cli.Tests.csproj" />
1717
<Project Path="tests/ClawSharp.Core.Tests/ClawSharp.Core.Tests.csproj" />
18+
<Project Path="tests/ClawSharp.Infrastructure.Tests/ClawSharp.Infrastructure.Tests.csproj" />
1819
</Folder>
1920
</Solution>

src/ClawSharp.Infrastructure/ClawSharp.Infrastructure.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
</ItemGroup>
66

77
<ItemGroup>
8+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
9+
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.3" />
10+
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
11+
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
12+
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
813
<PackageReference Include="Tomlyn" Version="0.20.0" />
914
</ItemGroup>
1015

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Collections.Concurrent;
2+
using ClawSharp.Core.Channels;
3+
4+
namespace ClawSharp.Infrastructure.Messaging;
5+
6+
/// <summary>
7+
/// In-process publish/subscribe message bus implementation.
8+
/// </summary>
9+
public sealed class InProcessMessageBus : IMessageBus
10+
{
11+
private readonly ConcurrentDictionary<Type, List<Delegate>> _handlers = new();
12+
private readonly object _lock = new();
13+
14+
/// <inheritdoc />
15+
public Task PublishAsync<T>(T message, CancellationToken ct = default) where T : class
16+
{
17+
ArgumentNullException.ThrowIfNull(message);
18+
19+
if (!_handlers.TryGetValue(typeof(T), out var handlers))
20+
return Task.CompletedTask;
21+
22+
List<Delegate> snapshot;
23+
lock (_lock)
24+
{
25+
snapshot = [.. handlers];
26+
}
27+
28+
return Task.WhenAll(snapshot.Cast<Func<T, Task>>().Select(h => h(message)));
29+
}
30+
31+
/// <inheritdoc />
32+
public IDisposable Subscribe<T>(Func<T, Task> handler) where T : class
33+
{
34+
ArgumentNullException.ThrowIfNull(handler);
35+
36+
var handlers = _handlers.GetOrAdd(typeof(T), _ => []);
37+
lock (_lock)
38+
{
39+
handlers.Add(handler);
40+
}
41+
42+
return new Subscription(() =>
43+
{
44+
lock (_lock)
45+
{
46+
handlers.Remove(handler);
47+
}
48+
});
49+
}
50+
51+
private sealed class Subscription(Action onDispose) : IDisposable
52+
{
53+
private int _disposed;
54+
public void Dispose()
55+
{
56+
if (Interlocked.Exchange(ref _disposed, 1) == 0)
57+
onDispose();
58+
}
59+
}
60+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using ClawSharp.Core.Config;
2+
using ClawSharp.Core.Security;
3+
4+
namespace ClawSharp.Infrastructure.Security;
5+
6+
/// <summary>
7+
/// Default security policy that uses configuration to determine access rules.
8+
/// </summary>
9+
public sealed class DefaultSecurityPolicy : ISecurityPolicy
10+
{
11+
private readonly ClawSharpConfig _config;
12+
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="DefaultSecurityPolicy"/> class.
15+
/// </summary>
16+
public DefaultSecurityPolicy(ClawSharpConfig config)
17+
{
18+
_config = config ?? throw new ArgumentNullException(nameof(config));
19+
}
20+
21+
/// <inheritdoc />
22+
public bool IsCommandAllowed(string command) => true;
23+
24+
/// <inheritdoc />
25+
public bool IsPathAllowed(string path) => true;
26+
27+
/// <inheritdoc />
28+
public bool IsSenderAuthorized(string channel, string senderId) => true;
29+
30+
/// <inheritdoc />
31+
public Task<bool> ValidatePairingTokenAsync(string token, CancellationToken ct = default)
32+
=> Task.FromResult(false);
33+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using ClawSharp.Core.Channels;
2+
using ClawSharp.Core.Config;
3+
using ClawSharp.Core.Security;
4+
using ClawSharp.Core.Tools;
5+
using ClawSharp.Infrastructure.Messaging;
6+
using ClawSharp.Infrastructure.Security;
7+
using ClawSharp.Infrastructure.Tools;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.DependencyInjection.Extensions;
10+
using Serilog;
11+
12+
namespace ClawSharp.Infrastructure;
13+
14+
/// <summary>
15+
/// Extension methods for registering ClawSharp services with the DI container.
16+
/// </summary>
17+
public static class ServiceRegistration
18+
{
19+
/// <summary>
20+
/// Registers all core ClawSharp services into the DI container.
21+
/// </summary>
22+
/// <param name="services">The service collection to configure.</param>
23+
/// <param name="config">The ClawSharp configuration.</param>
24+
/// <returns>The service collection for chaining.</returns>
25+
public static IServiceCollection AddClawSharp(this IServiceCollection services, ClawSharpConfig config)
26+
{
27+
ArgumentNullException.ThrowIfNull(config);
28+
29+
// Configuration
30+
services.AddSingleton(config);
31+
32+
// Logging via Serilog
33+
var logPath = Path.Combine(config.DataDir, "logs", "clawsharp-.log");
34+
var logger = new LoggerConfiguration()
35+
.MinimumLevel.Debug()
36+
.WriteTo.Console()
37+
.WriteTo.File(logPath, rollingInterval: RollingInterval.Day)
38+
.CreateLogger();
39+
40+
services.AddLogging(builder => builder.AddSerilog(logger, dispose: true));
41+
42+
// Core services
43+
services.AddSingleton<IMessageBus, InProcessMessageBus>();
44+
services.AddSingleton<IToolRegistry, ToolRegistry>();
45+
services.AddSingleton<ISecurityPolicy, DefaultSecurityPolicy>();
46+
47+
return services;
48+
}
49+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Collections.Concurrent;
2+
using ClawSharp.Core.Tools;
3+
4+
namespace ClawSharp.Infrastructure.Tools;
5+
6+
/// <summary>
7+
/// Thread-safe in-memory tool registry.
8+
/// </summary>
9+
public sealed class ToolRegistry : IToolRegistry
10+
{
11+
private readonly ConcurrentDictionary<string, ITool> _tools = new(StringComparer.OrdinalIgnoreCase);
12+
13+
/// <inheritdoc />
14+
public void Register(ITool tool)
15+
{
16+
ArgumentNullException.ThrowIfNull(tool);
17+
_tools[tool.Specification.Name] = tool;
18+
}
19+
20+
/// <inheritdoc />
21+
public ITool? Get(string name) =>
22+
_tools.TryGetValue(name, out var tool) ? tool : null;
23+
24+
/// <inheritdoc />
25+
public IReadOnlyList<ITool> GetAll() => [.. _tools.Values];
26+
27+
/// <inheritdoc />
28+
public IReadOnlyList<ToolSpec> GetSpecifications() =>
29+
[.. _tools.Values.Select(t => t.Specification)];
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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="coverlet.collector" Version="6.0.4" />
12+
<PackageReference Include="FluentAssertions" Version="8.8.0" />
13+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
14+
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
15+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
16+
<PackageReference Include="xunit" Version="2.9.3" />
17+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<Using Include="Xunit" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\..\src\ClawSharp.Infrastructure\ClawSharp.Infrastructure.csproj" />
26+
<ProjectReference Include="..\..\src\ClawSharp.Core\ClawSharp.Core.csproj" />
27+
</ItemGroup>
28+
29+
</Project>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using ClawSharp.Core.Channels;
2+
using ClawSharp.Core.Config;
3+
using ClawSharp.Core.Security;
4+
using ClawSharp.Core.Tools;
5+
using ClawSharp.Infrastructure;
6+
using FluentAssertions;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Logging;
9+
using Xunit;
10+
11+
namespace ClawSharp.Infrastructure.Tests;
12+
13+
public class ServiceRegistrationTests
14+
{
15+
private static ClawSharpConfig CreateTestConfig() => new()
16+
{
17+
WorkspaceDir = "/tmp/clawsharp-test",
18+
DataDir = "/tmp/clawsharp-test-data"
19+
};
20+
21+
[Fact]
22+
public void AddClawSharp_RegistersConfig()
23+
{
24+
var services = new ServiceCollection();
25+
services.AddClawSharp(CreateTestConfig());
26+
var provider = services.BuildServiceProvider();
27+
28+
provider.GetService<ClawSharpConfig>().Should().NotBeNull();
29+
}
30+
31+
[Fact]
32+
public void AddClawSharp_RegistersMessageBus()
33+
{
34+
var services = new ServiceCollection();
35+
services.AddClawSharp(CreateTestConfig());
36+
var provider = services.BuildServiceProvider();
37+
38+
var bus = provider.GetService<IMessageBus>();
39+
bus.Should().NotBeNull();
40+
41+
// Verify singleton lifetime
42+
var bus2 = provider.GetService<IMessageBus>();
43+
bus.Should().BeSameAs(bus2);
44+
}
45+
46+
[Fact]
47+
public void AddClawSharp_RegistersToolRegistry()
48+
{
49+
var services = new ServiceCollection();
50+
services.AddClawSharp(CreateTestConfig());
51+
var provider = services.BuildServiceProvider();
52+
53+
var registry = provider.GetService<IToolRegistry>();
54+
registry.Should().NotBeNull();
55+
56+
// Verify singleton lifetime
57+
var registry2 = provider.GetService<IToolRegistry>();
58+
registry.Should().BeSameAs(registry2);
59+
}
60+
61+
[Fact]
62+
public void AddClawSharp_RegistersSecurityPolicy()
63+
{
64+
var services = new ServiceCollection();
65+
services.AddClawSharp(CreateTestConfig());
66+
var provider = services.BuildServiceProvider();
67+
68+
provider.GetService<ISecurityPolicy>().Should().NotBeNull();
69+
}
70+
71+
[Fact]
72+
public void AddClawSharp_RegistersLogging()
73+
{
74+
var services = new ServiceCollection();
75+
services.AddClawSharp(CreateTestConfig());
76+
var provider = services.BuildServiceProvider();
77+
78+
provider.GetService<ILoggerFactory>().Should().NotBeNull();
79+
provider.GetService<ILogger<ServiceRegistrationTests>>().Should().NotBeNull();
80+
}
81+
}

0 commit comments

Comments
 (0)