diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 3396476..353dd48 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -71,7 +71,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor. ## Phase 2 — Configuration binding -### [ ] 2.1 `MqttOptions`, `StorageOptions`, `UrlFetchOptions`, `ApiKeyOptions` +### [x] 2.1 `MqttOptions`, `StorageOptions`, `UrlFetchOptions`, `ApiKeyOptions` - POCOs in `src/FrameProcessor/Configuration/`. - Bound from `appsettings.json` via `builder.Services.Configure(...)`. - Validate on startup (`ValidateOnStart` + `IValidateOptions` or DataAnnotations). diff --git a/src/FrameProcessor/Configuration/ApiKeyOptions.cs b/src/FrameProcessor/Configuration/ApiKeyOptions.cs new file mode 100644 index 0000000..2b2d2a4 --- /dev/null +++ b/src/FrameProcessor/Configuration/ApiKeyOptions.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using FrameProcessor.Domain; + +namespace FrameProcessor.Configuration; + +/// +/// Wraps the shared API key from appsettings.json. The configured value is bound +/// from the top-level ApiKey key (which is a plain string in the JSON schema — +/// see SPEC.md §6.1) into ; consumers should call +/// to obtain the constant-time-comparing value type. +/// +public sealed class ApiKeyOptions +{ + public const string SectionName = "ApiKey"; + + [Required(AllowEmptyStrings = false)] + public string Value { get; set; } = string.Empty; + + public ApiKey ToApiKey() => new(Value); +} diff --git a/src/FrameProcessor/Configuration/MqttOptions.cs b/src/FrameProcessor/Configuration/MqttOptions.cs new file mode 100644 index 0000000..6c7c845 --- /dev/null +++ b/src/FrameProcessor/Configuration/MqttOptions.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; + +namespace FrameProcessor.Configuration; + +/// +/// Bound from the Mqtt section of appsettings.json. See SPEC.md §6.1. +/// +public sealed class MqttOptions +{ + public const string SectionName = "Mqtt"; + + [Required(AllowEmptyStrings = false)] + public string Host { get; set; } = string.Empty; + + [Range(1, 65535)] + public int Port { get; set; } = 1883; + + [Required(AllowEmptyStrings = false)] + public string ClientId { get; set; } = string.Empty; + + public string? Username { get; set; } + + public string? Password { get; set; } + + public bool UseTls { get; set; } + + [Required(AllowEmptyStrings = false)] + public string BaseTopic { get; set; } = "frames"; + + [Range(0, 2)] + public int PublishQos { get; set; } = 1; + + [MinLength(1)] + public int[] RetryBackoffSeconds { get; set; } = Array.Empty(); +} diff --git a/src/FrameProcessor/Configuration/StorageOptions.cs b/src/FrameProcessor/Configuration/StorageOptions.cs new file mode 100644 index 0000000..318bd10 --- /dev/null +++ b/src/FrameProcessor/Configuration/StorageOptions.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace FrameProcessor.Configuration; + +/// +/// Bound from the Storage section of appsettings.json. See SPEC.md §6.1, §7. +/// +public sealed class StorageOptions +{ + public const string SectionName = "Storage"; + + [Required(AllowEmptyStrings = false)] + public string ImageDirectory { get; set; } = string.Empty; +} diff --git a/src/FrameProcessor/Configuration/UrlFetchOptions.cs b/src/FrameProcessor/Configuration/UrlFetchOptions.cs new file mode 100644 index 0000000..19384d4 --- /dev/null +++ b/src/FrameProcessor/Configuration/UrlFetchOptions.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace FrameProcessor.Configuration; + +/// +/// Bound from the UrlFetch section of appsettings.json. See SPEC.md §6.1, §8. +/// +public sealed class UrlFetchOptions +{ + public const string SectionName = "UrlFetch"; + + [Range(1, long.MaxValue)] + public long MaxBytes { get; set; } = 52_428_800L; + + [Range(1, int.MaxValue)] + public int TimeoutSeconds { get; set; } = 30; + + [Range(0, int.MaxValue)] + public int MaxRedirects { get; set; } = 3; +} diff --git a/src/FrameProcessor/Program.cs b/src/FrameProcessor/Program.cs index dc7c950..4022b0d 100644 --- a/src/FrameProcessor/Program.cs +++ b/src/FrameProcessor/Program.cs @@ -1,7 +1,29 @@ +using FrameProcessor.Configuration; + var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(MqttOptions.SectionName)) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(StorageOptions.SectionName)) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(UrlFetchOptions.SectionName)) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddOptions() + .Configure((opts, cfg) => opts.Value = cfg[ApiKeyOptions.SectionName] ?? string.Empty) + .ValidateDataAnnotations() + .ValidateOnStart(); + var app = builder.Build(); app.MapControllers();