// SPDX-FileCopyrightText: 2026 Progmasoft // SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 namespace Progmasoft.Progmaweb.Api.ViGet; /// One published version of a Tazel plugin. /// The version. /// The SHA-256 digest of its JAR, in lower-case hex. /// The plugin API of Tazel it was built for. /// The size of its JAR in bytes. /// When it was published. /// Whether it was yanked: Tazel takes it only when a project names it. internal sealed record TazelPluginVersion( string Version, string Sha256, int ApiVersion, long Size, DateTimeOffset PublishedAt, bool Yanked); /// A Tazel plugin of the catalog, described by the descriptor of its newest publication. /// The Progmasoft Account it is published under. /// Its name. /// What it does. /// Its authors. /// An SPDX license expression. /// An https address of the plugin. /// An https address of its sources. /// Its versions, newest publication first. internal sealed record TazelPlugin( string Publisher, string Name, string? Description, IReadOnlyList Authors, string? License, string? Homepage, string? Repository, IReadOnlyList Versions) { /// The newest version that is not yanked, or the newest of all when every one is. public TazelPluginVersion Latest => Versions.Where(version => !version.Yanked).DefaultIfEmpty(Versions[0]).First(); } /// What adding a version to the catalog did. internal enum AddVersionResult { /// The version was added. Added, /// The plugin has this version already; a version is never replaced. VersionExists, /// A plugin of the publisher has the same name in other letter case. NameTaken, } /// The Tazel plugins of ViGet: their descriptions and versions. The JARs live in files beside it. internal interface ITazelPluginCatalog { /// Every plugin, ordered by publisher and name. ValueTask> ListAsync(CancellationToken cancellationToken); /// A plugin by its exact publisher and name. ValueTask FindAsync(string publisher, string name, CancellationToken cancellationToken); /// Adds a version, and describes the plugin with the descriptor of that version. ValueTask AddVersionAsync( TazelPluginDescriptor descriptor, string sha256, long size, DateTimeOffset publishedAt, CancellationToken cancellationToken); /// Yanks a version, or takes the yank back. /// Whether the plugin has the version. ValueTask SetYankedAsync( string publisher, string name, string version, bool yanked, CancellationToken cancellationToken); } /// A catalog that lives as long as the process, for development and tests. internal sealed class InMemoryTazelPluginCatalog : ITazelPluginCatalog { /// Guards . private readonly Lock gate = new(); /// The plugins by their exact publisher and name. private readonly Dictionary<(string Publisher, string Name), TazelPlugin> plugins = []; /// public ValueTask> ListAsync(CancellationToken cancellationToken) { lock (gate) { return ValueTask.FromResult>(plugins.Values .OrderBy(plugin => plugin.Publisher, StringComparer.Ordinal) .ThenBy(plugin => plugin.Name, StringComparer.Ordinal) .ToList()); } } /// public ValueTask FindAsync(string publisher, string name, CancellationToken cancellationToken) { lock (gate) { return ValueTask.FromResult(plugins.GetValueOrDefault((publisher, name))); } } /// public ValueTask AddVersionAsync( TazelPluginDescriptor descriptor, string sha256, long size, DateTimeOffset publishedAt, CancellationToken cancellationToken) { lock (gate) { (string, string) key = (descriptor.Publisher, descriptor.Name); if (!plugins.ContainsKey(key) && plugins.Keys.Any(existing => string.Equals(existing.Publisher, descriptor.Publisher, StringComparison.OrdinalIgnoreCase) && string.Equals(existing.Name, descriptor.Name, StringComparison.OrdinalIgnoreCase))) { return ValueTask.FromResult(AddVersionResult.NameTaken); } List versions = plugins.TryGetValue(key, out TazelPlugin? existingPlugin) ? [.. existingPlugin.Versions] : []; if (versions.Any(version => version.Version == descriptor.Version)) { return ValueTask.FromResult(AddVersionResult.VersionExists); } versions.Insert( 0, new TazelPluginVersion(descriptor.Version, sha256, descriptor.ApiVersion, size, publishedAt, false)); plugins[key] = new TazelPlugin( descriptor.Publisher, descriptor.Name, descriptor.Description, descriptor.Authors, descriptor.License, descriptor.Homepage, descriptor.Repository, versions); return ValueTask.FromResult(AddVersionResult.Added); } } /// public ValueTask SetYankedAsync( string publisher, string name, string version, bool yanked, CancellationToken cancellationToken) { lock (gate) { if (!plugins.TryGetValue((publisher, name), out TazelPlugin? plugin) || plugin.Versions.All(existing => existing.Version != version)) { return ValueTask.FromResult(false); } plugins[(publisher, name)] = plugin with { Versions = [.. plugin.Versions.Select(existing => existing.Version == version ? existing with { Yanked = yanked } : existing)] }; return ValueTask.FromResult(true); } } }