up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,160 +1,160 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
|
||||
|
||||
internal static class JavaServiceProviderScanner
|
||||
{
|
||||
public static JavaServiceProviderAnalysis Scan(JavaClassPathAnalysis classPath, JavaSpiCatalog catalog, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(classPath);
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
|
||||
var services = new Dictionary<string, ServiceAccumulator>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var segment in classPath.Segments.OrderBy(static s => s.Order))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var kvp in segment.ServiceDefinitions)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (kvp.Value.IsDefaultOrEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!services.TryGetValue(kvp.Key, out var accumulator))
|
||||
{
|
||||
accumulator = new ServiceAccumulator();
|
||||
services[kvp.Key] = accumulator;
|
||||
}
|
||||
|
||||
var providerIndex = 0;
|
||||
foreach (var provider in kvp.Value)
|
||||
{
|
||||
var normalizedProvider = provider?.Trim();
|
||||
if (string.IsNullOrEmpty(normalizedProvider))
|
||||
{
|
||||
providerIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
accumulator.AddCandidate(new JavaServiceProviderCandidateRecord(
|
||||
ProviderClass: normalizedProvider,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
SegmentOrder: segment.Order,
|
||||
ProviderIndex: providerIndex++,
|
||||
IsSelected: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var records = new List<JavaServiceProviderRecord>(services.Count);
|
||||
|
||||
foreach (var pair in services.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var descriptor = catalog.Resolve(pair.Key);
|
||||
var accumulator = pair.Value;
|
||||
var orderedCandidates = accumulator.GetOrderedCandidates();
|
||||
|
||||
if (orderedCandidates.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var selectedIndex = accumulator.DetermineSelection(orderedCandidates);
|
||||
var warnings = accumulator.BuildWarnings();
|
||||
|
||||
var candidateArray = ImmutableArray.CreateRange(orderedCandidates.Select((candidate, index) =>
|
||||
candidate with { IsSelected = index == selectedIndex }));
|
||||
|
||||
var warningsArray = warnings.Count == 0
|
||||
? ImmutableArray<string>.Empty
|
||||
: ImmutableArray.CreateRange(warnings);
|
||||
|
||||
records.Add(new JavaServiceProviderRecord(
|
||||
ServiceId: pair.Key,
|
||||
DisplayName: descriptor.DisplayName,
|
||||
Category: descriptor.Category,
|
||||
Candidates: candidateArray,
|
||||
SelectedIndex: selectedIndex,
|
||||
Warnings: warningsArray));
|
||||
}
|
||||
|
||||
return new JavaServiceProviderAnalysis(records.ToImmutableArray());
|
||||
}
|
||||
|
||||
private sealed class ServiceAccumulator
|
||||
{
|
||||
private readonly List<JavaServiceProviderCandidateRecord> _candidates = new();
|
||||
private readonly Dictionary<string, HashSet<string>> _providerSources = new(StringComparer.Ordinal);
|
||||
|
||||
public void AddCandidate(JavaServiceProviderCandidateRecord candidate)
|
||||
{
|
||||
_candidates.Add(candidate);
|
||||
|
||||
if (!_providerSources.TryGetValue(candidate.ProviderClass, out var sources))
|
||||
{
|
||||
sources = new HashSet<string>(StringComparer.Ordinal);
|
||||
_providerSources[candidate.ProviderClass] = sources;
|
||||
}
|
||||
|
||||
sources.Add(candidate.SegmentIdentifier);
|
||||
}
|
||||
|
||||
public IReadOnlyList<JavaServiceProviderCandidateRecord> GetOrderedCandidates()
|
||||
=> _candidates
|
||||
.OrderBy(static c => c.SegmentOrder)
|
||||
.ThenBy(static c => c.ProviderIndex)
|
||||
.ThenBy(static c => c.ProviderClass, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
public int DetermineSelection(IReadOnlyList<JavaServiceProviderCandidateRecord> orderedCandidates)
|
||||
=> orderedCandidates.Count == 0 ? -1 : 0;
|
||||
|
||||
public List<string> BuildWarnings()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
foreach (var pair in _providerSources.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
|
||||
{
|
||||
if (pair.Value.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var locations = pair.Value
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
warnings.Add($"duplicate-provider: {pair.Key} ({string.Join(", ", locations)})");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record JavaServiceProviderAnalysis(ImmutableArray<JavaServiceProviderRecord> Services)
|
||||
{
|
||||
public static readonly JavaServiceProviderAnalysis Empty = new(ImmutableArray<JavaServiceProviderRecord>.Empty);
|
||||
}
|
||||
|
||||
internal sealed record JavaServiceProviderRecord(
|
||||
string ServiceId,
|
||||
string DisplayName,
|
||||
string Category,
|
||||
ImmutableArray<JavaServiceProviderCandidateRecord> Candidates,
|
||||
int SelectedIndex,
|
||||
ImmutableArray<string> Warnings);
|
||||
|
||||
internal sealed record JavaServiceProviderCandidateRecord(
|
||||
string ProviderClass,
|
||||
string SegmentIdentifier,
|
||||
int SegmentOrder,
|
||||
int ProviderIndex,
|
||||
bool IsSelected);
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
|
||||
|
||||
internal static class JavaServiceProviderScanner
|
||||
{
|
||||
public static JavaServiceProviderAnalysis Scan(JavaClassPathAnalysis classPath, JavaSpiCatalog catalog, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(classPath);
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
|
||||
var services = new Dictionary<string, ServiceAccumulator>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var segment in classPath.Segments.OrderBy(static s => s.Order))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var kvp in segment.ServiceDefinitions)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (kvp.Value.IsDefaultOrEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!services.TryGetValue(kvp.Key, out var accumulator))
|
||||
{
|
||||
accumulator = new ServiceAccumulator();
|
||||
services[kvp.Key] = accumulator;
|
||||
}
|
||||
|
||||
var providerIndex = 0;
|
||||
foreach (var provider in kvp.Value)
|
||||
{
|
||||
var normalizedProvider = provider?.Trim();
|
||||
if (string.IsNullOrEmpty(normalizedProvider))
|
||||
{
|
||||
providerIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
accumulator.AddCandidate(new JavaServiceProviderCandidateRecord(
|
||||
ProviderClass: normalizedProvider,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
SegmentOrder: segment.Order,
|
||||
ProviderIndex: providerIndex++,
|
||||
IsSelected: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var records = new List<JavaServiceProviderRecord>(services.Count);
|
||||
|
||||
foreach (var pair in services.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var descriptor = catalog.Resolve(pair.Key);
|
||||
var accumulator = pair.Value;
|
||||
var orderedCandidates = accumulator.GetOrderedCandidates();
|
||||
|
||||
if (orderedCandidates.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var selectedIndex = accumulator.DetermineSelection(orderedCandidates);
|
||||
var warnings = accumulator.BuildWarnings();
|
||||
|
||||
var candidateArray = ImmutableArray.CreateRange(orderedCandidates.Select((candidate, index) =>
|
||||
candidate with { IsSelected = index == selectedIndex }));
|
||||
|
||||
var warningsArray = warnings.Count == 0
|
||||
? ImmutableArray<string>.Empty
|
||||
: ImmutableArray.CreateRange(warnings);
|
||||
|
||||
records.Add(new JavaServiceProviderRecord(
|
||||
ServiceId: pair.Key,
|
||||
DisplayName: descriptor.DisplayName,
|
||||
Category: descriptor.Category,
|
||||
Candidates: candidateArray,
|
||||
SelectedIndex: selectedIndex,
|
||||
Warnings: warningsArray));
|
||||
}
|
||||
|
||||
return new JavaServiceProviderAnalysis(records.ToImmutableArray());
|
||||
}
|
||||
|
||||
private sealed class ServiceAccumulator
|
||||
{
|
||||
private readonly List<JavaServiceProviderCandidateRecord> _candidates = new();
|
||||
private readonly Dictionary<string, HashSet<string>> _providerSources = new(StringComparer.Ordinal);
|
||||
|
||||
public void AddCandidate(JavaServiceProviderCandidateRecord candidate)
|
||||
{
|
||||
_candidates.Add(candidate);
|
||||
|
||||
if (!_providerSources.TryGetValue(candidate.ProviderClass, out var sources))
|
||||
{
|
||||
sources = new HashSet<string>(StringComparer.Ordinal);
|
||||
_providerSources[candidate.ProviderClass] = sources;
|
||||
}
|
||||
|
||||
sources.Add(candidate.SegmentIdentifier);
|
||||
}
|
||||
|
||||
public IReadOnlyList<JavaServiceProviderCandidateRecord> GetOrderedCandidates()
|
||||
=> _candidates
|
||||
.OrderBy(static c => c.SegmentOrder)
|
||||
.ThenBy(static c => c.ProviderIndex)
|
||||
.ThenBy(static c => c.ProviderClass, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
public int DetermineSelection(IReadOnlyList<JavaServiceProviderCandidateRecord> orderedCandidates)
|
||||
=> orderedCandidates.Count == 0 ? -1 : 0;
|
||||
|
||||
public List<string> BuildWarnings()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
foreach (var pair in _providerSources.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
|
||||
{
|
||||
if (pair.Value.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var locations = pair.Value
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
warnings.Add($"duplicate-provider: {pair.Key} ({string.Join(", ", locations)})");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record JavaServiceProviderAnalysis(ImmutableArray<JavaServiceProviderRecord> Services)
|
||||
{
|
||||
public static readonly JavaServiceProviderAnalysis Empty = new(ImmutableArray<JavaServiceProviderRecord>.Empty);
|
||||
}
|
||||
|
||||
internal sealed record JavaServiceProviderRecord(
|
||||
string ServiceId,
|
||||
string DisplayName,
|
||||
string Category,
|
||||
ImmutableArray<JavaServiceProviderCandidateRecord> Candidates,
|
||||
int SelectedIndex,
|
||||
ImmutableArray<string> Warnings);
|
||||
|
||||
internal sealed record JavaServiceProviderCandidateRecord(
|
||||
string ProviderClass,
|
||||
string SegmentIdentifier,
|
||||
int SegmentOrder,
|
||||
int ProviderIndex,
|
||||
bool IsSelected);
|
||||
|
||||
@@ -1,103 +1,103 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
|
||||
|
||||
internal sealed class JavaSpiCatalog
|
||||
{
|
||||
private static readonly Lazy<JavaSpiCatalog> LazyDefault = new(LoadDefaultCore);
|
||||
private readonly ImmutableDictionary<string, JavaSpiDescriptor> _descriptors;
|
||||
|
||||
private JavaSpiCatalog(ImmutableDictionary<string, JavaSpiDescriptor> descriptors)
|
||||
{
|
||||
_descriptors = descriptors;
|
||||
}
|
||||
|
||||
public static JavaSpiCatalog Default => LazyDefault.Value;
|
||||
|
||||
public JavaSpiDescriptor Resolve(string serviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serviceId))
|
||||
{
|
||||
return JavaSpiDescriptor.CreateUnknown(string.Empty);
|
||||
}
|
||||
|
||||
var key = serviceId.Trim();
|
||||
if (_descriptors.TryGetValue(key, out var descriptor))
|
||||
{
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return JavaSpiDescriptor.CreateUnknown(key);
|
||||
}
|
||||
|
||||
private static JavaSpiCatalog LoadDefaultCore()
|
||||
{
|
||||
var assembly = typeof(JavaSpiCatalog).GetTypeInfo().Assembly;
|
||||
var resourceName = "StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders.java-spi-catalog.json";
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException($"Embedded SPI catalog '{resourceName}' not found.");
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: false);
|
||||
var json = reader.ReadToEnd();
|
||||
|
||||
var items = JsonSerializer.Deserialize<List<JavaSpiDescriptor>>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
}) ?? new List<JavaSpiDescriptor>();
|
||||
|
||||
var descriptors = items
|
||||
.Select(Normalize)
|
||||
.Where(static item => !string.IsNullOrWhiteSpace(item.ServiceId))
|
||||
.ToImmutableDictionary(
|
||||
static item => item.ServiceId,
|
||||
static item => item,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
return new JavaSpiCatalog(descriptors);
|
||||
}
|
||||
|
||||
private static JavaSpiDescriptor Normalize(JavaSpiDescriptor descriptor)
|
||||
{
|
||||
var serviceId = descriptor.ServiceId?.Trim() ?? string.Empty;
|
||||
var category = string.IsNullOrWhiteSpace(descriptor.Category)
|
||||
? "unknown"
|
||||
: descriptor.Category.Trim().ToLowerInvariant();
|
||||
var displayName = string.IsNullOrWhiteSpace(descriptor.DisplayName)
|
||||
? serviceId
|
||||
: descriptor.DisplayName.Trim();
|
||||
|
||||
return descriptor with
|
||||
{
|
||||
ServiceId = serviceId,
|
||||
Category = category,
|
||||
DisplayName = displayName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record class JavaSpiDescriptor
|
||||
{
|
||||
[JsonPropertyName("serviceId")]
|
||||
public string ServiceId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("category")]
|
||||
public string Category { get; init; } = "unknown";
|
||||
|
||||
[JsonPropertyName("displayName")]
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("notes")]
|
||||
public string? Notes { get; init; }
|
||||
|
||||
public static JavaSpiDescriptor CreateUnknown(string serviceId)
|
||||
=> new()
|
||||
{
|
||||
ServiceId = serviceId,
|
||||
Category = "unknown",
|
||||
DisplayName = serviceId,
|
||||
};
|
||||
}
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
|
||||
|
||||
internal sealed class JavaSpiCatalog
|
||||
{
|
||||
private static readonly Lazy<JavaSpiCatalog> LazyDefault = new(LoadDefaultCore);
|
||||
private readonly ImmutableDictionary<string, JavaSpiDescriptor> _descriptors;
|
||||
|
||||
private JavaSpiCatalog(ImmutableDictionary<string, JavaSpiDescriptor> descriptors)
|
||||
{
|
||||
_descriptors = descriptors;
|
||||
}
|
||||
|
||||
public static JavaSpiCatalog Default => LazyDefault.Value;
|
||||
|
||||
public JavaSpiDescriptor Resolve(string serviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serviceId))
|
||||
{
|
||||
return JavaSpiDescriptor.CreateUnknown(string.Empty);
|
||||
}
|
||||
|
||||
var key = serviceId.Trim();
|
||||
if (_descriptors.TryGetValue(key, out var descriptor))
|
||||
{
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return JavaSpiDescriptor.CreateUnknown(key);
|
||||
}
|
||||
|
||||
private static JavaSpiCatalog LoadDefaultCore()
|
||||
{
|
||||
var assembly = typeof(JavaSpiCatalog).GetTypeInfo().Assembly;
|
||||
var resourceName = "StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders.java-spi-catalog.json";
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException($"Embedded SPI catalog '{resourceName}' not found.");
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: false);
|
||||
var json = reader.ReadToEnd();
|
||||
|
||||
var items = JsonSerializer.Deserialize<List<JavaSpiDescriptor>>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
}) ?? new List<JavaSpiDescriptor>();
|
||||
|
||||
var descriptors = items
|
||||
.Select(Normalize)
|
||||
.Where(static item => !string.IsNullOrWhiteSpace(item.ServiceId))
|
||||
.ToImmutableDictionary(
|
||||
static item => item.ServiceId,
|
||||
static item => item,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
return new JavaSpiCatalog(descriptors);
|
||||
}
|
||||
|
||||
private static JavaSpiDescriptor Normalize(JavaSpiDescriptor descriptor)
|
||||
{
|
||||
var serviceId = descriptor.ServiceId?.Trim() ?? string.Empty;
|
||||
var category = string.IsNullOrWhiteSpace(descriptor.Category)
|
||||
? "unknown"
|
||||
: descriptor.Category.Trim().ToLowerInvariant();
|
||||
var displayName = string.IsNullOrWhiteSpace(descriptor.DisplayName)
|
||||
? serviceId
|
||||
: descriptor.DisplayName.Trim();
|
||||
|
||||
return descriptor with
|
||||
{
|
||||
ServiceId = serviceId,
|
||||
Category = category,
|
||||
DisplayName = displayName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record class JavaSpiDescriptor
|
||||
{
|
||||
[JsonPropertyName("serviceId")]
|
||||
public string ServiceId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("category")]
|
||||
public string Category { get; init; } = "unknown";
|
||||
|
||||
[JsonPropertyName("displayName")]
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("notes")]
|
||||
public string? Notes { get; init; }
|
||||
|
||||
public static JavaSpiDescriptor CreateUnknown(string serviceId)
|
||||
=> new()
|
||||
{
|
||||
ServiceId = serviceId,
|
||||
Category = "unknown",
|
||||
DisplayName = serviceId,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user