-
Notifications
You must be signed in to change notification settings - Fork 1
Add basic ID classes and parsing #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 12 commits
b292422
1c16c42
ad08be8
db55ed3
c28ded7
8b64be0
6d7ede3
8cd0a8b
0b61e8d
e78d652
df48d16
efb1184
bae43f8
49cb31a
27911e9
6e7730c
daf4581
29a9093
4cddf82
396b321
3dad591
04ce3ea
4ed98bb
87addd9
18dcf0c
dd8386c
0510f3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| [submodule ".gts-spec"] | ||
| path = .gts-spec | ||
| url = https://github.com/GlobalTypeSystem/gts-spec.git |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\Gts\Gts.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||||||||||||||||||||||||
| using Gts.Extraction; | ||||||||||||||||||||||||||||||||
| using Gts.Store.InMemory; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| namespace Gts.Store; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Registry for GTS JSON entities with configurable storage and validation.</summary> | ||||||||||||||||||||||||||||||||
| public abstract class GtsRegistry | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| private readonly IGtsStore _store; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Registry configuration (e.g. reference validation).</summary> | ||||||||||||||||||||||||||||||||
| public GtsRegistryConfig Config { get; } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Initializes the registry with the given store and config.</summary> | ||||||||||||||||||||||||||||||||
| protected GtsRegistry(IGtsStore store, GtsRegistryConfig config) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| ArgumentNullException.ThrowIfNull(store); | ||||||||||||||||||||||||||||||||
| ArgumentNullException.ThrowIfNull(config); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| _store = store; | ||||||||||||||||||||||||||||||||
| Config = config; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Stores or overwrites the entity in the registry.</summary> | ||||||||||||||||||||||||||||||||
| public ValueTask SaveAsync(GtsJsonEntity entity) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| // TODO: validation logic | ||||||||||||||||||||||||||||||||
| return _store.SaveAsync(entity); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 20 leaves validation unimplemented, but the API exposes a config flag for it. Since 🔧 Minimal fail-fast patch to avoid silent bypass public ValueTask SaveAsync(GtsJsonEntity entity)
{
- // TODO: validation logic
+ ArgumentNullException.ThrowIfNull(entity);
+ if (Config.ValidateGtsReferences)
+ {
+ throw new NotSupportedException(
+ "ValidateGtsReferences is enabled, but reference validation is not implemented yet.");
+ }
return _store.SaveAsync(entity);
}If you want, I can draft a concrete reference-validation implementation next (and you can track it as a follow-up issue). 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Retrieves an entity by GTS ID, or null if not found.</summary> | ||||||||||||||||||||||||||||||||
| public ValueTask<GtsJsonEntity?> GetAsync(GtsId id) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| return _store.GetAsync(id); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Returns all entities in the registry.</summary> | ||||||||||||||||||||||||||||||||
| public ValueTask<IList<GtsJsonEntity>> GetAllAsync() | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| return _store.GetAllAsync(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Returns the number of entities in the registry.</summary> | ||||||||||||||||||||||||||||||||
| public ValueTask<int> CountAsync() | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| return _store.CountAsync(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Creates an in-memory registry (single-threaded).</summary> | ||||||||||||||||||||||||||||||||
| public static GtsRegistry InMemory(GtsRegistryConfig config) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| return InMemoryGtsRegistry.Simple(config); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// <summary>Creates an in-memory registry with thread-safe storage.</summary> | ||||||||||||||||||||||||||||||||
| public static GtsRegistry InMemoryThreadSafe(GtsRegistryConfig config) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| return InMemoryGtsRegistry.Concurrent(config); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| namespace Gts.Store; | ||
|
|
||
| /// <summary>Configuration for a GTS registry.</summary> | ||
| /// <param name="ValidateGtsReferences">When true, validate GTS references on save.</param> | ||
| public record GtsRegistryConfig( | ||
| bool ValidateGtsReferences | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using Gts.Extraction; | ||
|
|
||
| namespace Gts.Store; | ||
|
|
||
| /// <summary>Storage abstraction for GTS JSON entities keyed by GTS ID.</summary> | ||
| public interface IGtsStore | ||
| { | ||
| /// <summary>Stores or overwrites the entity (keyed by its GTS ID).</summary> | ||
| ValueTask SaveAsync(GtsJsonEntity entity); | ||
|
|
||
| /// <summary>Retrieves an entity by GTS ID, or null if not found.</summary> | ||
| ValueTask<GtsJsonEntity?> GetAsync(GtsId id); | ||
|
|
||
| /// <summary>Returns all stored entities.</summary> | ||
| ValueTask<IList<GtsJsonEntity>> GetAllAsync(); | ||
|
|
||
| /// <summary>Returns the number of stored entities.</summary> | ||
| ValueTask<int> CountAsync(); | ||
|
|
||
| //ValueTask<int> ValidateAsync(); // TODO: | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| using System.Collections.Concurrent; | ||
| using Gts.Extraction; | ||
|
|
||
| namespace Gts.Store.InMemory; | ||
|
|
||
| /// <summary>In-memory implementation of <see cref="GtsRegistry"/>.</summary> | ||
| internal class InMemoryGtsRegistry : GtsRegistry | ||
| { | ||
| private InMemoryGtsRegistry(IGtsStore store, GtsRegistryConfig config) | ||
| : base(store, config) | ||
| { | ||
| } | ||
|
|
||
| /// <summary>Creates a simple (non-concurrent) in-memory registry.</summary> | ||
| internal static InMemoryGtsRegistry Simple(GtsRegistryConfig config) | ||
| { | ||
| return new InMemoryGtsRegistry( | ||
| new InMemoryGtsStore<Dictionary<GtsId, GtsJsonEntity>>(), config); | ||
| } | ||
|
|
||
| /// <summary>Creates a thread-safe in-memory registry using a concurrent dictionary.</summary> | ||
| internal static InMemoryGtsRegistry Concurrent(GtsRegistryConfig config) | ||
| { | ||
| return new InMemoryGtsRegistry( | ||
| new InMemoryGtsStore<ConcurrentDictionary<GtsId, GtsJsonEntity>>(), config); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| using Gts.Extraction; | ||
|
|
||
| namespace Gts.Store.InMemory; | ||
|
|
||
| /// <summary>In-memory implementation of <see cref="IGtsStore"/> using a dictionary-like backing store.</summary> | ||
| internal class InMemoryGtsStore<T> : IGtsStore | ||
| where T : class, IDictionary<GtsId, GtsJsonEntity>, new() | ||
| { | ||
| private readonly T _entities = new(); | ||
|
|
||
| /// <inheritdoc/> | ||
| public ValueTask SaveAsync(GtsJsonEntity entity) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(entity); | ||
|
|
||
| var key = entity.GtsId; | ||
|
|
||
| ArgumentNullException.ThrowIfNull(key); | ||
|
|
||
| _entities[key] = entity; | ||
|
|
||
| return ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public ValueTask<GtsJsonEntity?> GetAsync(GtsId id) | ||
| { | ||
| _entities.TryGetValue(id, out var entity); | ||
| return ValueTask.FromResult(entity); | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public ValueTask<IList<GtsJsonEntity>> GetAllAsync() | ||
| { | ||
| return ValueTask.FromResult<IList<GtsJsonEntity>>(_entities.Values.ToArray()); | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public ValueTask<int> CountAsync() | ||
| { | ||
| return ValueTask.FromResult(_entities.Count); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| using System.Text.Json.Nodes; | ||
| using Gts.Extraction; | ||
|
|
||
| namespace Gts.Tests.Extraction; | ||
|
|
||
| public class ExtractBasicTests | ||
| { | ||
| [Theory] | ||
| [InlineData("$id")] | ||
| [InlineData("gtsId")] | ||
| [InlineData("gtsIid")] | ||
| [InlineData("gtsOid")] | ||
| [InlineData("gtsI")] | ||
| [InlineData("gts_id")] | ||
| [InlineData("gts_oid")] | ||
| [InlineData("gts_iid")] | ||
| [InlineData("id")] | ||
| public void ExtractingOneOfDefaultIdsReturnsId(string id) | ||
| { | ||
| var gtsId = "gts.vendor.package.namespace.type.v0~a.b.c.d.v1"; | ||
|
|
||
| var result = GtsJsonEntity.ExtractId(new JsonObject | ||
| { | ||
| [id] = gtsId, | ||
| ["name"] = "Some Name" | ||
| }); | ||
|
|
||
| Assert.Equal(id, result.SelectedEntityField); | ||
| Assert.Equal(gtsId, result.Id); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("invalid_id")] | ||
| public void ExtractingOneOfNonDefaultIdsReturnsNulls(string id) | ||
| { | ||
| var gtsId = "gts.vendor.package.namespace.type.v0~a.b.c.d.v1"; | ||
|
|
||
| var result = GtsJsonEntity.ExtractId(new JsonObject | ||
| { | ||
| [id] = gtsId, | ||
| ["name"] = "Some Name", | ||
| }); | ||
|
|
||
| Assert.Null(result.SelectedEntityField); | ||
| Assert.Null(result.Id); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("custom_id")] | ||
| public void ExtractingOneOfCustomIdsWithConfigReturnsId(string id) | ||
| { | ||
| var gtsId = "gts.vendor.package.namespace.type.v0~a.b.c.d.v1"; | ||
|
|
||
| var result = GtsJsonEntity.ExtractId( | ||
| new JsonObject | ||
| { | ||
| [id] = gtsId, | ||
| ["name"] = "Some Name" | ||
| }, | ||
| new (){ EntityIdPropertyNames = [id]}); | ||
|
|
||
| Assert.Equal(id, result.SelectedEntityField); | ||
| Assert.Equal(gtsId, result.Id); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExtractingOneOfDefaultIdsReturnsIdsByOrdering() | ||
| { | ||
| var gtsId = "gts.vendor.package.namespace.type.v0~a.b.c.d.v1"; | ||
| var result = GtsJsonEntity.ExtractId( | ||
| new JsonObject | ||
| { | ||
| ["name"] = "Some Name", | ||
| ["gtsId"] = gtsId, // 1st | ||
| ["$id"] = gtsId, // 2nd | ||
| }); | ||
|
|
||
| Assert.Equal(gtsId, result.Id); | ||
| Assert.Equal("gtsId", result.SelectedEntityField); // 1st wins | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExtractingDollarIdReturnsIdWithStrippedPrefix() | ||
| { | ||
| var result = GtsJsonEntity.ExtractId( | ||
| new JsonObject | ||
| { | ||
| ["$id"] = "gts://gts.vendor.package.namespace.type.v1.0~", | ||
| ["$schema"] = "http://json-schema.org/draft-07/schema#", | ||
| ["type"] = "object", | ||
| }); | ||
|
|
||
| Assert.Equal("gts.vendor.package.namespace.type.v1.0~", result.Id); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExtractingInvalidIdFallbacksToNextValidId() | ||
| { | ||
| var result = GtsJsonEntity.ExtractId( | ||
| new JsonObject | ||
| { | ||
| ["gtsId"] = "invalid-id", // invalid | ||
| ["name"] = "Some Name", | ||
| ["id"] = "gts.vendor.package.namespace.type.v1.0~", // valid | ||
| }); | ||
|
|
||
| Assert.Equal("gts.vendor.package.namespace.type.v1.0~", result.Id); | ||
| Assert.Equal("id", result.SelectedEntityField); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExtractingIdReturnsNullWhenValidIdDoesNotExist() | ||
| { | ||
| var result = GtsJsonEntity.ExtractId( | ||
| new JsonObject | ||
| { | ||
| ["name"] = "Some Name", | ||
| }); | ||
|
|
||
| Assert.Null(result.Id); | ||
| Assert.Null(result.SelectedEntityField); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| using System.Text.Json.Nodes; | ||
| using Gts.Extraction; | ||
|
|
||
| namespace Gts.Tests.Extraction; | ||
|
|
||
| public class ExtractEntityTests | ||
| { | ||
| [Fact] | ||
| public void ExtractingPopulatesRefsWithIds() | ||
| { | ||
| var entity = GtsJsonEntity.ExtractEntity(new JsonObject | ||
| { | ||
| ["$id"] = "gts.x.test.core.schema.v1~", | ||
| ["$ref"] = "gts.x.test.core.base.v1~", | ||
| }); | ||
|
|
||
| Assert.Contains(entity.GtsRefs, | ||
| r => r is { Id: "gts.x.test.core.schema.v1~", SourcePath: "$id" }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExtractingPopulatesRefsWithExplicitRefs() | ||
| { | ||
| var entity = GtsJsonEntity.ExtractEntity(new JsonObject | ||
| { | ||
| ["$id"] = "gts.x.test.core.schema.v1~", | ||
| ["$ref"] = "gts.x.test.core.base.v1~", | ||
| }); | ||
|
|
||
| Assert.Contains(entity.GtsRefs, | ||
| r => r is { Id: "gts.x.test.core.base.v1~", SourcePath: "$ref" }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExtractingPopulatesRefsWithExplicitNestedObjectRefs() | ||
| { | ||
| var entity = GtsJsonEntity.ExtractEntity(new JsonObject | ||
| { | ||
| ["$id"] = "gts.x.test.core.schema.v1~", | ||
| ["properties"] = new JsonObject | ||
| { | ||
| ["field1"] = new JsonObject | ||
| { | ||
| ["$ref"] = "gts.x.test.core.field.v1~", | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| Assert.Contains(entity.GtsRefs, | ||
| r => r is { Id: "gts.x.test.core.field.v1~", SourcePath: "properties.field1.$ref" }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExtractingPopulatesRefsWithExplicitNestedArrayRefs() | ||
| { | ||
| var entity = GtsJsonEntity.ExtractEntity(new JsonObject | ||
| { | ||
| ["$id"] = "gts.x.test.core.schema.v1~", | ||
| ["items"] = new JsonArray | ||
| { | ||
| new JsonObject | ||
| { | ||
| ["$ref"] = "gts.x.test.core.item.v1~", | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| Assert.Contains(entity.GtsRefs, | ||
| r => r is { Id: "gts.x.test.core.item.v1~", SourcePath: "items[0].$ref" }); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.