Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b292422
Add basic ID classes and parsing
javiers451 Mar 5, 2026
1c16c42
Fix GetHashCode and Equals
javiers451 Mar 6, 2026
ad08be8
Fix end of input parsing
javiers451 Mar 6, 2026
db55ed3
Handle null as input for parsing
javiers451 Mar 6, 2026
c28ded7
Fix StringBuilder buffer
javiers451 Mar 6, 2026
8b64be0
Check null params for store
javiers451 Mar 6, 2026
6d7ede3
Materialize store on enumeration
javiers451 Mar 6, 2026
8cd0a8b
Bring back spec submodule
javiers451 Mar 6, 2026
0b61e8d
Add XML documentation comments
javiers451 Mar 6, 2026
e78d652
Fix mathcing of shorter non-wildcard pattern
javiers451 Mar 6, 2026
df48d16
Move extraction methods to GtsJsonEntity.
javiers451 Mar 9, 2026
efb1184
Fix extract tests
javiers451 Mar 9, 2026
bae43f8
Try original impl for extract method
javiers451 Mar 9, 2026
49cb31a
Initial instance validation
javiers451 Apr 1, 2026
27911e9
Update readme
javiers451 Apr 1, 2026
6e7730c
Add basic relationship resolution
javiers451 Apr 8, 2026
daf4581
Layout resolution and compatibility
javiers451 Apr 10, 2026
29a9093
Bring back compatibility tests
javiers451 Apr 13, 2026
4cddf82
Fix schema ref format validator
javiers451 Apr 13, 2026
396b321
Basic GTS server impl
javiers451 Apr 13, 2026
3dad591
Start implementing compatibility checking
javiers451 Apr 20, 2026
04ce3ea
Implement CLI
javiers451 Apr 21, 2026
4ed98bb
Fix projects references
javiers451 Apr 21, 2026
87addd9
Start version casting work
javiers451 Apr 21, 2026
18dcf0c
Initiate query engine
javiers451 Apr 30, 2026
dd8386c
Implement attribute access
javiers451 Apr 30, 2026
0510f3e
Start schema validation impl
javiers451 May 4, 2026
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
3 changes: 3 additions & 0 deletions .gitmodules
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
1 change: 1 addition & 0 deletions .gts-spec
Submodule .gts-spec added at e08828
13 changes: 13 additions & 0 deletions Gts.Store/Gts.Store.csproj
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>
60 changes: 60 additions & 0 deletions Gts.Store/GtsRegistry.cs
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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ValidateGtsReferences is currently a no-op.

Line 20 leaves validation unimplemented, but the API exposes a config flag for it. Since InMemoryGtsRegistry (Gts.Store/InMemory/InMemoryGtsRegistry.cs, Lines 6-23) does not override SaveAsync, validation is silently skipped for all current registry variants.

🔧 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ValueTask SaveAsync(GtsJsonEntity entity)
{
// TODO: validation logic
return _store.SaveAsync(entity);
}
public ValueTask SaveAsync(GtsJsonEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (Config.ValidateGtsReferences)
{
throw new NotSupportedException(
"ValidateGtsReferences is enabled, but reference validation is not implemented yet.");
}
return _store.SaveAsync(entity);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Gts.Store/GtsRegistry.cs` around lines 18 - 22, GtsRegistry.SaveAsync
currently skips validation (TODO) causing ValidateGtsReferences to be a no-op
and allowing InMemoryGtsRegistry to bypass reference checks; update SaveAsync to
call the validation routine when the config flag is enabled (e.g., call
ValidateGtsReferences(entity) or the configured validator before delegating to
_store.SaveAsync) and make the validator throw or return a clear error if
references are invalid so SaveAsync fails fast instead of silently saving;
ensure the same behavior applies for subclasses like InMemoryGtsRegistry by
keeping the call in the base SaveAsync.


/// <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);
}
}
7 changes: 7 additions & 0 deletions Gts.Store/GtsRegistryConfig.cs
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
);
21 changes: 21 additions & 0 deletions Gts.Store/IGtsStore.cs
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:
}
27 changes: 27 additions & 0 deletions Gts.Store/InMemory/InMemoryGtsRegistry.cs
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);
}
}
43 changes: 43 additions & 0 deletions Gts.Store/InMemory/InMemoryGtsStore.cs
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);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
123 changes: 123 additions & 0 deletions Gts.Tests/Extraction/ExtractBasicTests.cs
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);
}
}
71 changes: 71 additions & 0 deletions Gts.Tests/Extraction/ExtractEntityTests.cs
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" });
}
}
Loading