diff --git a/src/AcDream.Core/Placeholder.cs b/src/AcDream.Core/Placeholder.cs deleted file mode 100644 index d66c94e..0000000 --- a/src/AcDream.Core/Placeholder.cs +++ /dev/null @@ -1,3 +0,0 @@ -// src/AcDream.Core/Placeholder.cs -namespace AcDream.Core; -internal static class Placeholder { } diff --git a/src/AcDream.Core/Plugins/PluginManifest.cs b/src/AcDream.Core/Plugins/PluginManifest.cs new file mode 100644 index 0000000..446178c --- /dev/null +++ b/src/AcDream.Core/Plugins/PluginManifest.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AcDream.Core.Plugins; + +public sealed record PluginManifest( + string Id, + string DisplayName, + string Version, + string EntryDll, + int ApiVersion, + IReadOnlyList Dependencies) +{ + public static PluginManifest Parse(string json) + { + PluginManifestDto? dto; + try + { + dto = JsonSerializer.Deserialize(json, JsonOptions); + } + catch (JsonException ex) + { + throw new PluginManifestException($"invalid json: {ex.Message}", ex); + } + + if (dto is null) + throw new PluginManifestException("manifest is empty"); + + Require(dto.Id, nameof(dto.Id)); + Require(dto.DisplayName, nameof(dto.DisplayName)); + Require(dto.Version, nameof(dto.Version)); + Require(dto.EntryDll, nameof(dto.EntryDll)); + if (dto.ApiVersion <= 0) + throw new PluginManifestException("apiVersion must be >= 1"); + + return new PluginManifest( + dto.Id!, + dto.DisplayName!, + dto.Version!, + dto.EntryDll!, + dto.ApiVersion, + dto.Dependencies ?? Array.Empty()); + } + + private static void Require(string? value, string fieldName) + { + if (string.IsNullOrWhiteSpace(value)) + throw new PluginManifestException( + $"missing required field: {char.ToLowerInvariant(fieldName[0])}{fieldName[1..]}"); + } + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; + + private sealed class PluginManifestDto + { + public string? Id { get; set; } + public string? DisplayName { get; set; } + public string? Version { get; set; } + public string? EntryDll { get; set; } + public int ApiVersion { get; set; } + public IReadOnlyList? Dependencies { get; set; } + } +} + +public sealed class PluginManifestException : Exception +{ + public PluginManifestException(string message) : base(message) { } + public PluginManifestException(string message, Exception inner) : base(message, inner) { } +} diff --git a/tests/AcDream.Core.Tests/Plugins/PluginManifestTests.cs b/tests/AcDream.Core.Tests/Plugins/PluginManifestTests.cs new file mode 100644 index 0000000..180eeee --- /dev/null +++ b/tests/AcDream.Core.Tests/Plugins/PluginManifestTests.cs @@ -0,0 +1,62 @@ +using AcDream.Core.Plugins; + +namespace AcDream.Core.Tests.Plugins; + +public class PluginManifestTests +{ + [Fact] + public void Parse_ValidManifest_ReturnsManifest() + { + const string json = """ + { + "id": "acdream.smoke", + "displayName": "Smoke Test", + "version": "0.1.0", + "entryDll": "AcDream.Plugins.Smoke.dll", + "apiVersion": 1 + } + """; + + var manifest = PluginManifest.Parse(json); + + Assert.Equal("acdream.smoke", manifest.Id); + Assert.Equal("Smoke Test", manifest.DisplayName); + Assert.Equal("0.1.0", manifest.Version); + Assert.Equal("AcDream.Plugins.Smoke.dll", manifest.EntryDll); + Assert.Equal(1, manifest.ApiVersion); + } + + [Fact] + public void Parse_MissingRequiredField_Throws() + { + const string json = """ + { "id": "x", "version": "0.1.0", "entryDll": "x.dll", "apiVersion": 1 } + """; + + var ex = Assert.Throws(() => PluginManifest.Parse(json)); + Assert.Contains("displayName", ex.Message); + } + + [Fact] + public void Parse_MalformedJson_Throws() + { + Assert.Throws(() => PluginManifest.Parse("{ not json")); + } + + [Fact] + public void Parse_EmptyDependencies_DefaultsToEmptyList() + { + const string json = """ + { + "id": "x", + "displayName": "X", + "version": "0.1.0", + "entryDll": "x.dll", + "apiVersion": 1 + } + """; + + var manifest = PluginManifest.Parse(json); + Assert.Empty(manifest.Dependencies); + } +}