diff --git a/src/GameCult.Mesh/CultMeshDocumentRegistries.cs b/src/GameCult.Mesh/CultMeshDocumentRegistries.cs new file mode 100644 index 00000000..a210671a --- /dev/null +++ b/src/GameCult.Mesh/CultMeshDocumentRegistries.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameCult.Caching; +using GameCult.Networking; + +namespace GameCult.Mesh +{ + public static partial class CultMesh + { + /// + /// Creates a CultCache document registry and resolves the supplied document types into it. + /// + public static CultDocumentRegistry CreateCultCacheDocumentRegistry(params Type[] documentTypes) + { + return CreateCultCacheDocumentRegistry((IEnumerable)documentTypes); + } + + /// + /// Creates a CultCache document registry and resolves the supplied document types into it. + /// + public static CultDocumentRegistry CreateCultCacheDocumentRegistry(IEnumerable documentTypes) + { + if (documentTypes == null) + { + throw new ArgumentNullException(nameof(documentTypes)); + } + + var registry = new CultDocumentRegistry(); + foreach (var documentType in DistinctDocumentTypes(documentTypes)) + { + registry.GetRequired(documentType); + } + + return registry; + } + + /// + /// Creates a CultNet document registry with default bindings for the supplied document types. + /// + public static CultNetDocumentRegistry CreateCultNetDocumentRegistry(params Type[] documentTypes) + { + return CreateCultNetDocumentRegistry((IEnumerable)documentTypes); + } + + /// + /// Creates a CultNet document registry with default bindings for the supplied document types. + /// + public static CultNetDocumentRegistry CreateCultNetDocumentRegistry(IEnumerable documentTypes) + { + return CreateCultNetDocumentRegistry(documentTypes, null); + } + + /// + /// Creates a CultNet document registry with default bindings for the supplied document types. + /// + public static CultNetDocumentRegistry CreateCultNetDocumentRegistry( + IEnumerable documentTypes, + CultDocumentRegistry? documents) + { + if (documentTypes == null) + { + throw new ArgumentNullException(nameof(documentTypes)); + } + + documents ??= CreateCultCacheDocumentRegistry(documentTypes); + var registry = new CultNetDocumentRegistry(documents); + var descriptors = DistinctDocumentTypes(documentTypes) + .Select(documentType => documents.GetRequired(documentType)) + .ToArray(); + foreach (var descriptorGroup in descriptors.GroupBy(descriptor => descriptor.SchemaId, StringComparer.Ordinal)) + { + foreach (var descriptor in descriptorGroup.Reverse()) + { + registry.Register(CultNetDocumentBinding.ForDocument(descriptor.DocumentType, documents)); + } + } + + return registry; + } + + /// + /// Creates a CultNet document registry with default bindings for the supplied document types. + /// + public static CultNetDocumentRegistry CreateCultNetDocumentRegistry( + CultDocumentRegistry documents, + params Type[] documentTypes) + { + return CreateCultNetDocumentRegistry(documentTypes, documents); + } + + /// + /// Creates a CultNet document registry with default bindings for the supplied document types. + /// + public static CultNetDocumentRegistry CreateCultNetDocumentRegistry( + CultDocumentRegistry documents, + IEnumerable documentTypes) + { + return CreateCultNetDocumentRegistry(documentTypes, documents); + } + + private static IEnumerable DistinctDocumentTypes(IEnumerable documentTypes) + { + return documentTypes + .Select(documentType => documentType ?? throw new ArgumentException( + "Document type collections cannot contain null entries.", + nameof(documentTypes))) + .Distinct(); + } + } +} diff --git a/src/GameCult.Networking/CultNetDocumentRegistry.cs b/src/GameCult.Networking/CultNetDocumentRegistry.cs index d6446b2d..fe039dfa 100644 --- a/src/GameCult.Networking/CultNetDocumentRegistry.cs +++ b/src/GameCult.Networking/CultNetDocumentRegistry.cs @@ -94,6 +94,42 @@ namespace GameCult.Networking ? payloadDeserializer(payload) : CultDocumentMessagePackSerialization.Deserialize(payload)); } + + /// + /// Creates a document binding for a CultCache document type discovered at runtime. + /// + public static CultNetDocumentBinding ForDocument( + Type documentType, + CultDocumentRegistry? registry = null, + string? schemaId = null, + Func? payloadSerializer = null, + Func? payloadDeserializer = null) + { + if (documentType == null) + { + throw new ArgumentNullException(nameof(documentType)); + } + + var descriptor = (registry ?? CultDocumentRegistry.Shared).GetRequired(documentType); + return new CultNetDocumentBinding( + documentType, + string.IsNullOrWhiteSpace(schemaId) ? descriptor.SchemaId : schemaId!, + document => + { + if (!documentType.IsInstanceOfType(document)) + { + throw new InvalidOperationException( + $"Document payload type {document.GetType().FullName} is not assignable to {documentType.FullName}."); + } + + return payloadSerializer != null + ? payloadSerializer(document) + : CultDocumentMessagePackSerialization.SerializeUntyped(document, documentType); + }, + payload => payloadDeserializer != null + ? payloadDeserializer(payload) + : CultDocumentMessagePackSerialization.DeserializeUntyped(documentType, payload)); + } } /// diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs index bad9afb0..356b5dec 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs @@ -1101,6 +1101,43 @@ public sealed class CultMeshStreamingTests request.MessageId.StartsWith("mesh-test-scoped-snapshot:", StringComparison.Ordinal)); } + [Test] + public async Task DocumentRegistryHelpers_CreateTypedNetworkRegistriesForAliases() + { + var sourceCache = new CultCache(); + var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry( + typeof(MeshNoteDocument), + typeof(MeshNoteAliasDocument)); + var networkRegistry = CultMesh.CreateCultNetDocumentRegistry( + new[] { typeof(MeshNoteDocument), typeof(MeshNoteAliasDocument) }, + cacheRegistry); + var key = new CultRecordKey("mesh-note:registry-helper"); + await sourceCache.UpsertAsync(new MeshNoteDocument + { + Schema = "tests.mesh_note.v1", + Text = "registry-helper", + Revision = 23 + }, new CultRecordHandle(key)); + var endpoint = "cultnet://registry-helper.test:3077"; + + var documents = await CultMesh.FetchSnapshotDocumentsAsync( + endpoint, + new CultMeshSnapshotRequestOptions + { + RecordKeys = new[] { key.Value }, + CreateClient = () => new MeshSnapshotSchemaClient(request => + networkRegistry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request)) + }, + networkRegistry); + + var schemaId = cacheRegistry.GetRequired().SchemaId; + cacheRegistry.GetRequired().SchemaId.Should().Be(schemaId); + networkRegistry.GetByDocumentType(typeof(MeshNoteDocument)).Should().NotBeNull(); + networkRegistry.GetByDocumentType(typeof(MeshNoteAliasDocument)).Should().NotBeNull(); + networkRegistry.GetBySchemaId(schemaId)!.DocumentType.Should().Be(typeof(MeshNoteDocument)); + documents.Should().ContainSingle().Which.Text.Should().Be("registry-helper"); + } + [Test] public async Task SnapshotEndpoint_ProvidesTypedHandlesAndSchemaAliases() {