diff --git a/src/GameCult.Caching.MessagePack.Generator/CultDocumentMessagePackGenerator.cs b/src/GameCult.Caching.MessagePack.Generator/CultDocumentMessagePackGenerator.cs index 7a92cdc4..eeb09475 100644 --- a/src/GameCult.Caching.MessagePack.Generator/CultDocumentMessagePackGenerator.cs +++ b/src/GameCult.Caching.MessagePack.Generator/CultDocumentMessagePackGenerator.cs @@ -9,10 +9,16 @@ using Microsoft.CodeAnalysis.Text; namespace GameCult.Caching.MessagePack.Generator { - [Generator] - public sealed class CultDocumentMessagePackGenerator : IIncrementalGenerator - { - public void Initialize(IncrementalGeneratorInitializationContext context) +/// +/// Emits MessagePack payload serializers for CultCache document types. +/// +[Generator] +public sealed class CultDocumentMessagePackGenerator : IIncrementalGenerator +{ + /// + /// Registers the generator pipeline. + /// + public void Initialize(IncrementalGeneratorInitializationContext context) { var documentShapes = context.SyntaxProvider .CreateSyntaxProvider( diff --git a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs index b4651c95..4a447433 100644 --- a/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs +++ b/src/GameCult.Caching.MessagePack/CultDocumentMessagePackSerialization.cs @@ -7,11 +7,20 @@ using MessagePack.Resolvers; namespace GameCult.Caching.MessagePack; +/// +/// MessagePack resolver for CultCache-specific value types. +/// public sealed class CultDocumentResolver : IFormatterResolver { + /// + /// Gets the shared resolver instance. + /// public static readonly CultDocumentResolver Instance = new(); private CultDocumentResolver() { } + /// + /// Gets a formatter for the requested type, when this resolver owns it. + /// public IMessagePackFormatter? GetFormatter() { var type = typeof(T); @@ -25,12 +34,18 @@ public sealed class CultDocumentResolver : IFormatterResolver } } +/// +/// MessagePack serialization helpers for CultCache documents and backing stores. +/// public static class CultDocumentMessagePackSerialization { private const int PersistedRecordFieldCount = 4; private const int SchemaCatalogEntryFieldCount = 6; private const int StoreSnapshotFieldCount = 3; + /// + /// Gets the shared MessagePack serializer options for CultCache payloads. + /// public static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard .WithResolver(CompositeResolver.Create( @@ -38,16 +53,25 @@ public static class CultDocumentMessagePackSerialization StandardResolver.Instance)) .WithSecurity(MessagePackSecurity.UntrustedData); + /// + /// Serializes a typed value with the CultCache MessagePack options. + /// public static byte[] Serialize(T value) { return MessagePackSerializer.Serialize(value, Options); } + /// + /// Deserializes a typed value with the CultCache MessagePack options. + /// public static T Deserialize(byte[] payload) { return MessagePackSerializer.Deserialize(payload, Options); } + /// + /// Serializes a value whose document type is known at runtime. + /// public static byte[] SerializeUntyped(object value, Type type) { if (value != null) @@ -62,6 +86,9 @@ public static class CultDocumentMessagePackSerialization return MessagePackSerializer.Serialize(type, value, Options); } + /// + /// Deserializes a value whose document type is known at runtime. + /// public static object DeserializeUntyped(Type type, byte[] payload) { var descriptor = CultDocumentRegistry.Shared.GetRequired(type); @@ -73,6 +100,9 @@ public static class CultDocumentMessagePackSerialization return MessagePackSerializer.Deserialize(type, payload, Options); } + /// + /// Serializes one persisted store record. + /// public static byte[] SerializePersistedRecord(CultPersistedRecord record) { var buffer = new global::System.Buffers.ArrayBufferWriter(); @@ -82,12 +112,18 @@ public static class CultDocumentMessagePackSerialization return buffer.WrittenSpan.ToArray(); } + /// + /// Deserializes one persisted store record. + /// public static CultPersistedRecord DeserializePersistedRecord(byte[] payload) { var reader = new MessagePackReader(payload); return ReadPersistedRecord(ref reader); } + /// + /// Serializes a schema catalog. + /// public static byte[] SerializeSchemaCatalog(CultSchemaCatalogEntry[] catalog) { var buffer = new global::System.Buffers.ArrayBufferWriter(); @@ -102,6 +138,9 @@ public static class CultDocumentMessagePackSerialization return buffer.WrittenSpan.ToArray(); } + /// + /// Deserializes a schema catalog. + /// public static CultSchemaCatalogEntry[] DeserializeSchemaCatalog(byte[] payload) { var reader = new MessagePackReader(payload); @@ -115,6 +154,9 @@ public static class CultDocumentMessagePackSerialization return catalog; } + /// + /// Serializes a complete persisted store snapshot. + /// public static byte[] SerializeSnapshot(CultPersistedStoreSnapshot snapshot) { var buffer = new global::System.Buffers.ArrayBufferWriter(); @@ -137,6 +179,9 @@ public static class CultDocumentMessagePackSerialization return buffer.WrittenSpan.ToArray(); } + /// + /// Deserializes a complete persisted store snapshot. + /// public static CultPersistedStoreSnapshot DeserializeSnapshot(byte[] payload) { var reader = new MessagePackReader(payload); @@ -282,27 +327,45 @@ public static class CultDocumentMessagePackSerialization } } +/// +/// Single-file CultCache backing store that persists snapshots as MessagePack. +/// public class SingleFileMessagePackBackingStore : SingleFileBackingStore { + /// + /// Creates a MessagePack single-file backing store. + /// public SingleFileMessagePackBackingStore(string filePath) : base(filePath) { } + /// + /// Serializes a store snapshot. + /// protected override byte[] SerializeSnapshot(CultPersistedStoreSnapshot snapshot) { return CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot); } + /// + /// Deserializes a store snapshot. + /// protected override CultPersistedStoreSnapshot DeserializeSnapshot(byte[] data) { return CultDocumentMessagePackSerialization.DeserializeSnapshot(data); } + /// + /// Serializes one document payload. + /// protected override byte[] SerializePayload(object document) { return CultDocumentMessagePackSerialization.SerializeUntyped(document, document.GetType()); } + /// + /// Deserializes one document payload. + /// protected override object DeserializePayload(Type documentType, byte[] payload) { return CultDocumentMessagePackSerialization.DeserializeUntyped(documentType, payload); diff --git a/src/GameCult.Caching.MessagePack/CultRecordRefFormatter.cs b/src/GameCult.Caching.MessagePack/CultRecordRefFormatter.cs index a5f984fc..3348c1c3 100644 --- a/src/GameCult.Caching.MessagePack/CultRecordRefFormatter.cs +++ b/src/GameCult.Caching.MessagePack/CultRecordRefFormatter.cs @@ -3,13 +3,22 @@ using MessagePack.Formatters; namespace GameCult.Caching; +/// +/// Serializes typed CultCache record references as their persisted key strings. +/// public sealed class CultRecordRefFormatter : IMessagePackFormatter> { + /// + /// Writes a typed record reference to MessagePack. + /// public void Serialize(ref MessagePackWriter writer, CultRecordRef value, MessagePackSerializerOptions options) { options.Resolver.GetFormatterWithVerify().Serialize(ref writer, value.Key.Value, options); } + /// + /// Reads a typed record reference from MessagePack. + /// public CultRecordRef Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { var key = options.Resolver.GetFormatterWithVerify().Deserialize(ref reader, options) ?? string.Empty; diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs index 09ec4e81..e4460ebd 100644 --- a/src/GameCult.Caching/CultCache.cs +++ b/src/GameCult.Caching/CultCache.cs @@ -13,31 +13,82 @@ using R3; namespace GameCult.Caching { + /// + /// Persisted schema metadata embedded in a CultCache backing store. + /// public sealed class CultSchemaCatalogEntry { + /// + /// Gets or sets the content-derived schema identifier. + /// public string SchemaId { get; set; } = string.Empty; + /// + /// Gets or sets the stable schema name. + /// public string SchemaName { get; set; } = string.Empty; + /// + /// Gets or sets the schema version string. + /// public string SchemaVersion { get; set; } = string.Empty; + /// + /// Gets or sets the canonical schema content hash. + /// public string ContentHash { get; set; } = string.Empty; + /// + /// Gets or sets the canonical schema description. + /// public string CanonicalSchemaJson { get; set; } = string.Empty; + /// + /// Gets or sets schema identifiers compatible with this entry. + /// public string[] CompatibleSchemaIds { get; set; } = Array.Empty(); } + /// + /// Persisted serialized document record. + /// public sealed class CultPersistedRecord { + /// + /// Gets or sets the persisted record key. + /// public string Key { get; set; } = string.Empty; + /// + /// Gets or sets the schema identifier used to deserialize the payload. + /// public string SchemaId { get; set; } = string.Empty; + /// + /// Gets or sets the storage timestamp. + /// public string StoredAt { get; set; } = string.Empty; + /// + /// Gets or sets the serialized document payload. + /// public byte[] Payload { get; set; } = Array.Empty(); } + /// + /// Complete snapshot persisted by a single-file CultCache backing store. + /// public sealed class CultPersistedStoreSnapshot { + /// + /// Gets or sets the backing store format version. + /// public string FormatVersion { get; set; } = "cultcache.store.v1"; + /// + /// Gets or sets the embedded schema catalog. + /// public CultSchemaCatalogEntry[] SchemaCatalog { get; set; } = Array.Empty(); + /// + /// Gets or sets the persisted records. + /// public CultPersistedRecord[] Records { get; set; } = Array.Empty(); } + /// + /// Runtime descriptor for a CultCache document type. + /// public sealed class CultDocumentDescriptor { internal CultDocumentDescriptor( @@ -70,20 +121,53 @@ namespace GameCult.Caching Members = members; } + /// + /// Gets the CLR document type. + /// public Type DocumentType { get; } + /// + /// Gets the stable schema name. + /// public string SchemaName { get; } + /// + /// Gets the schema version string. + /// public string SchemaVersion { get; } + /// + /// Gets the content-derived schema identifier. + /// public string SchemaId { get; } + /// + /// Gets the canonical schema content hash. + /// public string ContentHash { get; } + /// + /// Gets the canonical schema description. + /// public string CanonicalSchemaJson { get; } + /// + /// Gets whether this document type stores one global record. + /// public bool IsGlobal { get; } + /// + /// Gets the document name member, if any. + /// public string? NameMember { get; } internal Func? NameAccessor { get; } + /// + /// Gets the generated payload serializer, if one is available. + /// public Func? GeneratedPayloadSerializer { get; } + /// + /// Gets the generated payload deserializer, if one is available. + /// public Func? GeneratedPayloadDeserializer { get; } internal IReadOnlyDictionary> IndexAccessors { get; } internal IReadOnlyList Members { get; } + /// + /// Converts this descriptor into a persisted schema catalog entry. + /// public CultSchemaCatalogEntry ToCatalogEntry() { return new CultSchemaCatalogEntry @@ -118,8 +202,14 @@ namespace GameCult.Caching public string? IndexAlias { get; set; } } + /// + /// Stores a resolved document together with its descriptor and key. + /// public sealed class CultStoredDocument { + /// + /// Creates a stored document wrapper. + /// public CultStoredDocument( CultRecordKey key, string storedAt, @@ -132,12 +222,27 @@ namespace GameCult.Caching Document = document; } + /// + /// Gets the record key. + /// public CultRecordKey Key { get; } + /// + /// Gets the storage timestamp. + /// public string StoredAt { get; } + /// + /// Gets the document descriptor. + /// public CultDocumentDescriptor Descriptor { get; } + /// + /// Gets the document instance. + /// public object Document { get; } } + /// + /// Discovers, indexes, and resolves CultCache document descriptors. + /// public sealed class CultDocumentRegistry { private static readonly Lazy SharedRegistry = @@ -149,15 +254,27 @@ namespace GameCult.Caching private readonly ConcurrentDictionary _bySchemaName = new(StringComparer.Ordinal); + /// + /// Gets the shared process-wide document registry. + /// public static CultDocumentRegistry Shared => SharedRegistry.Value; + /// + /// Creates a registry and discovers currently loaded document metadata. + /// public CultDocumentRegistry() { Refresh(); } + /// + /// Gets all known document descriptors. + /// public IEnumerable AllDescriptors => _byType.Values.OrderBy(d => d.SchemaName, StringComparer.Ordinal); + /// + /// Rebuilds the registry from generated metadata and reflected document attributes. + /// public void Refresh() { _byType.Clear(); @@ -178,6 +295,9 @@ namespace GameCult.Caching } } + /// + /// Gets the descriptor for a document type, building it when needed. + /// public CultDocumentDescriptor GetRequired(Type type) { if (_byType.TryGetValue(type, out var descriptor)) @@ -204,11 +324,17 @@ namespace GameCult.Caching return descriptor; } + /// + /// Gets the descriptor for a document type. + /// public CultDocumentDescriptor GetRequired() where T : class { return GetRequired(typeof(T)); } + /// + /// Gets a descriptor by its schema identifier. + /// public CultDocumentDescriptor GetRequiredBySchemaId(string schemaId) { if (_bySchemaId.TryGetValue(schemaId, out var descriptor)) @@ -219,6 +345,9 @@ namespace GameCult.Caching throw new InvalidOperationException($"Unknown CultCache schema id '{schemaId}'."); } + /// + /// Resolves a persisted schema identifier against the local registry and embedded catalog. + /// public CultDocumentDescriptor ResolvePersistedSchema(string schemaId, IReadOnlyCollection catalog) { if (_bySchemaId.TryGetValue(schemaId, out var exact)) @@ -583,6 +712,9 @@ namespace GameCult.Caching } } + /// + /// In-memory document cache with pluggable persisted backing stores. + /// public sealed class CultCache : IDisposable { private readonly CultDocumentRegistry _registry; @@ -594,24 +726,42 @@ namespace GameCult.Caching private readonly ConditionalWeakTable _documentHandles = new(); private ILogger _logger = new NullLogger(); + /// + /// Creates a cache using the supplied document registry or the shared registry. + /// public CultCache(CultDocumentRegistry? registry = null) { _registry = registry ?? CultDocumentRegistry.Shared; InitializeGlobals(); } + /// + /// Gets or sets the cache logger. + /// public ILogger Logger { get => _logger; set => _logger = value ?? new NullLogger(); } + /// + /// Raised when a backing store adds, updates, or removes a document. + /// public event Action? OnUpdate; + /// + /// Gets all document instances currently held by the cache. + /// public IEnumerable AllEntries => _entries.Values.Select(entry => entry.Document); + /// + /// Gets the document registry used by this cache. + /// public CultDocumentRegistry Registry => _registry; + /// + /// Attaches a backing store to this cache. + /// public void AddBackingStore(CacheBackingStore store) { if (store == null) throw new ArgumentNullException(nameof(store)); @@ -623,6 +773,9 @@ namespace GameCult.Caching _backingStores.Add(store); } + /// + /// Pulls all documents from every attached backing store. + /// public async Task PullAllBackingStoresAsync() { foreach (var store in _backingStores) @@ -633,6 +786,9 @@ namespace GameCult.Caching await Task.CompletedTask; } + /// + /// Adds or replaces a typed document and returns its record handle. + /// public async Task> AddAsync(T document, CultRecordHandle? handle = null) { if (document == null) throw new ArgumentNullException(nameof(document)); @@ -643,6 +799,9 @@ namespace GameCult.Caching return new CultRecordHandle(stored.Key); } + /// + /// Gets the record handle for a document instance, if it is tracked. + /// public CultRecordHandle? TryGetHandle(T document) { if (document == null) throw new ArgumentNullException(nameof(document)); @@ -651,6 +810,9 @@ namespace GameCult.Caching : null; } + /// + /// Gets a document by record key. + /// public object? Get(CultRecordKey key) { return _entries.TryGetValue(key.Value, out var stored) @@ -658,11 +820,17 @@ namespace GameCult.Caching : null; } + /// + /// Gets a typed document by record key. + /// public T? Get(CultRecordKey key) where T : class { return Get(key) as T; } + /// + /// Gets all cached documents assignable to the requested type. + /// public IEnumerable GetAll() where T : class { var type = typeof(T); @@ -671,6 +839,9 @@ namespace GameCult.Caching .Select(entry => (T)entry.Document); } + /// + /// Gets the global document for the requested type, if one exists. + /// public T? GetGlobal() where T : class { return _globalKeys.TryGetValue(typeof(T), out var key) @@ -678,6 +849,9 @@ namespace GameCult.Caching : null; } + /// + /// Gets a typed document by its CultName value. + /// public T? GetByName(string name) where T : class { var type = typeof(T); @@ -690,6 +864,9 @@ namespace GameCult.Caching return null; } + /// + /// Gets a typed document by an indexed value. + /// public T? GetByIndex(string alias, string value) where T : class { if (_indexMaps.TryGetValue((typeof(T), alias), out var map) && @@ -701,11 +878,17 @@ namespace GameCult.Caching return null; } + /// + /// Resolves a typed document reference against this cache. + /// public T? Resolve(CultRecordRef reference) where T : class { return Get(reference.Key); } + /// + /// Removes a document by typed handle. + /// public void Remove(CultRecordHandle handle) { if (_entries.TryGetValue(handle.Key.Value, out var stored)) @@ -714,6 +897,9 @@ namespace GameCult.Caching } } + /// + /// Disposes attached disposable backing stores. + /// public void Dispose() { foreach (var store in _backingStores.OfType()) @@ -904,25 +1090,46 @@ namespace GameCult.Caching } } + /// + /// Base class for CultCache persistence adapters. + /// public abstract class CacheBackingStore : IDisposable { private CultDocumentRegistry? _registry; private ILogger _logger = new NullLogger(); + /// + /// Gets the attached document registry. + /// protected CultDocumentRegistry Registry => _registry ?? throw new InvalidOperationException("Backing store is not attached to a CultDocumentRegistry."); + /// + /// Gets the backing store entries by record key. + /// protected ConcurrentDictionary Entries { get; } = new(StringComparer.Ordinal); + /// + /// Gets or sets the backing store logger. + /// public ILogger Logger { get => _logger; set => _logger = value ?? new NullLogger(); } + /// + /// Publishes documents added by the backing store. + /// public Subject EntryAdded { get; } = new(); + /// + /// Publishes documents updated by the backing store. + /// public Subject EntryUpdated { get; } = new(); + /// + /// Publishes documents deleted by the backing store. + /// public Subject EntryDeleted { get; } = new(); internal void AttachRegistry(CultDocumentRegistry registry) @@ -930,10 +1137,25 @@ namespace GameCult.Caching _registry = registry ?? throw new ArgumentNullException(nameof(registry)); } + /// + /// Pulls all persisted records into the backing store. + /// public abstract void PullAll(); + /// + /// Pushes one stored document into the backing store. + /// public abstract void Push(CultStoredDocument entry); + /// + /// Deletes one stored document from the backing store. + /// public abstract void Delete(CultStoredDocument entry); + /// + /// Persists all current backing store entries. + /// public abstract void PushAll(bool soft = false); + /// + /// Releases backing store event subjects. + /// public virtual void Dispose() { EntryAdded.Dispose(); @@ -941,6 +1163,9 @@ namespace GameCult.Caching EntryDeleted.Dispose(); } + /// + /// Converts a stored document into a persisted record. + /// protected CultPersistedRecord ToPersistedRecord(CultStoredDocument entry, Func serializePayload) { return new CultPersistedRecord @@ -952,6 +1177,9 @@ namespace GameCult.Caching }; } + /// + /// Converts a persisted record into a stored document. + /// protected CultStoredDocument ToStoredDocument( CultPersistedRecord record, IReadOnlyCollection catalog, @@ -967,19 +1195,43 @@ namespace GameCult.Caching } } + /// + /// Base class for backing stores that persist a complete snapshot to one file. + /// public abstract class SingleFileBackingStore : CacheBackingStore { + /// + /// Creates a single-file backing store. + /// protected SingleFileBackingStore(string filePath) { FileInfo = new FileInfo(filePath); } + /// + /// Gets the file used by this backing store. + /// protected FileInfo FileInfo { get; } + /// + /// Serializes a full store snapshot. + /// protected abstract byte[] SerializeSnapshot(CultPersistedStoreSnapshot snapshot); + /// + /// Deserializes a full store snapshot. + /// protected abstract CultPersistedStoreSnapshot DeserializeSnapshot(byte[] data); + /// + /// Serializes one document payload. + /// protected abstract byte[] SerializePayload(object document); + /// + /// Deserializes one document payload. + /// protected abstract object DeserializePayload(Type documentType, byte[] payload); + /// + /// Loads every persisted record from disk. + /// public override void PullAll() { if (!FileInfo.Exists) @@ -996,16 +1248,25 @@ namespace GameCult.Caching } } + /// + /// Stages one stored document in memory. + /// public override void Push(CultStoredDocument entry) { Entries[entry.Key.Value] = entry; } + /// + /// Removes one staged document. + /// public override void Delete(CultStoredDocument entry) { Entries.TryRemove(entry.Key.Value, out _); } + /// + /// Writes the staged snapshot to disk. + /// public override void PushAll(bool soft = false) { var snapshot = new CultPersistedStoreSnapshot diff --git a/src/GameCult.Caching/CultDocumentContracts.cs b/src/GameCult.Caching/CultDocumentContracts.cs index d770b0f1..46dca479 100644 --- a/src/GameCult.Caching/CultDocumentContracts.cs +++ b/src/GameCult.Caching/CultDocumentContracts.cs @@ -2,9 +2,15 @@ using System; namespace GameCult.Caching { + /// + /// Marks a type as a CultCache document with a stable schema name and version. + /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class CultDocumentAttribute : Attribute { + /// + /// Creates a document marker for the supplied schema identity. + /// public CultDocumentAttribute(string schemaName, string schemaVersion) { SchemaName = string.IsNullOrWhiteSpace(schemaName) @@ -15,87 +21,172 @@ namespace GameCult.Caching : schemaVersion; } + /// + /// Gets the stable schema name used by CultCache and CultNet. + /// public string SchemaName { get; } + + /// + /// Gets the semantic schema version string for this document shape. + /// public string SchemaVersion { get; } } + /// + /// Marks a document type as having one global record in a cache. + /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class CultGlobalAttribute : Attribute { } + /// + /// Marks the member that supplies the human-readable document name. + /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] public sealed class CultNameAttribute : Attribute { } + /// + /// Marks a member as an indexed lookup value. + /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] public sealed class CultIndexAttribute : Attribute { + /// + /// Creates an index marker with an optional alias. + /// public CultIndexAttribute(string? alias = null) { Alias = alias; } + /// + /// Gets the external index alias, or null to use the member name. + /// public string? Alias { get; } } + /// + /// Marks a member as a reference to another CultCache document. + /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] public sealed class CultReferenceAttribute : Attribute { + /// + /// Creates a document reference marker. + /// public CultReferenceAttribute(Type? targetType = null, bool many = false) { TargetType = targetType; Many = many; } + /// + /// Gets the explicit referenced document type, when one is required. + /// public Type? TargetType { get; } + + /// + /// Gets whether this member stores many references rather than one reference. + /// public bool Many { get; } } + /// + /// Identifies a record inside a CultCache backing store. + /// public readonly struct CultRecordKey : IEquatable { + /// + /// Creates a record key from its persisted string value. + /// public CultRecordKey(string value) { Value = value ?? string.Empty; } + /// + /// Gets the persisted key value. + /// public string Value { get; } + /// + /// Returns the persisted key value. + /// public override string ToString() => Value; + /// + /// Returns whether this key and another key contain the same persisted value. + /// public bool Equals(CultRecordKey other) => string.Equals(Value, other.Value, StringComparison.Ordinal); + + /// + /// Returns whether this key and another object contain the same persisted value. + /// public override bool Equals(object? obj) => obj is CultRecordKey other && Equals(other); + + /// + /// Returns a hash code for the persisted key value. + /// public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value); } + /// + /// Carries the persisted record key for a typed document. + /// public readonly struct CultRecordHandle { + /// + /// Creates a typed document handle. + /// public CultRecordHandle(CultRecordKey key) { Key = key; } + /// + /// Gets the persisted record key. + /// public CultRecordKey Key { get; } + /// + /// Returns the persisted key value. + /// public override string ToString() => Key.ToString(); } + /// + /// Stores a typed reference to a CultCache document. + /// public readonly struct CultRecordRef { + /// + /// Creates a typed document reference. + /// public CultRecordRef(CultRecordKey key) { Key = key; } + /// + /// Gets the referenced record key. + /// public CultRecordKey Key { get; } + /// + /// Resolves the referenced record against the provided cache. + /// public T? Resolve(CultCache cache) { if (cache == null) throw new ArgumentNullException(nameof(cache)); return cache.Get(Key) is T typed ? typed : default; } + /// + /// Returns the referenced key value. + /// public override string ToString() => Key.ToString(); } } diff --git a/src/GameCult.Caching/CultGeneratedDocumentMetadata.cs b/src/GameCult.Caching/CultGeneratedDocumentMetadata.cs index af241811..7314c87f 100644 --- a/src/GameCult.Caching/CultGeneratedDocumentMetadata.cs +++ b/src/GameCult.Caching/CultGeneratedDocumentMetadata.cs @@ -6,24 +6,45 @@ using System.Text; namespace GameCult.Caching { + /// + /// Registers a generated CultCache document metadata provider with an assembly. + /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public sealed class CultGeneratedDocumentMetadataProviderAttribute : Attribute { + /// + /// Creates an assembly metadata provider registration. + /// public CultGeneratedDocumentMetadataProviderAttribute(Type providerType) { ProviderType = providerType ?? throw new ArgumentNullException(nameof(providerType)); } + /// + /// Gets the provider type that can create generated document definitions. + /// public Type ProviderType { get; } } + /// + /// Supplies generated CultCache document metadata for an assembly. + /// public interface ICultGeneratedDocumentMetadataProvider { + /// + /// Gets the generated document definitions exposed by the provider. + /// IEnumerable GetDocumentDefinitions(); } + /// + /// Describes one generated CultCache document contract. + /// public sealed class CultGeneratedDocumentDefinition { + /// + /// Creates a generated document contract definition. + /// public CultGeneratedDocumentDefinition( Type documentType, string schemaName, @@ -52,20 +73,65 @@ namespace GameCult.Caching Members = members ?? Array.Empty(); } + /// + /// Gets the CLR document type. + /// public Type DocumentType { get; } + + /// + /// Gets the stable CultCache schema name. + /// public string SchemaName { get; } + + /// + /// Gets the schema version string. + /// public string SchemaVersion { get; } + + /// + /// Gets whether this document type stores one global record. + /// public bool IsGlobal { get; } + + /// + /// Gets the member used as the document name, if any. + /// public string? NameMember { get; } + + /// + /// Gets the generated name accessor, if any. + /// public Func? NameAccessor { get; } + + /// + /// Gets the generated payload serializer, if one was emitted. + /// public Func? SerializePayload { get; } + + /// + /// Gets the generated payload deserializer, if one was emitted. + /// public Func? DeserializePayload { get; } + + /// + /// Gets the generated index accessors. + /// public IReadOnlyList IndexAccessors { get; } + + /// + /// Gets the generated persisted member definitions. + /// public IReadOnlyList Members { get; } } + /// + /// Describes a generated accessor for one CultCache index. + /// public sealed class CultGeneratedDocumentIndexAccessor { + /// + /// Creates a generated index accessor. + /// public CultGeneratedDocumentIndexAccessor(string alias, Func accessor) { Alias = string.IsNullOrWhiteSpace(alias) @@ -74,12 +140,25 @@ namespace GameCult.Caching Accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); } + /// + /// Gets the index alias. + /// public string Alias { get; } + + /// + /// Gets the function that extracts the index value. + /// public Func Accessor { get; } } + /// + /// Describes one generated persisted document member. + /// public sealed class CultGeneratedDocumentMemberDefinition { + /// + /// Creates a generated persisted member definition. + /// public CultGeneratedDocumentMemberDefinition( string memberName, int slot, @@ -104,13 +183,44 @@ namespace GameCult.Caching IndexAlias = indexAlias; } + /// + /// Gets the CLR member name. + /// public string MemberName { get; } + + /// + /// Gets the persisted slot number. + /// public int Slot { get; } + + /// + /// Gets the persisted type name. + /// public string TypeName { get; } + + /// + /// Gets whether this member stores a document reference. + /// public bool IsReference { get; } + + /// + /// Gets whether this member stores many references. + /// public bool IsMany { get; } + + /// + /// Gets the target schema name for reference members. + /// public string? TargetSchemaName { get; } + + /// + /// Gets whether this member is the document name. + /// public bool IsName { get; } + + /// + /// Gets the index alias for indexed members. + /// public string? IndexAlias { get; } } diff --git a/src/GameCult.Networking/CultNetDocumentRegistry.cs b/src/GameCult.Networking/CultNetDocumentRegistry.cs index e67cb90b..7b402a4d 100644 --- a/src/GameCult.Networking/CultNetDocumentRegistry.cs +++ b/src/GameCult.Networking/CultNetDocumentRegistry.cs @@ -8,15 +8,36 @@ using GameCult.Caching.MessagePack; namespace GameCult.Networking { + /// + /// Optional metadata applied when creating CultNet document messages. + /// public sealed class CultNetDocumentMessageOptions { + /// + /// Gets or sets an explicit storage timestamp. + /// public string? StoredAt { get; set; } + /// + /// Gets or sets the runtime that produced the document. + /// public string? SourceRuntimeId { get; set; } + /// + /// Gets or sets the agent that produced the document. + /// public string? SourceAgentId { get; set; } + /// + /// Gets or sets the role that produced the document. + /// public string? SourceRole { get; set; } + /// + /// Gets or sets optional document tags. + /// public string[]? Tags { get; set; } } + /// + /// Binds a CultCache document type to CultNet payload serialization. + /// public sealed class CultNetDocumentBinding { internal CultNetDocumentBinding( @@ -31,11 +52,26 @@ namespace GameCult.Networking PayloadDeserializer = payloadDeserializer; } + /// + /// Gets the bound document type. + /// public Type DocumentType { get; } + /// + /// Gets the bound schema identifier. + /// public string SchemaId { get; } + /// + /// Gets the payload serializer. + /// public Func PayloadSerializer { get; } + /// + /// Gets the payload deserializer. + /// public Func PayloadDeserializer { get; } + /// + /// Creates a document binding for a typed CultCache document. + /// public static CultNetDocumentBinding ForDocument( CultDocumentRegistry? registry = null, Func? payloadSerializer = null, @@ -59,6 +95,9 @@ namespace GameCult.Networking } } + /// + /// Creates and applies CultNet messages that replicate CultCache documents. + /// public sealed class CultNetDocumentRegistry { private readonly CultDocumentRegistry _documents; @@ -66,6 +105,9 @@ namespace GameCult.Networking new(StringComparer.Ordinal); private readonly Dictionary _bindingsByType = new(); + /// + /// Creates a document registry with optional bindings. + /// public CultNetDocumentRegistry( CultDocumentRegistry? documents = null, IEnumerable? bindings = null) @@ -82,6 +124,9 @@ namespace GameCult.Networking } } + /// + /// Registers a document binding. + /// public CultNetDocumentRegistry Register(CultNetDocumentBinding binding) { _bindingsBySchemaId[binding.SchemaId] = binding; @@ -89,16 +134,25 @@ namespace GameCult.Networking return this; } + /// + /// Gets a binding by schema identifier. + /// public CultNetDocumentBinding? GetBySchemaId(string schemaId) { return _bindingsBySchemaId.TryGetValue(schemaId, out var binding) ? binding : null; } + /// + /// Gets a binding by CLR document type. + /// public CultNetDocumentBinding? GetByDocumentType(Type documentType) { return _bindingsByType.TryGetValue(documentType, out var binding) ? binding : null; } + /// + /// Creates a document delete message. + /// public CultNetDocumentDeleteMessage CreateDocumentDeleteMessage( string messageId, string schemaId, @@ -112,6 +166,9 @@ namespace GameCult.Networking }; } + /// + /// Creates a raw document put message for a typed document. + /// public CultNetDocumentPutRawMessage CreateRawDocumentPutMessage( string messageId, CultRecordHandle handle, @@ -141,6 +198,9 @@ namespace GameCult.Networking }; } + /// + /// Creates a snapshot request message. + /// public CultNetSnapshotRequestMessage CreateSnapshotRequest( string messageId, IEnumerable? schemaIds = null, @@ -154,6 +214,9 @@ namespace GameCult.Networking }; } + /// + /// Creates a raw snapshot response from the cache. + /// public CultNetSnapshotResponseRawMessage CreateRawSnapshotResponse( CultCache cache, string messageId, @@ -225,6 +288,9 @@ namespace GameCult.Networking }; } + /// + /// Applies a raw document put message to a cache. + /// public async Task ApplyRawDocumentPutMessageAsync( CultCache cache, CultNetDocumentPutRawMessage message) @@ -253,6 +319,9 @@ namespace GameCult.Networking return document; } + /// + /// Applies a raw document put message and returns the typed document. + /// public async Task ApplyRawDocumentPutMessageAsync( CultCache cache, CultNetDocumentPutRawMessage message) @@ -261,6 +330,9 @@ namespace GameCult.Networking return (T)await ApplyRawDocumentPutMessageAsync(cache, message).ConfigureAwait(false); } + /// + /// Applies all documents from a raw snapshot response. + /// public async Task> ApplyRawSnapshotResponseAsync( CultCache cache, CultNetSnapshotResponseRawMessage response) @@ -280,6 +352,9 @@ namespace GameCult.Networking return applied; } + /// + /// Applies all typed documents from a raw snapshot response. + /// public async Task> ApplyRawSnapshotResponseAsync( CultCache cache, CultNetSnapshotResponseRawMessage response) diff --git a/src/GameCult.Networking/CultNetSchemaMessageSerialization.cs b/src/GameCult.Networking/CultNetSchemaMessageSerialization.cs index c32b395e..e48ed310 100644 --- a/src/GameCult.Networking/CultNetSchemaMessageSerialization.cs +++ b/src/GameCult.Networking/CultNetSchemaMessageSerialization.cs @@ -8,21 +8,33 @@ namespace GameCult.Networking /// public static class CultNetSchemaMessageSerialization { + /// + /// Gets the serializer options used for CultNet schema-v0 messages. + /// public static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData); + /// + /// Serializes a typed CultNet schema message. + /// public static byte[] Serialize(T message) where T : ICultNetSchemaMessage { if (message == null) throw new ArgumentNullException(nameof(message)); return MessagePackSerializer.Serialize(message, Options); } + /// + /// Deserializes a typed CultNet schema message. + /// public static T Deserialize(byte[] payload) where T : ICultNetSchemaMessage { if (payload == null) throw new ArgumentNullException(nameof(payload)); return MessagePackSerializer.Deserialize(payload, Options); } + /// + /// Deserializes a CultNet schema message by inspecting its schema version. + /// public static ICultNetSchemaMessage Deserialize(byte[] payload) { if (payload == null) throw new ArgumentNullException(nameof(payload)); @@ -54,7 +66,7 @@ namespace GameCult.Networking }; } - [MessagePackObject] + [MessagePackObject(AllowPrivate = true)] internal sealed class CultNetSchemaHeader { [Key("schemaVersion")] public string SchemaVersion { get; set; } = string.Empty; diff --git a/src/GameCult.Networking/CultNetSchemaMessages.cs b/src/GameCult.Networking/CultNetSchemaMessages.cs index 23ae0b3e..69ff8694 100644 --- a/src/GameCult.Networking/CultNetSchemaMessages.cs +++ b/src/GameCult.Networking/CultNetSchemaMessages.cs @@ -8,7 +8,13 @@ namespace GameCult.Networking /// public static class CultNetWireContracts { + /// + /// schema v0 contract identifier. + /// public const string SchemaV0 = "cultnet.schema.v0"; + /// + /// game cult networking v0 contract identifier. + /// public const string GameCultNetworkingV0 = "gamecult.networking.v0"; } @@ -17,19 +23,61 @@ namespace GameCult.Networking /// public static class CultNetSchemaVersions { + /// + /// hello contract identifier. + /// public const string Hello = "cultnet.hello.v0"; + /// + /// login contract identifier. + /// public const string Login = "cultnet.login.v0"; + /// + /// register contract identifier. + /// public const string Register = "cultnet.register.v0"; + /// + /// verify contract identifier. + /// public const string Verify = "cultnet.verify.v0"; + /// + /// login success contract identifier. + /// public const string LoginSuccess = "cultnet.login_success.v0"; + /// + /// error contract identifier. + /// public const string Error = "cultnet.error.v0"; + /// + /// sample change name contract identifier. + /// public const string SampleChangeName = "cultnet.sample.change_name.v0"; + /// + /// sample chat contract identifier. + /// public const string SampleChat = "cultnet.sample.chat.v0"; + /// + /// document delete contract identifier. + /// public const string DocumentDelete = "cultnet.document_delete.v0"; + /// + /// document put raw contract identifier. + /// public const string DocumentPutRaw = "cultnet.document_put_raw.v0"; + /// + /// snapshot request contract identifier. + /// public const string SnapshotRequest = "cultnet.snapshot_request.v0"; + /// + /// snapshot response raw contract identifier. + /// public const string SnapshotResponseRaw = "cultnet.snapshot_response_raw.v0"; + /// + /// schema catalog request contract identifier. + /// public const string SchemaCatalogRequest = "cultnet.schema_catalog_request.v0"; + /// + /// schema catalog response contract identifier. + /// public const string SchemaCatalogResponse = "cultnet.schema_catalog_response.v0"; } @@ -38,155 +86,413 @@ namespace GameCult.Networking /// public interface ICultNetSchemaMessage { + /// + /// Gets or sets the schema version carried by this message. + /// string SchemaVersion { get; set; } } + /// + /// CultNet hello message used to announce a runtime and its supported contracts. + /// [MessagePackObject] public class CultNetHelloMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Hello; + /// + /// Gets or sets the runtime id. + /// [Key("runtimeId")] public string RuntimeId { get; set; } = string.Empty; + /// + /// Gets or sets the runtime kind. + /// [Key("runtimeKind")] public string RuntimeKind { get; set; } = string.Empty; + /// + /// Gets or sets the agent id. + /// [Key("agentId")] public string? AgentId { get; set; } + /// + /// Gets or sets the role. + /// [Key("role")] public string? Role { get; set; } + /// + /// Gets or sets the display name. + /// [Key("displayName")] public string? DisplayName { get; set; } + /// + /// Gets or sets the supported document types. + /// [Key("supportedDocumentTypes")] public string[]? SupportedDocumentTypes { get; set; } + /// + /// Gets or sets the supported message versions. + /// [Key("supportedMessageVersions")] public string[]? SupportedMessageVersions { get; set; } + /// + /// Gets or sets the supports schema catalog. + /// [Key("supportsSchemaCatalog")] public bool SupportsSchemaCatalog { get; set; } } + /// + /// CultNet login request message. + /// [MessagePackObject] public class CultNetLoginMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Login; + /// + /// Gets or sets the nonce. + /// [Key("nonce")] public string Nonce { get; set; } = string.Empty; + /// + /// Gets or sets the auth. + /// [Key("auth")] public string Auth { get; set; } = string.Empty; + /// + /// Gets or sets the password. + /// [Key("password")] public string Password { get; set; } = string.Empty; } + /// + /// CultNet registration request message. + /// [MessagePackObject] public class CultNetRegisterMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Register; + /// + /// Gets or sets the nonce. + /// [Key("nonce")] public string Nonce { get; set; } = string.Empty; + /// + /// Gets or sets the email. + /// [Key("email")] public string Email { get; set; } = string.Empty; + /// + /// Gets or sets the password. + /// [Key("password")] public string Password { get; set; } = string.Empty; + /// + /// Gets or sets the name. + /// [Key("name")] public string Name { get; set; } = string.Empty; } + /// + /// CultNet session verification request message. + /// [MessagePackObject] public class CultNetVerifyMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Verify; + /// + /// Gets or sets the nonce. + /// [Key("nonce")] public string Nonce { get; set; } = string.Empty; + /// + /// Gets or sets the session. + /// [Key("session")] public string Session { get; set; } = string.Empty; } + /// + /// CultNet message returned after successful session verification. + /// [MessagePackObject] public class CultNetLoginSuccessMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.LoginSuccess; + /// + /// Gets or sets the nonce. + /// [Key("nonce")] public string Nonce { get; set; } = string.Empty; + /// + /// Gets or sets the session. + /// [Key("session")] public string Session { get; set; } = string.Empty; } + /// + /// CultNet error response message. + /// [MessagePackObject] public class CultNetErrorMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Error; + /// + /// Gets or sets the error. + /// [Key("error")] public string Error { get; set; } = string.Empty; } + /// + /// Sample CultNet message used by development clients to change a display name. + /// [MessagePackObject] public class CultNetSampleChangeNameMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SampleChangeName; + /// + /// Gets or sets the name. + /// [Key("name")] public string Name { get; set; } = string.Empty; } + /// + /// Sample CultNet chat message used by development clients. + /// [MessagePackObject] public class CultNetSampleChatMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SampleChat; + /// + /// Gets or sets the text. + /// [Key("text")] public string Text { get; set; } = string.Empty; } + /// + /// Raw document record carried over CultNet. + /// [MessagePackObject] public class CultNetRawDocumentRecord { + /// + /// Gets or sets the schema id. + /// [Key("schemaId")] public string SchemaId { get; set; } = string.Empty; + /// + /// Gets or sets the record key. + /// [Key("recordKey")] public string RecordKey { get; set; } = string.Empty; + /// + /// Gets or sets the stored at. + /// [Key("storedAt")] public string StoredAt { get; set; } = string.Empty; + /// + /// Gets or sets the payload encoding. + /// [Key("payloadEncoding")] public string PayloadEncoding { get; set; } = "messagepack"; + /// + /// Gets or sets the payload. + /// [Key("payload")] public byte[] Payload { get; set; } = Array.Empty(); + /// + /// Gets or sets the source runtime id. + /// [Key("sourceRuntimeId")] public string? SourceRuntimeId { get; set; } + /// + /// Gets or sets the source agent id. + /// [Key("sourceAgentId")] public string? SourceAgentId { get; set; } + /// + /// Gets or sets the source role. + /// [Key("sourceRole")] public string? SourceRole { get; set; } + /// + /// Gets or sets the tags. + /// [Key("tags")] public string[]? Tags { get; set; } } + /// + /// CultNet message requesting deletion of a document record. + /// [MessagePackObject] public class CultNetDocumentDeleteMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.DocumentDelete; + /// + /// Gets or sets the message id. + /// [Key("messageId")] public string MessageId { get; set; } = string.Empty; + /// + /// Gets or sets the schema id. + /// [Key("schemaId")] public string SchemaId { get; set; } = string.Empty; + /// + /// Gets or sets the record key. + /// [Key("recordKey")] public string RecordKey { get; set; } = string.Empty; } + /// + /// CultNet message carrying one raw document record. + /// [MessagePackObject] public class CultNetDocumentPutRawMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.DocumentPutRaw; + /// + /// Gets or sets the message id. + /// [Key("messageId")] public string MessageId { get; set; } = string.Empty; + /// + /// Gets or sets the document. + /// [Key("document")] public CultNetRawDocumentRecord Document { get; set; } = new CultNetRawDocumentRecord(); } + /// + /// CultNet message requesting a document snapshot. + /// [MessagePackObject] public class CultNetSnapshotRequestMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SnapshotRequest; + /// + /// Gets or sets the message id. + /// [Key("messageId")] public string MessageId { get; set; } = string.Empty; + /// + /// Gets or sets the schema ids. + /// [Key("schemaIds")] public string[]? SchemaIds { get; set; } + /// + /// Gets or sets the record keys. + /// [Key("recordKeys")] public string[]? RecordKeys { get; set; } } + /// + /// CultNet message returning raw document records from a snapshot request. + /// [MessagePackObject] public class CultNetSnapshotResponseRawMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SnapshotResponseRaw; + /// + /// Gets or sets the message id. + /// [Key("messageId")] public string MessageId { get; set; } = string.Empty; + /// + /// Gets or sets the documents. + /// [Key("documents")] public CultNetRawDocumentRecord[] Documents { get; set; } = Array.Empty(); } + /// + /// Describes one schema exposed through the CultNet schema catalog. + /// [MessagePackObject] public class CultNetSchemaDescriptor { + /// + /// Gets or sets the schema id. + /// [Key("schemaId")] public string SchemaId { get; set; } = string.Empty; + /// + /// Gets or sets the kind. + /// [Key("kind")] public string Kind { get; set; } = string.Empty; + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string? SchemaVersion { get; set; } + /// + /// Gets or sets the document type. + /// [Key("documentType")] public string? DocumentType { get; set; } + /// + /// Gets or sets the title. + /// [Key("title")] public string? Title { get; set; } + /// + /// Gets or sets the wire contracts. + /// [Key("wireContracts")] public string[] WireContracts { get; set; } = Array.Empty(); + /// + /// Gets or sets the content hash. + /// [Key("contentHash")] public string ContentHash { get; set; } = string.Empty; + /// + /// Gets or sets the schema json. + /// [Key("schemaJson")] public string? SchemaJson { get; set; } } + /// + /// CultNet message requesting schema catalog entries. + /// [MessagePackObject] public class CultNetSchemaCatalogRequestMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SchemaCatalogRequest; + /// + /// Gets or sets the message id. + /// [Key("messageId")] public string MessageId { get; set; } = string.Empty; + /// + /// Gets or sets the include schema json. + /// [Key("includeSchemaJson")] public bool IncludeSchemaJson { get; set; } + /// + /// Gets or sets the schema ids. + /// [Key("schemaIds")] public string[]? SchemaIds { get; set; } + /// + /// Gets or sets the kinds. + /// [Key("kinds")] public string[]? Kinds { get; set; } } + /// + /// CultNet message returning schema catalog entries. + /// [MessagePackObject] public class CultNetSchemaCatalogResponseMessage : ICultNetSchemaMessage { + /// + /// Gets or sets the schema version. + /// [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SchemaCatalogResponse; + /// + /// Gets or sets the message id. + /// [Key("messageId")] public string MessageId { get; set; } = string.Empty; + /// + /// Gets or sets the schemas. + /// [Key("schemas")] public CultNetSchemaDescriptor[] Schemas { get; set; } = Array.Empty(); } } diff --git a/src/GameCult.Networking/CultNetSchemaRegistry.cs b/src/GameCult.Networking/CultNetSchemaRegistry.cs index 21a96ec5..90c2a273 100644 --- a/src/GameCult.Networking/CultNetSchemaRegistry.cs +++ b/src/GameCult.Networking/CultNetSchemaRegistry.cs @@ -17,6 +17,9 @@ namespace GameCult.Networking { private readonly Dictionary _entries = new Dictionary(StringComparer.Ordinal); + /// + /// Creates a schema registry with optional initial entries. + /// public CultNetSchemaRegistry(IEnumerable? entries = null) { if (entries == null) @@ -30,6 +33,9 @@ namespace GameCult.Networking } } + /// + /// Adds or replaces one schema registration. + /// public CultNetSchemaRegistry Register(CultNetSchemaRegistration entry) { if (entry == null) throw new ArgumentNullException(nameof(entry)); @@ -47,6 +53,9 @@ namespace GameCult.Networking return this; } + /// + /// Gets one schema descriptor by schema identifier. + /// public CultNetSchemaDescriptor? Get(string schemaId, bool includeSchemaJson = false) { if (schemaId == null) throw new ArgumentNullException(nameof(schemaId)); @@ -55,6 +64,9 @@ namespace GameCult.Networking : null; } + /// + /// Lists schema descriptors, optionally filtered by identifier or kind. + /// public CultNetSchemaDescriptor[] List( bool includeSchemaJson = false, IEnumerable? schemaIds = null, @@ -75,6 +87,9 @@ namespace GameCult.Networking .ToArray(); } + /// + /// Creates a schema catalog response for a request. + /// public CultNetSchemaCatalogResponseMessage CreateCatalogResponse( CultNetSchemaCatalogRequestMessage request) { @@ -90,6 +105,9 @@ namespace GameCult.Networking }; } + /// + /// Creates a registry populated from embedded CultNet schema resources. + /// public static CultNetSchemaRegistry CreateBuiltIn() { return new CultNetSchemaRegistry(BuiltInSchemaManifest.Select(LoadEmbeddedRegistration)); @@ -98,6 +116,9 @@ namespace GameCult.Networking private static readonly Lazy BuiltInRegistry = new Lazy(CreateBuiltIn); + /// + /// Gets the lazily initialized built-in schema registry. + /// public static CultNetSchemaRegistry BuiltIn => BuiltInRegistry.Value; private static CultNetSchemaRegistration LoadEmbeddedRegistration(SchemaResourceSpec spec) @@ -357,12 +378,33 @@ namespace GameCult.Networking /// public sealed class CultNetSchemaRegistration { + /// + /// Gets or sets the schema identifier. + /// public string SchemaId { get; set; } = string.Empty; + /// + /// Gets or sets the schema kind. + /// public string Kind { get; set; } = string.Empty; + /// + /// Gets or sets the wire contracts implemented by the schema. + /// public string[] WireContracts { get; set; } = Array.Empty(); + /// + /// Gets or sets the schema document. + /// public string SchemaJson { get; set; } = string.Empty; + /// + /// Gets or sets the message or document schema version. + /// public string? SchemaVersion { get; set; } + /// + /// Gets or sets the document type associated with the schema. + /// public string? DocumentType { get; set; } + /// + /// Gets or sets the display title. + /// public string? Title { get; set; } } } diff --git a/src/GameCult.Networking/PlayerData.cs b/src/GameCult.Networking/PlayerData.cs index 1d2ecf6d..f0f19019 100644 --- a/src/GameCult.Networking/PlayerData.cs +++ b/src/GameCult.Networking/PlayerData.cs @@ -4,20 +4,35 @@ using MessagePack; namespace GameCult.Networking { + /// + /// Basic player account document used by the sample networking stack. + /// [CultDocument("gamecult.player_data", "gamecult.player_data.v1")] public class PlayerData { + /// + /// Unique player identifier. + /// [Key(0)] [CultIndex] public Guid PlayerId = Guid.NewGuid(); + /// + /// Player email address. + /// [Key(1)] [CultIndex] public string Email = string.Empty; + /// + /// Stored password hash. + /// [Key(2)] public string PasswordHash = string.Empty; + /// + /// Player display name. + /// [Key(3)] [CultName] public string Username = string.Empty;