Thing/packages/GameCult.Thing.Contract/Domain.cs
Meta Vens e25311a0c0 GameCult.Thing.Contract: every empty memory is an empty RecordPayload; wire nil is refused on read
Present(ReadOnlyMemory<byte>) is deleted. The constructor copies any view, default
included, as an empty payload; a nil on the wire (which decodes to a default view) is
noted by the constructor and refused in OnAfterDeserialize, so the nil-positions rows hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:53:40 +02:00

64 lines
2.9 KiB
C#

using System;
using System.Collections.Generic;
namespace GameCult.Thing.Contract;
/// <summary>A value was outside its domain. Names the domain and its bound, never the value.</summary>
public sealed class ThingDomainException : Exception
{
public ThingDomainException(string domain, string bound) : base($"{domain} must be {bound}")
{
Domain = domain;
Bound = bound;
}
/// <summary>The domain that refused.</summary>
public string Domain { get; }
/// <summary>The bound that was not met.</summary>
public string Bound { get; }
}
/// <summary>
/// The value domains of the contract's numbers, local names and slots. A required slot is never null, and a list is a
/// private copy that holds no nil item. Every constructor whose field carries a domain calls the
/// matching check, and every deserializer reaches a value through that constructor, so no door skips a check.
/// </summary>
public static class ThingDomain
{
/// <summary>A result's position in its invocation's answers, counting from 1.</summary>
public static int Seq(int value) => value >= 1 ? value : throw new ThingDomainException("Seq", "at least 1");
/// <summary>A document or source version, never negative.</summary>
public static long Version(long value) => value >= 0 ? value : throw new ThingDomainException("Version", "at least 0");
public static long? Version(long? value) => value.HasValue ? Version(value.Value) : null;
/// <summary>A count, an extent or an index, never negative.</summary>
public static int Count(int value) => value >= 0 ? value : throw new ThingDomainException("Count", "at least 0");
public static int? Count(int? value) => value.HasValue ? Count(value.Value) : null;
/// <summary>A name the contract joins into a key: non-empty, no '.'.</summary>
public static string Segment(string value) =>
string.IsNullOrEmpty(value) ? throw new ThingDomainException("Segment", "non-empty")
: value.IndexOf('.') >= 0 ? throw new ThingDomainException("Segment", "free of '.'")
: value;
/// <summary>A required slot: the value, or a refusal naming the slot when it is null.</summary>
public static T Present<T>(T? value, string slot) where T : class =>
value ?? throw new ThingDomainException(slot, "present");
/// <summary>
/// A required list slot: a read-only copy, so the caller's list cannot change the value afterwards. A null list or
/// a null item is refused naming the slot.
/// </summary>
public static IReadOnlyList<T> Items<T>(IReadOnlyList<T>? items, string slot) where T : class
{
if (items is null) throw new ThingDomainException(slot, "present");
var copy = new T[items.Count];
for (var i = 0; i < copy.Length; i++)
copy[i] = items[i] ?? throw new ThingDomainException(slot, "free of nil items");
return Array.AsReadOnly(copy);
}
}