Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps/api/ViGet/TazelPluginCatalog.cs
blob: ea7ff7a40127ea4634fd70052e4beea37b733dfb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1

namespace Progmasoft.Progmaweb.Api.ViGet;

/// <summary>One published version of a Tazel plugin.</summary>
/// <param name="Version">The version.</param>
/// <param name="Sha256">The SHA-256 digest of its JAR, in lower-case hex.</param>
/// <param name="ApiVersion">The plugin API of Tazel it was built for.</param>
/// <param name="Size">The size of its JAR in bytes.</param>
/// <param name="PublishedAt">When it was published.</param>
/// <param name="Yanked">Whether it was yanked: Tazel takes it only when a project names it.</param>
internal sealed record TazelPluginVersion(
    string Version,
    string Sha256,
    int ApiVersion,
    long Size,
    DateTimeOffset PublishedAt,
    bool Yanked);

/// <summary>A Tazel plugin of the catalog, described by the descriptor of its newest publication.</summary>
/// <param name="Publisher">The Progmasoft Account it is published under.</param>
/// <param name="Name">Its name.</param>
/// <param name="Description">What it does.</param>
/// <param name="Authors">Its authors.</param>
/// <param name="License">An SPDX license expression.</param>
/// <param name="Homepage">An https address of the plugin.</param>
/// <param name="Repository">An https address of its sources.</param>
/// <param name="Versions">Its versions, newest publication first.</param>
internal sealed record TazelPlugin(
    string Publisher,
    string Name,
    string? Description,
    IReadOnlyList<string> Authors,
    string? License,
    string? Homepage,
    string? Repository,
    IReadOnlyList<TazelPluginVersion> Versions)
{
    /// <summary>The newest version that is not yanked, or the newest of all when every one is.</summary>
    public TazelPluginVersion Latest =>
        Versions.Where(version => !version.Yanked).DefaultIfEmpty(Versions[0]).First();
}

/// <summary>What adding a version to the catalog did.</summary>
internal enum AddVersionResult
{
    /// <summary>The version was added.</summary>
    Added,

    /// <summary>The plugin has this version already; a version is never replaced.</summary>
    VersionExists,

    /// <summary>A plugin of the publisher has the same name in other letter case.</summary>
    NameTaken,
}

/// <summary>The Tazel plugins of ViGet: their descriptions and versions. The JARs live in files beside it.</summary>
internal interface ITazelPluginCatalog
{
    /// <summary>Every plugin, ordered by publisher and name.</summary>
    ValueTask<IReadOnlyList<TazelPlugin>> ListAsync(CancellationToken cancellationToken);

    /// <summary>A plugin by its exact publisher and name.</summary>
    ValueTask<TazelPlugin?> FindAsync(string publisher, string name, CancellationToken cancellationToken);

    /// <summary>Adds a version, and describes the plugin with the descriptor of that version.</summary>
    ValueTask<AddVersionResult> AddVersionAsync(
        TazelPluginDescriptor descriptor,
        string sha256,
        long size,
        DateTimeOffset publishedAt,
        CancellationToken cancellationToken);

    /// <summary>Yanks a version, or takes the yank back.</summary>
    /// <returns>Whether the plugin has the version.</returns>
    ValueTask<bool> SetYankedAsync(
        string publisher,
        string name,
        string version,
        bool yanked,
        CancellationToken cancellationToken);
}

/// <summary>A catalog that lives as long as the process, for development and tests.</summary>
internal sealed class InMemoryTazelPluginCatalog : ITazelPluginCatalog
{
    /// <summary>Guards <see cref="plugins"/>.</summary>
    private readonly Lock gate = new();
    /// <summary>The plugins by their exact publisher and name.</summary>
    private readonly Dictionary<(string Publisher, string Name), TazelPlugin> plugins = [];

    /// <inheritdoc/>
    public ValueTask<IReadOnlyList<TazelPlugin>> ListAsync(CancellationToken cancellationToken)
    {
        lock (gate)
        {
            return ValueTask.FromResult<IReadOnlyList<TazelPlugin>>(plugins.Values
                .OrderBy(plugin => plugin.Publisher, StringComparer.Ordinal)
                .ThenBy(plugin => plugin.Name, StringComparer.Ordinal)
                .ToList());
        }
    }

    /// <inheritdoc/>
    public ValueTask<TazelPlugin?> FindAsync(string publisher, string name, CancellationToken cancellationToken)
    {
        lock (gate)
        {
            return ValueTask.FromResult(plugins.GetValueOrDefault((publisher, name)));
        }
    }

    /// <inheritdoc/>
    public ValueTask<AddVersionResult> 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<TazelPluginVersion> 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);
        }
    }

    /// <inheritdoc/>
    public ValueTask<bool> 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);
        }
    }
}