diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Flavours/FlavourLoader.cs b/windows/welcome/src/SilverOS.Welcome.Core/Flavours/FlavourLoader.cs new file mode 100644 index 0000000..ca023c1 --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Flavours/FlavourLoader.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +namespace SilverOS.Welcome.Core.Flavours; + +public sealed class FlavourValidationException(string message) : Exception(message); + +public sealed class FlavourLoader : IFlavourLoader +{ + public IReadOnlyList Load(string directory) + { + var list = new List(); + foreach (var file in Directory.EnumerateFiles(directory, "*.json").OrderBy(f => f)) + { + FlavourManifest m; + try { m = JsonSerializer.Deserialize(File.ReadAllText(file), FlavourManifest.JsonOptions) + ?? throw new FlavourValidationException($"{Path.GetFileName(file)}: empty"); } + catch (JsonException ex) { throw new FlavourValidationException($"{Path.GetFileName(file)}: {ex.Message}"); } + if (string.IsNullOrWhiteSpace(m.Id)) throw new FlavourValidationException($"{Path.GetFileName(file)}: missing id"); + if (m.Hardening.Modules.Count == 0) throw new FlavourValidationException($"{Path.GetFileName(file)}: no hardening modules"); + list.Add(m); + } + if (list.Count == 0) throw new FlavourValidationException("no flavour manifests found"); + if (list.Count(m => m.IsDefault) != 1) throw new FlavourValidationException("exactly one flavour must be isDefault"); + return list.OrderByDescending(m => m.IsDefault).ThenBy(m => m.Label).ToList(); + } +} diff --git a/windows/welcome/src/SilverOS.Welcome.Core/Flavours/IFlavourLoader.cs b/windows/welcome/src/SilverOS.Welcome.Core/Flavours/IFlavourLoader.cs new file mode 100644 index 0000000..69f9e60 --- /dev/null +++ b/windows/welcome/src/SilverOS.Welcome.Core/Flavours/IFlavourLoader.cs @@ -0,0 +1,2 @@ +namespace SilverOS.Welcome.Core.Flavours; +public interface IFlavourLoader { IReadOnlyList Load(string directory); } diff --git a/windows/welcome/tests/SilverOS.Welcome.Tests/FlavourLoaderTests.cs b/windows/welcome/tests/SilverOS.Welcome.Tests/FlavourLoaderTests.cs new file mode 100644 index 0000000..a4f6871 --- /dev/null +++ b/windows/welcome/tests/SilverOS.Welcome.Tests/FlavourLoaderTests.cs @@ -0,0 +1,38 @@ +using SilverOS.Welcome.Core.Flavours; +using Xunit; + +public class FlavourLoaderTests +{ + private static string WriteTemp(params (string name, string json)[] files) + { + var dir = Directory.CreateTempSubdirectory("flav").FullName; + foreach (var (name, json) in files) File.WriteAllText(Path.Combine(dir, name), json); + return dir; + } + + [Fact] + public void Loads_all_manifests_sorted_with_default_first() + { + var dir = WriteTemp( + ("privacy-max.json", """{ "id":"privacy-max","label":"Privacy-Max","hardening":{"modules":["00"]} }"""), + ("daily-driver.json", """{ "id":"daily-driver","label":"Daily-Driver","isDefault":true,"hardening":{"modules":["00"]} }""")); + var loaded = new FlavourLoader().Load(dir); + Assert.Equal(2, loaded.Count); + Assert.Equal("daily-driver", loaded[0].Id); // default first + } + + [Fact] + public void Throws_when_a_manifest_has_no_id() + { + var dir = WriteTemp(("bad.json", """{ "label":"No Id","hardening":{"modules":["00"]} }""")); + var ex = Assert.Throws(() => new FlavourLoader().Load(dir)); + Assert.Contains("bad.json", ex.Message); + } + + [Fact] + public void Throws_when_no_default_flavour_present() + { + var dir = WriteTemp(("a.json", """{ "id":"a","label":"A","hardening":{"modules":["00"]} }""")); + Assert.Throws(() => new FlavourLoader().Load(dir)); + } +}