Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NuGet.Versioning;
|
||||
using StellaOps.Concelier.Normalization.Identifiers;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Common.Packages;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helpers for working with Package URLs and SemVer coordinates inside connectors.
|
||||
/// </summary>
|
||||
public static class PackageCoordinateHelper
|
||||
{
|
||||
public static bool TryParsePackageUrl(string? value, out PackageCoordinates? coordinates)
|
||||
{
|
||||
coordinates = null;
|
||||
if (!IdentifierNormalizer.TryNormalizePackageUrl(value, out var canonical, out var packageUrl) || packageUrl is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var namespaceSegments = packageUrl.NamespaceSegments.ToArray();
|
||||
var subpathSegments = packageUrl.SubpathSegments.ToArray();
|
||||
var qualifiers = packageUrl.Qualifiers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase);
|
||||
var canonicalRebuilt = BuildPackageUrl(
|
||||
packageUrl.Type,
|
||||
namespaceSegments,
|
||||
packageUrl.Name,
|
||||
packageUrl.Version,
|
||||
qualifiers,
|
||||
subpathSegments);
|
||||
|
||||
coordinates = new PackageCoordinates(
|
||||
Canonical: canonicalRebuilt,
|
||||
Type: packageUrl.Type,
|
||||
NamespaceSegments: namespaceSegments,
|
||||
Name: packageUrl.Name,
|
||||
Version: packageUrl.Version,
|
||||
Qualifiers: qualifiers,
|
||||
SubpathSegments: subpathSegments,
|
||||
Original: packageUrl.Original);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PackageCoordinates ParsePackageUrl(string value)
|
||||
{
|
||||
if (!TryParsePackageUrl(value, out var coordinates) || coordinates is null)
|
||||
{
|
||||
throw new FormatException($"Value '{value}' is not a valid Package URL");
|
||||
}
|
||||
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
public static bool TryParseSemVer(string? value, out SemanticVersion? version, out string? normalized)
|
||||
{
|
||||
version = null;
|
||||
normalized = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParse(value.Trim(), out var parsed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
version = parsed;
|
||||
normalized = parsed.ToNormalizedString();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseSemVerRange(string? value, out VersionRange? range)
|
||||
{
|
||||
range = null;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (trimmed.StartsWith("^", StringComparison.Ordinal))
|
||||
{
|
||||
var baseSegment = trimmed[1..];
|
||||
if (!SemanticVersion.TryParse(baseSegment, out var baseVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var upperBound = CalculateCaretUpperBound(baseVersion);
|
||||
var caretExpression = $"[{baseVersion.ToNormalizedString()}, {upperBound.ToNormalizedString()})";
|
||||
if (VersionRange.TryParse(caretExpression, out var caretRange))
|
||||
{
|
||||
range = caretRange;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VersionRange.TryParse(trimmed, out var parsed))
|
||||
{
|
||||
try
|
||||
{
|
||||
parsed = VersionRange.Parse(trimmed);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
range = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string BuildPackageUrl(
|
||||
string type,
|
||||
IReadOnlyList<string>? namespaceSegments,
|
||||
string name,
|
||||
string? version = null,
|
||||
IReadOnlyDictionary<string, string>? qualifiers = null,
|
||||
IReadOnlyList<string>? subpathSegments = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(type);
|
||||
ArgumentException.ThrowIfNullOrEmpty(name);
|
||||
|
||||
var builder = new StringBuilder("pkg:");
|
||||
builder.Append(type.Trim().ToLowerInvariant());
|
||||
builder.Append('/');
|
||||
|
||||
if (namespaceSegments is not null && namespaceSegments.Count > 0)
|
||||
{
|
||||
builder.Append(string.Join('/', namespaceSegments.Select(NormalizeSegment)));
|
||||
builder.Append('/');
|
||||
}
|
||||
|
||||
builder.Append(NormalizeSegment(name));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
builder.Append('@');
|
||||
builder.Append(version.Trim());
|
||||
}
|
||||
|
||||
if (qualifiers is not null && qualifiers.Count > 0)
|
||||
{
|
||||
builder.Append('?');
|
||||
builder.Append(string.Join('&', qualifiers
|
||||
.OrderBy(static kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(kvp => $"{NormalizeSegment(kvp.Key)}={NormalizeSegment(kvp.Value)}")));
|
||||
}
|
||||
|
||||
if (subpathSegments is not null && subpathSegments.Count > 0)
|
||||
{
|
||||
builder.Append('#');
|
||||
builder.Append(string.Join('/', subpathSegments.Select(NormalizeSegment)));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string NormalizeSegment(string value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
var trimmed = value.Trim();
|
||||
var unescaped = Uri.UnescapeDataString(trimmed);
|
||||
var encoded = Uri.EscapeDataString(unescaped);
|
||||
return encoded.Replace("%40", "@");
|
||||
}
|
||||
|
||||
private static SemanticVersion CalculateCaretUpperBound(SemanticVersion baseVersion)
|
||||
{
|
||||
if (baseVersion.Major > 0)
|
||||
{
|
||||
return new SemanticVersion(baseVersion.Major + 1, 0, 0);
|
||||
}
|
||||
|
||||
if (baseVersion.Minor > 0)
|
||||
{
|
||||
return new SemanticVersion(0, baseVersion.Minor + 1, 0);
|
||||
}
|
||||
|
||||
return new SemanticVersion(0, 0, baseVersion.Patch + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record PackageCoordinates(
|
||||
string Canonical,
|
||||
string Type,
|
||||
IReadOnlyList<string> NamespaceSegments,
|
||||
string Name,
|
||||
string? Version,
|
||||
IReadOnlyDictionary<string, string> Qualifiers,
|
||||
IReadOnlyList<string> SubpathSegments,
|
||||
string Original);
|
||||
Reference in New Issue
Block a user