diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..aac61eb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +root = true + +# Baseline whitespace conventions enforced by `make fmt` (dotnet format). +# Kept intentionally minimal so the formatter's built-in C# defaults apply +# without imposing heavy style opinions. +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space + +[*.cs] +indent_size = 4 + +[*.{csproj,props,targets}] +indent_size = 2 + +[*.{json,yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5d6dfdf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,149 @@ +name: CI + +on: + push: + branches: [ main, develop ] + paths-ignore: + - '**/*.md' + - 'docs/**' + pull_request: + branches: [ main, develop ] + paths-ignore: + - '**/*.md' + - 'docs/**' + +# Cancel previous runs for the same ref to save CI minutes. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + DOTNET_NOLOGO: 'true' + DOTNET_CLI_TELEMETRY_OPTOUT: 'true' + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true' + +jobs: + test: + name: Test Suite (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + + steps: + - name: Force LF in working tree (w/a for Windows) + if: runner.os == 'Windows' + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + clean: 'true' + persist-credentials: false + + # Install the .NET 8 SDK for the net8.0 target. global.json intentionally + # stays permissive ("8.0.0" + rollForward=latestMajor, i.e. "min .NET 8, + # use whatever newer SDK is present"); that pin is not a downloadable + # build, so we install an 8.0 SDK explicitly rather than via global.json. + - name: Set up .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + dotnet-version: '8.0.x' + + - name: Show .NET version + run: dotnet --info + + - name: Restore + run: dotnet restore gts-dotnet.sln + + # Formatting check (fast fail for style issues) + - name: dotnet format (check) + run: make fmt + + # Analyzers (linter) + - name: dotnet format analyzers (check) + run: make lint + + # Build + - name: build + run: dotnet build gts-dotnet.sln -c Release --no-restore + + # Unit tests + - name: test + run: dotnet test gts-dotnet.sln -c Release --no-build + + security: + name: Security (dotnet list package --vulnerable on Ubuntu) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + + - name: Set up .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore gts-dotnet.sln + + # Report vulnerable NuGet dependencies but do not fail CI. + - name: dotnet list package --vulnerable + run: make security + continue-on-error: true + + coverage: + name: Code Coverage + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + + - name: Set up .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + dotnet-version: '8.0.x' + + # Generate a Cobertura coverage report. + - name: Run tests with coverage + run: dotnet test gts-dotnet.sln -c Release --collect:"XPlat Code Coverage" + + # Upload to Codecov; do not fail CI if Codecov is down/misconfigured. + - name: Upload to Codecov + uses: codecov/codecov-action@0f8570b1a125f4937846a11fcfa3bcd548bd8c97 # v4.6.0 + with: + files: '**/coverage.cobertura.xml' + fail_ci_if_error: false + # token: ${{ secrets.CODECOV_TOKEN }} # Uncomment if required for private repos + + gts-spec-tests: + name: GTS Spec Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + + - name: Set up .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + dotnet-version: '8.0.x' + + # Builds the gts CLI binary, pulls the gts-spec test-runner image from + # GHCR (tag from .gts-spec-version), starts the server natively, waits + # for readiness, then runs pytest inside the container against + # host.docker.internal:$PORT. See Makefile. + - name: Run gts-spec tests via docker + run: make gts-spec-tests diff --git a/Gts.Application/GtsHttpApiHelpers.cs b/Gts.Application/GtsHttpApiHelpers.cs index f5d8b4c..d8439b6 100644 --- a/Gts.Application/GtsHttpApiHelpers.cs +++ b/Gts.Application/GtsHttpApiHelpers.cs @@ -75,4 +75,4 @@ internal static JsonObject ResolveSchemaRefs(JsonObject schema, IReadOnlyDiction if (result.Errors is { Count: > 0 }) return string.Join("; ", result.Errors); return result.FailureReason == GtsValidationFailure.PrecedentIncompatible ? "Parent GTS Type Schema not found" : result.FailureReason?.ToWire() ?? "JSON Schema validation failed"; } -} \ No newline at end of file +} diff --git a/Gts.Application/GtsHttpContracts.cs b/Gts.Application/GtsHttpContracts.cs index df9c210..963be8c 100644 --- a/Gts.Application/GtsHttpContracts.cs +++ b/Gts.Application/GtsHttpContracts.cs @@ -17,4 +17,4 @@ internal static class GtsHttpJson { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; -} \ No newline at end of file +} diff --git a/Gts.Application/GtsOperationEndpoints.cs b/Gts.Application/GtsOperationEndpoints.cs index 935a8ba..855ac93 100644 --- a/Gts.Application/GtsOperationEndpoints.cs +++ b/Gts.Application/GtsOperationEndpoints.cs @@ -128,4 +128,4 @@ JsonValue value when value.TryGetValue(out var decimalNumber) => Result _ => Results.Json(new { resolved = true, value = result.Value?.DeepClone() }) }; } -} \ No newline at end of file +} diff --git a/Gts.Store/GtsCastService.cs b/Gts.Store/GtsCastService.cs index b22a8d5..b8818dc 100644 --- a/Gts.Store/GtsCastService.cs +++ b/Gts.Store/GtsCastService.cs @@ -80,4 +80,4 @@ private static JsonObject CanonicalSchema(JsonObject schema) => : schema; private static GtsInstanceCastResult Failure(string? id, GtsId target, GtsValidationFailure reason) => new() { Ok = false, InstanceId = id, ToSchemaId = target, FailureReason = reason }; -} \ No newline at end of file +} diff --git a/Gts.Store/GtsInstanceValidationService.cs b/Gts.Store/GtsInstanceValidationService.cs index 3f4e5bc..a4d3886 100644 --- a/Gts.Store/GtsInstanceValidationService.cs +++ b/Gts.Store/GtsInstanceValidationService.cs @@ -139,4 +139,4 @@ private static bool HasMixedDialectReferences(JsonNode? node, string dialect, IR } return node is JsonArray array && array.Any(child => HasMixedDialectReferences(child, dialect, schemas, visited)); } -} \ No newline at end of file +} diff --git a/Gts.Store/GtsRefConstraint.cs b/Gts.Store/GtsRefConstraint.cs index 2775dc9..8e91a45 100644 --- a/Gts.Store/GtsRefConstraint.cs +++ b/Gts.Store/GtsRefConstraint.cs @@ -30,4 +30,4 @@ internal bool Matches(string value) // requires a full match or a '~' segment boundary immediately after the pattern. return value.Length == Pattern.Length || Pattern.EndsWith('~') || value[Pattern.Length] == '~'; } -} \ No newline at end of file +} diff --git a/Gts.Store/GtsSchemaCompatibilityService.cs b/Gts.Store/GtsSchemaCompatibilityService.cs index c693319..8727048 100644 --- a/Gts.Store/GtsSchemaCompatibilityService.cs +++ b/Gts.Store/GtsSchemaCompatibilityService.cs @@ -17,4 +17,4 @@ public static (bool Backward, IReadOnlyList BackwardErrors, bool Forward var (forward, forwardErrors) = GtsJsonSchemaEvolutionCompatibility.CheckForward(oldSchema, newSchema); return (backward, backwardErrors, forward, forwardErrors); } -} \ No newline at end of file +} diff --git a/Gts.Store/GtsSchemaDependencyGraph.cs b/Gts.Store/GtsSchemaDependencyGraph.cs index 480c7f5..dd59bc8 100644 --- a/Gts.Store/GtsSchemaDependencyGraph.cs +++ b/Gts.Store/GtsSchemaDependencyGraph.cs @@ -134,4 +134,4 @@ private static JsonObject Resolve(JsonObject schema, Func lo }; return clone; } -} \ No newline at end of file +} diff --git a/Gts.Store/GtsSchemaValidationService.cs b/Gts.Store/GtsSchemaValidationService.cs index 818c81e..25a3133 100644 --- a/Gts.Store/GtsSchemaValidationService.cs +++ b/Gts.Store/GtsSchemaValidationService.cs @@ -105,4 +105,4 @@ internal async ValueTask ValidateAsync(GtsId schemaId } private static GtsSchemaValidationResult Failure(string? id, GtsValidationFailure reason) => new() { Ok = false, SchemaId = id, FailureReason = reason }; -} \ No newline at end of file +} diff --git a/Gts.Store/GtsTraitComposer.cs b/Gts.Store/GtsTraitComposer.cs index 07f311c..e117a47 100644 --- a/Gts.Store/GtsTraitComposer.cs +++ b/Gts.Store/GtsTraitComposer.cs @@ -70,4 +70,4 @@ private static void CollectValues(JsonObject schema, JsonObject values) CollectValues(branch, values); } } -} \ No newline at end of file +} diff --git a/Gts.Store/Validation/GtsFormatRegistry.cs b/Gts.Store/Validation/GtsFormatRegistry.cs index 61ec856..cc390d4 100644 --- a/Gts.Store/Validation/GtsFormatRegistry.cs +++ b/Gts.Store/Validation/GtsFormatRegistry.cs @@ -73,4 +73,4 @@ public override bool Validate(JsonElement value, out string? errorMessage) return valid; } } -} \ No newline at end of file +} diff --git a/Gts.Store/Validation/GtsJson.cs b/Gts.Store/Validation/GtsJson.cs index e8b8a2b..a93ea28 100644 --- a/Gts.Store/Validation/GtsJson.cs +++ b/Gts.Store/Validation/GtsJson.cs @@ -8,4 +8,4 @@ internal static class GtsJson internal static JsonElement ToElement(JsonNode? node) => JsonSerializer.SerializeToElement(node); internal static JsonObject CloneObject(JsonObject source) => (JsonObject)source.DeepClone(); -} \ No newline at end of file +} diff --git a/Gts.Store/Validation/GtsJsonPointer.cs b/Gts.Store/Validation/GtsJsonPointer.cs index cfebb13..8468413 100644 --- a/Gts.Store/Validation/GtsJsonPointer.cs +++ b/Gts.Store/Validation/GtsJsonPointer.cs @@ -17,4 +17,4 @@ internal static bool TryEvaluate(JsonNode? root, string reference, out JsonNode? var pointerText = reference.StartsWith('#') ? reference[1..] : reference; return JsonPointer.TryParse(pointerText, out var pointer) && pointer.TryEvaluate(root, out result); } -} \ No newline at end of file +} diff --git a/Gts.Store/Validation/GtsJsonSchemaEngine.cs b/Gts.Store/Validation/GtsJsonSchemaEngine.cs index fa60cd7..c75d305 100644 --- a/Gts.Store/Validation/GtsJsonSchemaEngine.cs +++ b/Gts.Store/Validation/GtsJsonSchemaEngine.cs @@ -199,4 +199,4 @@ private static void Walk(EvaluationResults node, List errors) foreach (var detail in node.Details ?? []) Walk(detail, errors); } -} \ No newline at end of file +} diff --git a/Gts.Store/Validation/GtsJsonSchemaEvaluator.cs b/Gts.Store/Validation/GtsJsonSchemaEvaluator.cs index 40fd1fb..497bbf0 100644 --- a/Gts.Store/Validation/GtsJsonSchemaEvaluator.cs +++ b/Gts.Store/Validation/GtsJsonSchemaEvaluator.cs @@ -20,4 +20,4 @@ internal static void ValidateSchema(JsonObject normalizedSchemaDocument) => Engine.ValidateSchema(normalizedSchemaDocument); internal static IReadOnlyList FlattenErrors(EvaluationResults results) => Engine.FlattenErrors(results); -} \ No newline at end of file +} diff --git a/Gts.Store/Validation/IGtsJsonSchemaEngine.cs b/Gts.Store/Validation/IGtsJsonSchemaEngine.cs index 908e888..2eb2f62 100644 --- a/Gts.Store/Validation/IGtsJsonSchemaEngine.cs +++ b/Gts.Store/Validation/IGtsJsonSchemaEngine.cs @@ -12,4 +12,4 @@ internal interface IGtsJsonSchemaEngine void ValidateSchema(JsonObject normalizedSchemaDocument); IReadOnlyList FlattenErrors(EvaluationResults results); -} \ No newline at end of file +} diff --git a/Gts.Tests/Extraction/ExtractBasicTests.cs b/Gts.Tests/Extraction/ExtractBasicTests.cs index f6f8172..9ffc00c 100644 --- a/Gts.Tests/Extraction/ExtractBasicTests.cs +++ b/Gts.Tests/Extraction/ExtractBasicTests.cs @@ -120,4 +120,4 @@ public void ExtractingIdReturnsNullWhenValidIdDoesNotExist() Assert.Null(result.Id); Assert.Null(result.SelectedEntityField); } -} \ No newline at end of file +} diff --git a/Gts.Tests/Extraction/ExtractEntityTests.cs b/Gts.Tests/Extraction/ExtractEntityTests.cs index 9970fb5..b91594d 100644 --- a/Gts.Tests/Extraction/ExtractEntityTests.cs +++ b/Gts.Tests/Extraction/ExtractEntityTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using Gts.Extraction; namespace Gts.Tests.Extraction; @@ -85,4 +85,4 @@ public void ExtractingPopulatesRefsFromDoubleDollarRefInAllOf() Assert.Contains(entity.GtsRefs, r => r.Id == "gts.x.test6.events.type.v1~"); } -} \ No newline at end of file +} diff --git a/Gts.Tests/Extraction/ExtractSchemaTests.cs b/Gts.Tests/Extraction/ExtractSchemaTests.cs index 8918b16..7f02859 100644 --- a/Gts.Tests/Extraction/ExtractSchemaTests.cs +++ b/Gts.Tests/Extraction/ExtractSchemaTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using Gts.Extraction; namespace Gts.Tests.Extraction; diff --git a/Gts.Tests/GtsIdTests.cs b/Gts.Tests/GtsIdTests.cs index 3e9bf8e..54b1472 100644 --- a/Gts.Tests/GtsIdTests.cs +++ b/Gts.Tests/GtsIdTests.cs @@ -129,4 +129,4 @@ public void EqualsIsTheSameAsEqualsForString() Assert.Equal(id1, id12); Assert.NotEqual(id1, id2); } -} \ No newline at end of file +} diff --git a/Gts.Tests/Parsing/IdentifierParserTests.cs b/Gts.Tests/Parsing/IdentifierParserTests.cs index 90c1424..6873f74 100644 --- a/Gts.Tests/Parsing/IdentifierParserTests.cs +++ b/Gts.Tests/Parsing/IdentifierParserTests.cs @@ -1,4 +1,4 @@ -using Gts.Parsing; +using Gts.Parsing; namespace Gts.Tests.Parsing; diff --git a/Gts.Tests/Parsing/InstanceParserTests.cs b/Gts.Tests/Parsing/InstanceParserTests.cs index 99374f8..f3fd30d 100644 --- a/Gts.Tests/Parsing/InstanceParserTests.cs +++ b/Gts.Tests/Parsing/InstanceParserTests.cs @@ -1,4 +1,4 @@ -using Gts.Parsing; +using Gts.Parsing; using Pidgin; namespace Gts.Tests.Parsing; @@ -30,4 +30,4 @@ public void InstanceParsesTripleSegment() var segments = id.ToArray(); Assert.Equal(3, segments.Length); } -} \ No newline at end of file +} diff --git a/Gts.Tests/Parsing/PatternParserTests.cs b/Gts.Tests/Parsing/PatternParserTests.cs index e1c79f4..97faf8e 100644 --- a/Gts.Tests/Parsing/PatternParserTests.cs +++ b/Gts.Tests/Parsing/PatternParserTests.cs @@ -1,4 +1,4 @@ -using Gts.Parsing; +using Gts.Parsing; using Pidgin; namespace Gts.Tests.Parsing; @@ -148,4 +148,4 @@ public void GtsPatternParsesMultipleWithTildeAtTheEnd() var segments = id.ToArray(); Assert.Single(segments); } -} \ No newline at end of file +} diff --git a/Gts.Tests/Parsing/SectionParserTests.cs b/Gts.Tests/Parsing/SectionParserTests.cs index f3ecc7b..306674f 100644 --- a/Gts.Tests/Parsing/SectionParserTests.cs +++ b/Gts.Tests/Parsing/SectionParserTests.cs @@ -1,4 +1,4 @@ -using Gts.Parsing; +using Gts.Parsing; using Pidgin; namespace Gts.Tests.Parsing; @@ -18,4 +18,4 @@ public void GtsDoesNotParseUppercaseLiteral() Assert.Throws>( () => Parsers.GtsPrefix.ParseOrThrow("GTS")); } -} \ No newline at end of file +} diff --git a/Gts.Tests/Parsing/SegmentParserTests.cs b/Gts.Tests/Parsing/SegmentParserTests.cs index 4bc953f..9bc77b1 100644 --- a/Gts.Tests/Parsing/SegmentParserTests.cs +++ b/Gts.Tests/Parsing/SegmentParserTests.cs @@ -1,4 +1,4 @@ -using Gts.Parsing; +using Gts.Parsing; using Pidgin; namespace Gts.Tests.Parsing; diff --git a/Gts.Tests/Parsing/TypeParserTests.cs b/Gts.Tests/Parsing/TypeParserTests.cs index 254527d..6978626 100644 --- a/Gts.Tests/Parsing/TypeParserTests.cs +++ b/Gts.Tests/Parsing/TypeParserTests.cs @@ -1,4 +1,4 @@ -using Gts.Parsing; +using Gts.Parsing; using Pidgin; namespace Gts.Tests.Parsing; @@ -30,4 +30,4 @@ public void TypeParsesDoubleSegment() var segments = id.ToArray(); Assert.Equal(2, segments.Length); } -} \ No newline at end of file +} diff --git a/Gts.Tests/Parsing/VersionParserTests.cs b/Gts.Tests/Parsing/VersionParserTests.cs index e040ef8..38e3781 100644 --- a/Gts.Tests/Parsing/VersionParserTests.cs +++ b/Gts.Tests/Parsing/VersionParserTests.cs @@ -46,4 +46,4 @@ public void VersionFullParsesPartialVersionString() Assert.Equal(123, major); Assert.Null(minor); } -} \ No newline at end of file +} diff --git a/Gts.Tests/Validation/JsonInfrastructureTests.cs b/Gts.Tests/Validation/JsonInfrastructureTests.cs index ddc59a7..0a86c75 100644 --- a/Gts.Tests/Validation/JsonInfrastructureTests.cs +++ b/Gts.Tests/Validation/JsonInfrastructureTests.cs @@ -152,4 +152,4 @@ public void Shared_id_parser_rejects_negative_versions(string id) { Assert.False(GtsId.TryParse(id, out _)); } -} \ No newline at end of file +} diff --git a/Gts/Parsing/GtsIdParser.cs b/Gts/Parsing/GtsIdParser.cs index 61c633c..9c35182 100644 --- a/Gts/Parsing/GtsIdParser.cs +++ b/Gts/Parsing/GtsIdParser.cs @@ -63,4 +63,4 @@ private static bool HasCanonicalVersion(string source, Parsers.VersionInfo? vers return false; return tokens.Length == 5 || tokens[5] == version.Value.Minor?.ToString(); } -} \ No newline at end of file +} diff --git a/Gts/Parsing/ParseException.cs b/Gts/Parsing/ParseException.cs index 05e5986..670af98 100644 --- a/Gts/Parsing/ParseException.cs +++ b/Gts/Parsing/ParseException.cs @@ -11,4 +11,4 @@ public ParseException(ParseResult parseResult) { ParseResult = parseResult; } -} \ No newline at end of file +} diff --git a/Gts/Properties/AssemblyInfo.cs b/Gts/Properties/AssemblyInfo.cs index 4aac24a..a89e18b 100644 --- a/Gts/Properties/AssemblyInfo.cs +++ b/Gts/Properties/AssemblyInfo.cs @@ -1,3 +1,3 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Gts.Tests")]