Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gts-spec-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.14.5
v0.17.0
73 changes: 22 additions & 51 deletions Gts.Application/GtsHttpApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,20 +257,11 @@ public static WebApplication MapGtsApi(this WebApplication app, GtsRegistry regi
{
var id = gts_id ?? "";
var isWildcard = id.Contains('*', StringComparison.Ordinal);
if (isWildcard)
{
if (id.Count(c => c == '*') != 1 || !id.EndsWith('*'))
return Results.Json(new { id, valid = false, error = "Invalid wildcard pattern", is_wildcard = true });
var pr = GtsId.TryParsePattern(id, out var pat);
if (pr && pat is not null)
return Results.Json(new { id, valid = true, error = "", is_wildcard = true });
return Results.Json(new { id, valid = false, error = "Invalid wildcard pattern", is_wildcard = true });
}

if (GtsId.TryParse(id, out var parsed) && parsed is not null && (parsed.IsType || parsed.Segments.Count > 1))
return Results.Json(new { id, valid = true, error = "", is_wildcard = false });

return Results.Json(new { id, valid = false, error = "Invalid GTS id", is_wildcard = false });
// The §10 engine enforces syntax, spelling, the 1024-char limit, unsigned 32-bit version
// bounds, and the issue-#37 single-segment-instance rule for both concrete ids and patterns.
if (GtsPattern.TryParse(id, out _))
return Results.Json(new { id, valid = true, error = "", is_wildcard = isWildcard });
return Results.Json(new { id, valid = false, error = isWildcard ? "Invalid wildcard pattern" : "Invalid GTS id", is_wildcard = isWildcard });
});

app.MapPost("/extract-id", async (HttpRequest req) =>
Expand All @@ -293,60 +284,40 @@ public static WebApplication MapGtsApi(this WebApplication app, GtsRegistry regi
{
var id = gts_id ?? "";
var isWildcard = id.Contains('*', StringComparison.Ordinal);
if (isWildcard)
{
if (id.Count(c => c == '*') == 1 && id.EndsWith('*') && GtsId.TryParsePattern(id, out var pat) && pat is not null)
{
var wildcardIsSchema = id.EndsWith(".*", StringComparison.Ordinal) ||
id.EndsWith("~*", StringComparison.Ordinal);
return Results.Json(new
{
id,
ok = true,
segments = pat.Segments.Select(GtsSegmentDto.FromSegment).ToList(),
error = "",
is_wildcard = true,
is_type = wildcardIsSchema
});
}

return Results.Json(new
{
id,
ok = false,
segments = Array.Empty<object>(),
error = "Invalid pattern",
is_wildcard = true,
is_type = false
});
}

if (!GtsId.TryParse(id, out var gid) || gid is null)
if (!GtsPattern.TryParse(id, out var pattern))
return Results.Json(new
{
id,
ok = false,
segments = Array.Empty<object>(),
error = "Parse error",
is_wildcard = false,
error = isWildcard ? "Invalid pattern" : "Parse error",
is_wildcard = isWildcard,
is_type = false
});

var isType = isWildcard
? id.EndsWith(".*", StringComparison.Ordinal) || id.EndsWith("~*", StringComparison.Ordinal)
: id.EndsWith('~');
return Results.Json(new
{
id,
ok = true,
segments = gid.Segments.Select(GtsSegmentDto.FromSegment).ToList(),
segments = pattern!.Segments.Select(GtsSegmentDto.FromSegment).ToList(),
error = "",
is_wildcard = false,
is_type = gid.IsType
is_wildcard = isWildcard,
is_type = isType
});
});

// OP#4 identifier matching (README §10): a concrete candidate matches when it is in the
// filter's selected set; a pattern candidate matches when the filter covers it.
app.MapGet("/match-id-pattern", (string candidate, string pattern) =>
GtsId.TryMatch(candidate, pattern, out var match)
? Results.Json(new { candidate, pattern, match })
: Results.Json(new { candidate, pattern, match = false, error = "Invalid candidate or pattern" }));
{
if (!GtsPattern.TryParse(pattern, out var filter) || !GtsPattern.TryParse(candidate, out var cand))
return Results.Json(new { candidate, pattern, match = false, error = "Invalid candidate or pattern" });
var match = cand!.HasWildcard ? filter!.Covers(cand) : filter!.MatchesCandidate(candidate);
return Results.Json(new { candidate, pattern, match });
});

app.MapGet("/uuid", (string gts_id) =>
{
Expand Down
41 changes: 33 additions & 8 deletions Gts.Application/GtsOperationEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,49 @@ private static async Task<IResult> Compatibility(GtsRegistry registry, string ol
var registered = await registry.GetAllAsync().ConfigureAwait(false);
var schemas = registered.Where(entity => entity.IsSchema && entity.GtsId is not null)
.ToDictionary(entity => entity.GtsId!.Id, entity => entity.Content, StringComparer.Ordinal);
var oldFlat = GtsJsonSchemaEvolutionCompatibility.FlattenSchema(GtsHttpApiExtensions.ResolveSchemaRefs(oldEntity.Content, schemas, new HashSet<string>()));
var newFlat = GtsJsonSchemaEvolutionCompatibility.FlattenSchema(GtsHttpApiExtensions.ResolveSchemaRefs(newEntity.Content, schemas, new HashSet<string>()));
var oldResolved = GtsHttpApiExtensions.ResolveSchemaRefs(oldEntity.Content, schemas, new HashSet<string>());
var newResolved = GtsHttpApiExtensions.ResolveSchemaRefs(newEntity.Content, schemas, new HashSet<string>());
var oldFlat = GtsJsonSchemaEvolutionCompatibility.FlattenSchema(oldResolved);
var newFlat = GtsJsonSchemaEvolutionCompatibility.FlattenSchema(newResolved);
var (backward, backwardErrors, forward, forwardErrors) = GtsSchemaCompatibilityService.CompareEvolution(oldFlat, newFlat);

// Fold in the GTS reference dimension (§9.6.1): backward needs Valid(old) ⊆ Valid(new) and
// forward needs Valid(new) ⊆ Valid(old). An unprovable inclusion is reported as "unknown".
var refBackward = GtsRefScopeCompat.SchemaRefRelation(newResolved, oldResolved, null, null);
var refForward = GtsRefScopeCompat.SchemaRefRelation(oldResolved, newResolved, null, null);
var backwardStatus = CombineStatus(backward, refBackward);
var forwardStatus = CombineStatus(forward, refForward);
var fullStatus = CombineFull(backwardStatus, forwardStatus);
return Results.Json(new
{
old = oldTypeId,
@new = newTypeId,
backward_compatibility = backward ? "compatible" : "incompatible",
forward_compatibility = forward ? "compatible" : "incompatible",
full_compatibility = backward && forward ? "compatible" : "incompatible",
is_backward_compatible = backward,
is_forward_compatible = forward,
is_fully_compatible = backward && forward,
backward_compatibility = backwardStatus,
forward_compatibility = forwardStatus,
full_compatibility = fullStatus,
is_backward_compatible = backwardStatus == "compatible",
is_forward_compatible = forwardStatus == "compatible",
is_fully_compatible = fullStatus == "compatible",
backward_errors = backwardErrors,
forward_errors = forwardErrors
});
}

/// <summary>Combines a structural verdict with the three-valued reference relation (§9.6.1).</summary>
private static string CombineStatus(bool structurallyCompatible, GtsRefScopeCompat.Relation reference)
{
if (!structurallyCompatible || reference == GtsRefScopeCompat.Relation.Excluded)
return "incompatible";
return reference == GtsRefScopeCompat.Relation.Unknown ? "unknown" : "compatible";
}

private static string CombineFull(string backward, string forward)
{
if (backward == "incompatible" || forward == "incompatible")
return "incompatible";
return backward == "unknown" || forward == "unknown" ? "unknown" : "compatible";
}

private static async Task<IResult> Cast(HttpRequest request, GtsRegistry registry)
{
var body = await JsonSerializer.DeserializeAsync<CastRequest>(request.Body, GtsHttpJson.Options).ConfigureAwait(false);
Expand Down
24 changes: 15 additions & 9 deletions Gts.Application/GtsSegmentDto.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
using System.Text.Json.Nodes;
using Gts;

namespace Gts.Application;

/// <summary>Maps <see cref="GtsIdSegment"/> to JSON-friendly segment objects (snake_case fields).</summary>
/// <summary>Maps a §10 <see cref="GtsPattern.SegmentView"/> to JSON-friendly segment objects (snake_case fields).</summary>
public static class GtsSegmentDto
{
public static object FromSegment(GtsIdSegment s) => new
/// <summary>
/// Maps a §10 <see cref="GtsPattern.SegmentView"/> to a JSON segment object. A <see cref="JsonObject"/>
/// is used so <c>ver_minor</c> is always present (as explicit <c>null</c> when a segment omits the
/// minor), rather than dropped by the server's global "ignore null" serialization policy.
/// </summary>
public static object FromSegment(GtsPattern.SegmentView s) => new JsonObject
{
vendor = s.Vendor,
package = s.Package,
@namespace = s.Namespace,
type = s.Type,
ver_major = s.VersionMajor,
ver_minor = s.VersionMinor,
is_type = s.IsType
["vendor"] = s.Vendor,
["package"] = s.Package,
["namespace"] = s.Namespace,
["type"] = s.Type,
["ver_major"] = s.VersionMajor is { } major ? JsonValue.Create(major) : null,
["ver_minor"] = s.VersionMinor is { } minor ? JsonValue.Create(minor) : null,
["is_type"] = s.IsType
};
}
113 changes: 27 additions & 86 deletions Gts.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,42 +253,27 @@ private static int RunValidateId(string[] args)
return 2;
}

// The §10 engine enforces syntax, spelling, the 1024-char limit, unsigned 32-bit version
// bounds and the single-segment-instance rule for both concrete ids and patterns, matching
// the HTTP /validate-id endpoint.
var isWildcard = id.Contains('*', StringComparison.Ordinal);
if (isWildcard)
{
var pr = GtsId.TryParsePattern(id, out var pat);
if (!pr || pat is null)
{
WriteJson(new
{
id,
valid = false,
is_schema = false,
is_wildcard = true,
error = $"Unable to validate GTS ID '{id}': Invalid wildcard pattern"
});
return 0;
}

var isSchema = id.EndsWith("~*", StringComparison.Ordinal) || id.EndsWith(".*", StringComparison.Ordinal);
WriteJson(new { id, valid = true, is_schema = isSchema, is_wildcard = true, error = "" });
return 0;
}

if (!GtsId.TryParse(id, out var gid) || gid is null)
if (!GtsPattern.TryParse(id, out _))
{
WriteJson(new
{
id,
valid = false,
is_schema = false,
is_wildcard = false,
error = $"Unable to validate GTS ID '{id}': Invalid GTS id"
is_wildcard = isWildcard,
error = $"Unable to validate GTS ID '{id}': {(isWildcard ? "Invalid wildcard pattern" : "Invalid GTS id")}"
});
return 0;
}

WriteJson(new { id, valid = true, is_schema = gid.IsType, is_wildcard = false, error = "" });
var isSchema = isWildcard
? id.EndsWith("~*", StringComparison.Ordinal) || id.EndsWith(".*", StringComparison.Ordinal)
: id.EndsWith('~');
WriteJson(new { id, valid = true, is_schema = isSchema, is_wildcard = isWildcard, error = "" });
return 0;
}

Expand All @@ -302,56 +287,30 @@ private static int RunParseId(string[] args)
}

var isWildcard = id.Contains('*', StringComparison.Ordinal);
if (isWildcard)
{
if (!GtsId.TryParsePattern(id, out var pat) || pat is null)
{
WriteJson(new
{
id,
ok = false,
is_wildcard = true,
is_schema = false,
segments = (object?)null,
error = "Invalid pattern"
});
return 0;
}

var wildcardIsSchema = id.EndsWith(".*", StringComparison.Ordinal) || id.EndsWith("~*", StringComparison.Ordinal);
WriteJson(new
{
id,
ok = true,
is_wildcard = true,
is_schema = wildcardIsSchema,
segments = pat.Segments.Select(GtsSegmentDto.FromSegment).ToList(),
error = ""
});
return 0;
}

if (!GtsId.TryParse(id, out var gid) || gid is null)
if (!GtsPattern.TryParse(id, out var pattern))
{
WriteJson(new
{
id,
ok = false,
is_wildcard = false,
is_wildcard = isWildcard,
is_schema = false,
segments = (object?)null,
error = "Parse error"
error = isWildcard ? "Invalid pattern" : "Parse error"
});
return 0;
}

var isSchema = isWildcard
? id.EndsWith(".*", StringComparison.Ordinal) || id.EndsWith("~*", StringComparison.Ordinal)
: id.EndsWith('~');
WriteJson(new
{
id,
ok = true,
is_wildcard = false,
is_schema = gid.IsType,
segments = gid.Segments.Select(GtsSegmentDto.FromSegment).ToList(),
is_wildcard = isWildcard,
is_schema = isSchema,
segments = pattern!.Segments.Select(GtsSegmentDto.FromSegment).ToList(),
error = ""
});
return 0;
Expand All @@ -367,35 +326,17 @@ private static int RunMatchIdPattern(string[] args)
return 2;
}

try
{
if (candidate.Contains('*', StringComparison.Ordinal))
{
if (!GtsId.TryParsePattern(candidate, out var cPat) || cPat is null ||
!GtsId.TryParsePattern(pattern, out var pPat) || pPat is null)
{
WriteJson(new { candidate, pattern, match = false, error = "invalid pattern" });
return 0;
}

WriteJson(new { candidate, pattern, match = cPat.Matches(pPat), error = "" });
return 0;
}

if (!GtsId.TryParse(candidate, out var cId) || cId is null)
{
WriteJson(new { candidate, pattern, match = false, error = "invalid candidate" });
return 0;
}

WriteJson(new { candidate, pattern, match = cId.Matches(pattern), error = "" });
return 0;
}
catch (Exception ex)
// OP#4 (README §10): a concrete candidate matches when it is in the filter's selected set;
// a pattern candidate matches when the filter covers it. Shared with HTTP /match-id-pattern.
if (!GtsPattern.TryParse(pattern, out var filter) || !GtsPattern.TryParse(candidate, out var cand))
{
WriteJson(new { candidate, pattern, match = false, error = ex.Message });
WriteJson(new { candidate, pattern, match = false, error = "invalid candidate or pattern" });
return 0;
}

var match = cand!.HasWildcard ? filter!.Covers(cand) : filter!.MatchesCandidate(candidate);
WriteJson(new { candidate, pattern, match, error = "" });
return 0;
}

private static int RunUuid(string[] args)
Expand Down
10 changes: 9 additions & 1 deletion Gts.Store/GtsCastService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@ internal async ValueTask<GtsInstanceCastResult> CastAsync(string instanceId, Gts
? CanonicalSchema(stored.Content)
: null;
}
var targetEffective = GtsJsonSchemaEvolutionCompatibility.FlattenSchema(GtsSchemaDependencyGraph.Resolve(targetDocument, Load));
JsonObject targetEffective;
try
{
targetEffective = GtsJsonSchemaEvolutionCompatibility.FlattenSchema(GtsSchemaDependencyGraph.Resolve(targetDocument, Load));
}
catch (GtsSchemaTooLargeException)
{
return new GtsInstanceCastResult { Ok = false, InstanceId = id, FromSchemaId = sourceSchemaId, ToSchemaId = targetSchemaId, FailureReason = GtsValidationFailure.SchemaNormalizationFailed, Comparison = comparison };
}
var casted = GtsInstanceCast.CastToEffectiveSchema(entity.Content, targetEffective);
var schemas = entities.Where(candidate => candidate.IsSchema && candidate.GtsId is not null)
.ToDictionary(candidate => candidate.GtsId!, candidate => GtsSchemaDocumentNormalizer.ForJsonSchemaEvaluation(candidate.Content));
Expand Down
Loading