feat(config): add FileConfig model and JSON loader
This commit is contained in:
41
src/ClaudeMailbox/Config/FileConfig.cs
Normal file
41
src/ClaudeMailbox/Config/FileConfig.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ClaudeMailbox.Config;
|
||||||
|
|
||||||
|
public sealed class FileConfig
|
||||||
|
{
|
||||||
|
[JsonPropertyName("port")]
|
||||||
|
public int? Port { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("bind")]
|
||||||
|
public string? Bind { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("dbPath")]
|
||||||
|
public string? DbPath { get; set; }
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions Options = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static FileConfig Load(string? explicitPath, string? defaultPath)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(explicitPath))
|
||||||
|
{
|
||||||
|
if (!File.Exists(explicitPath))
|
||||||
|
throw new FileNotFoundException($"Config file not found: {explicitPath}", explicitPath);
|
||||||
|
return Parse(File.ReadAllText(explicitPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(defaultPath) && File.Exists(defaultPath))
|
||||||
|
return Parse(File.ReadAllText(defaultPath));
|
||||||
|
|
||||||
|
return new FileConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FileConfig Parse(string json)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<FileConfig>(json, Options) ?? new FileConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
99
tests/ClaudeMailbox.Tests/Config/FileConfigTests.cs
Normal file
99
tests/ClaudeMailbox.Tests/Config/FileConfigTests.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
using ClaudeMailbox.Config;
|
||||||
|
|
||||||
|
namespace ClaudeMailbox.Tests.Config;
|
||||||
|
|
||||||
|
public sealed class FileConfigTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Load_ReturnsEmpty_WhenPathIsNullAndDefaultMissing()
|
||||||
|
{
|
||||||
|
var missing = Path.Combine(Path.GetTempPath(), $"nope-{Guid.NewGuid():N}.json");
|
||||||
|
var cfg = FileConfig.Load(explicitPath: null, defaultPath: missing);
|
||||||
|
|
||||||
|
Assert.Null(cfg.Port);
|
||||||
|
Assert.Null(cfg.Bind);
|
||||||
|
Assert.Null(cfg.DbPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_ReadsDefaultPath_WhenExplicitPathNull()
|
||||||
|
{
|
||||||
|
var path = WriteTemp("""{"port":9000,"bind":"0.0.0.0","dbPath":"C:\\tmp\\a.db"}""");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cfg = FileConfig.Load(explicitPath: null, defaultPath: path);
|
||||||
|
Assert.Equal(9000, cfg.Port);
|
||||||
|
Assert.Equal("0.0.0.0", cfg.Bind);
|
||||||
|
Assert.Equal(@"C:\tmp\a.db", cfg.DbPath);
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_ExplicitPath_WinsOverDefault()
|
||||||
|
{
|
||||||
|
var defaultPath = WriteTemp("""{"port":1111}""");
|
||||||
|
var explicitPath = WriteTemp("""{"port":2222}""");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cfg = FileConfig.Load(explicitPath: explicitPath, defaultPath: defaultPath);
|
||||||
|
Assert.Equal(2222, cfg.Port);
|
||||||
|
}
|
||||||
|
finally { File.Delete(defaultPath); File.Delete(explicitPath); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_ExplicitPathMissing_Throws()
|
||||||
|
{
|
||||||
|
var missing = Path.Combine(Path.GetTempPath(), $"nope-{Guid.NewGuid():N}.json");
|
||||||
|
var ex = Assert.Throws<FileNotFoundException>(() =>
|
||||||
|
FileConfig.Load(explicitPath: missing, defaultPath: null));
|
||||||
|
Assert.Contains(missing, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_MissingFields_AreNull()
|
||||||
|
{
|
||||||
|
var path = WriteTemp("""{"port":1234}""");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cfg = FileConfig.Load(explicitPath: path, defaultPath: null);
|
||||||
|
Assert.Equal(1234, cfg.Port);
|
||||||
|
Assert.Null(cfg.Bind);
|
||||||
|
Assert.Null(cfg.DbPath);
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_CaseInsensitive_PropertyNames()
|
||||||
|
{
|
||||||
|
var path = WriteTemp("""{"Port":1,"BIND":"x","DBPATH":"y"}""");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cfg = FileConfig.Load(explicitPath: path, defaultPath: null);
|
||||||
|
Assert.Equal(1, cfg.Port);
|
||||||
|
Assert.Equal("x", cfg.Bind);
|
||||||
|
Assert.Equal("y", cfg.DbPath);
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_MalformedJson_Throws()
|
||||||
|
{
|
||||||
|
var path = WriteTemp("not json");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.ThrowsAny<Exception>(() => FileConfig.Load(explicitPath: path, defaultPath: null));
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string WriteTemp(string content)
|
||||||
|
{
|
||||||
|
var p = Path.Combine(Path.GetTempPath(), $"mailbox-{Guid.NewGuid():N}.json");
|
||||||
|
File.WriteAllText(p, content);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user