CultLib/src/GameCult.Caching/CultDocumentContracts.cs
Meta Vens ba1119f18c Add the engine-free CultCache inspection model
The CultInspector* attributes move from the Unity-only
GameCult.Unity.Caching runtime assembly into GameCult.Caching, so headless
shared models carry them. CultInspectorAssetPath keeps a System.Type the
consumer names. CultInspectorDrawer claims either a value type (open
generic definitions claim every closed form) or an Attribute subclass.

CultInspectorModel owns what an inspector shows and which edits it may
make, with no engine reference:
- members: registry catalog slots for documents, [Key] slots for other
  MessagePack objects, public fields for unkeyed structs; Label, Hidden,
  ReadOnly, Order, Range, TextArea and AssetPath metadata; assignability by
  the rule the registry persists by.
- shapes: string, integer, float, bool, enum, composite (an unkeyed value
  type rebuilt through the constructor whose parameters match its members,
  which covers the CultMath vectors, rect and Color32 without naming
  them), CultRecordRef<T> with candidate records, rank-1 list or array,
  dictionary, union limited to declared [Union] subtypes, nested, and
  unsupported with a reason (multi-dimensional arrays included).
- CultInspectorDrawerClaims: attribute claims, then type claims, then
  built-in; invalid drawers and contested claims are errors and draw
  nothing.
- dictionary keys by serialized identity (a record reference by its key
  string, null and "" alike), refusal of null, empty-reference and
  duplicate keys, fresh keys, and collections rebuilt as new instances.
- CultInspectorEdit: a codec clone of the stored document; Commit upserts
  the copy, so a refused upsert leaves the cached object unchanged.

ICultRecordRef exposes CultRecordRef<T>.Key without T, replacing the
Studio's by-name GetProperty lookup.

Tests (CultInspectorModelTests): member list and metadata, claim
resolution order, declared-only union choices, record-reference
candidates, dictionary key refusal, refused commit leaving the cached
document unchanged, and shape classification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:43:45 +02:00

111 lines
3.2 KiB
C#

using System;
namespace GameCult.Caching
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class CultDocumentAttribute : Attribute
{
public CultDocumentAttribute(string schemaName, string schemaVersion)
{
SchemaName = string.IsNullOrWhiteSpace(schemaName)
? throw new ArgumentException("SchemaName must be non-empty.", nameof(schemaName))
: schemaName;
SchemaVersion = string.IsNullOrWhiteSpace(schemaVersion)
? throw new ArgumentException("SchemaVersion must be non-empty.", nameof(schemaVersion))
: schemaVersion;
}
public string SchemaName { get; }
public string SchemaVersion { get; }
}
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class CultGlobalAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)]
public sealed class CultNameAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class CultIndexAttribute : Attribute
{
public CultIndexAttribute(string? alias = null)
{
Alias = alias;
}
public string? Alias { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)]
public sealed class CultReferenceAttribute : Attribute
{
public CultReferenceAttribute(Type? targetType = null, bool many = false)
{
TargetType = targetType;
Many = many;
}
public Type? TargetType { get; }
public bool Many { get; }
}
public readonly struct CultRecordKey : IEquatable<CultRecordKey>
{
public CultRecordKey(string value)
{
Value = value ?? string.Empty;
}
public string Value { get; }
public override string ToString() => Value;
public bool Equals(CultRecordKey other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
public override bool Equals(object? obj) => obj is CultRecordKey other && Equals(other);
public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
}
public readonly struct CultRecordHandle<T>
{
public CultRecordHandle(CultRecordKey key)
{
Key = key;
}
public CultRecordKey Key { get; }
public override string ToString() => Key.ToString();
}
// The key of any CultRecordRef<T>, for code that holds one without knowing T.
public interface ICultRecordRef
{
CultRecordKey Key { get; }
}
public readonly struct CultRecordRef<T> : ICultRecordRef
{
public CultRecordRef(CultRecordKey key)
{
Key = key;
}
public CultRecordKey Key { get; }
public T? Resolve(CultCache cache)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));
return cache.Get(Key) is T typed ? typed : default;
}
public override string ToString() => Key.ToString();
}
}