Thing/packages/GameCult.Thing.Contract/Result.cs
Meta Vens ee80246cd0 GameCult.Thing.Contract: nil refused in every required slot, lists read-only, slot 2 is InputKind
ThingDomain.Present and Items; every constructor, factory and key builder passes each non-nullable
reference parameter through Present (refusal names the slot, never a value) and each list through
Items (a private read-only copy, nil items refused). Lists are IReadOnlyList on every type.
TextInputNode's slot 2 property and parameter are InputKind, matching Rust input_kind.
Tests: NilPositionTests reads the Rust vector row by row; PresenceTests and ReadOnlyListTests
walk every door by reflection; InputKindNameTests. NameSetTests and the unit-arm rewrite follow.
Unit-arm statics are still present in this commit.

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

132 lines
4.1 KiB
C#

using System.Collections.Generic;
using GameCult.Caching;
using MessagePack;
namespace GameCult.Thing.Contract;
[Union(0, typeof(PendingOutcome))]
[Union(1, typeof(CompletedOutcome))]
[Union(2, typeof(DeniedOutcome))]
[Union(3, typeof(FailedOutcome))]
public abstract class Outcome
{
internal Outcome() { }
}
[MessagePackObject]
public sealed class PendingOutcome : Outcome
{
[Key(0)] public string Message { get; }
public PendingOutcome(string message)
{
Message = ThingDomain.Present(message, "Message");
}
}
[MessagePackObject]
public sealed class CompletedOutcome : Outcome
{
[Key(0)] public long? SourceVersion { get; }
[Key(1)] public bool ClearDrafts { get; }
[Key(2)] public Effect? Effect { get; }
public CompletedOutcome(long? sourceVersion, bool clearDrafts, Effect? effect)
{
SourceVersion = ThingDomain.Version(sourceVersion);
ClearDrafts = clearDrafts;
Effect = effect;
}
}
[MessagePackObject]
public sealed class DeniedOutcome : Outcome
{
[Key(0)] public string Reason { get; }
[Key(1)] public string? Node { get; }
public DeniedOutcome(string reason, string? node)
{
Reason = ThingDomain.Present(reason, "Reason");
Node = node;
}
}
[MessagePackObject]
public sealed class FailedOutcome : Outcome
{
[Key(0)] public string Reason { get; }
public FailedOutcome(string reason)
{
Reason = ThingDomain.Present(reason, "Reason");
}
}
[Union(0, typeof(OpenSurfaceEffect))]
[Union(1, typeof(OpenExternalEffect))]
public abstract class Effect
{
internal Effect() { }
}
[MessagePackObject]
public sealed class OpenSurfaceEffect : Effect
{
[Key(0)] public string Key { get; }
public OpenSurfaceEffect(string key)
{
Key = ThingDomain.Present(key, "Key");
}
}
[MessagePackObject]
public sealed class OpenExternalEffect : Effect
{
[Key(0)] public string Url { get; }
[Key(1)] public CommandRef Resume { get; }
public OpenExternalEffect(string url, CommandRef resume)
{
Url = ThingDomain.Present(url, "Url");
Resume = ThingDomain.Present(resume, "Resume");
}
}
/// <summary>
/// A provider's answer to one invocation. Results are built by the four factories and nothing else: the constructor is
/// private (MessagePack calls it on decode, and it holds the same <see cref="ThingDomain.Seq"/> bound), and no API takes a
/// state as free text or as an untyped value. <c>Seq</c> counts from 1 per invocation. The copied invocation id is a
/// convenience: core binds a result to an invocation it issued, so the copy is not provenance.
/// </summary>
[CultDocument("gamecult.thing.command_result", "gamecult.thing.command_result.v1")]
[MessagePackObject(AllowPrivate = true)]
public sealed class CommandResult
{
[Key(0)] public string InvocationId { get; }
[Key(1)] public int Seq { get; }
[Key(2)] public Outcome Outcome { get; }
[SerializationConstructor]
private CommandResult(string invocationId, int seq, Outcome outcome)
{
InvocationId = ThingDomain.Present(invocationId, "InvocationId");
Seq = ThingDomain.Seq(seq);
Outcome = ThingDomain.Present(outcome, "Outcome");
}
[IgnoreMember] public string RecordKey => ThingKeys.ResultKey(InvocationId, Seq);
public static CommandResult Pending(CommandInvocation invocation, int seq, string message) =>
new(ThingDomain.Present(invocation, "Invocation").InvocationId, seq, new PendingOutcome(message));
public static CommandResult Completed(CommandInvocation invocation, int seq, long? sourceVersion, bool clearDrafts, Effect? effect) =>
new(ThingDomain.Present(invocation, "Invocation").InvocationId, seq, new CompletedOutcome(sourceVersion, clearDrafts, effect));
public static CommandResult Denied(CommandInvocation invocation, int seq, string reason, string? node) =>
new(ThingDomain.Present(invocation, "Invocation").InvocationId, seq, new DeniedOutcome(reason, node));
public static CommandResult Failed(CommandInvocation invocation, int seq, string reason) =>
new(ThingDomain.Present(invocation, "Invocation").InvocationId, seq, new FailedOutcome(reason));
}