Thing/crates/thing-contract/src/result.rs
Meta Vens 852c34b60a thing-contract: the gamecult.thing.* kinds as CultCache entries and unions (first commit, build not yet verified)
Does not build-verified at this commit; tests follow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 03:51:58 +02:00

185 lines
5.2 KiB
Rust

//! The answer to a command invocation. A `CommandResult` is built only by its four
//! constructors, each of which copies the invocation's id, and is read through accessors.
//! Nothing here takes a state as a string, and nothing builds a result from a map.
use cultcache_rs::{CultSlots, CultUnion, DatabaseEntry};
use crate::command::{CommandInvocation, CommandRef};
use crate::keys::{NodeId, result_key};
#[derive(Clone, Debug, PartialEq, CultSlots)]
pub struct PendingOutcome {
#[cultcache(key = 0)]
pub message: String,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
pub struct CompletedOutcome {
#[cultcache(key = 0)]
pub source_version: Option<i64>,
/// Whether the captured drafts of the invocation clear.
#[cultcache(key = 1)]
pub clear_drafts: bool,
#[cultcache(key = 2)]
pub effect: Option<Effect>,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
pub struct DeniedOutcome {
#[cultcache(key = 0)]
pub reason: String,
#[cultcache(key = 1)]
pub node: Option<NodeId>,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
pub struct FailedOutcome {
#[cultcache(key = 0)]
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, CultUnion)]
pub enum Outcome {
#[cultcache(key = 0)]
Pending(PendingOutcome),
#[cultcache(key = 1)]
Completed(CompletedOutcome),
#[cultcache(key = 2)]
Denied(DeniedOutcome),
#[cultcache(key = 3)]
Failed(FailedOutcome),
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
pub struct OpenSurfaceEffect {
#[cultcache(key = 0)]
pub key: String,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
pub struct OpenExternalEffect {
#[cultcache(key = 0)]
pub url: String,
/// The command run when the external step returns.
#[cultcache(key = 1)]
pub resume: CommandRef,
}
#[derive(Clone, Debug, PartialEq, CultUnion)]
pub enum Effect {
#[cultcache(key = 0)]
OpenSurface(OpenSurfaceEffect),
#[cultcache(key = 1)]
OpenExternal(OpenExternalEffect),
}
/// A provider's answer to one invocation. `seq` counts from 1 per invocation.
///
/// The positive case, a result built by a constructor from the invocation it answers:
///
/// ```
/// use thing_contract::{CommandInvocation, CommandResult};
/// fn answer(invocation: &CommandInvocation) -> CommandResult {
/// CommandResult::failed(invocation, 1, "refused")
/// }
/// ```
///
/// A struct literal does not compile, because the fields are private:
///
/// ```compile_fail,E0451
/// use thing_contract::{CommandResult, Outcome, FailedOutcome};
/// let _ = CommandResult {
/// invocation_id: String::new(),
/// seq: 1,
/// outcome: Outcome::Failed(FailedOutcome { reason: String::new() }),
/// };
/// ```
///
/// Neither does assigning the outcome:
///
/// ```compile_fail,E0616
/// use thing_contract::{CommandInvocation, CommandResult, Outcome, FailedOutcome};
/// fn tamper(invocation: &CommandInvocation) {
/// let mut result = CommandResult::failed(invocation, 1, "refused");
/// result.outcome = Outcome::Failed(FailedOutcome { reason: String::new() });
/// }
/// ```
///
/// Nothing converts a map into a result:
///
/// ```compile_fail,E0277
/// use std::collections::BTreeMap;
/// use thing_contract::CommandResult;
/// let flat: BTreeMap<String, String> = BTreeMap::new();
/// let _ = CommandResult::from(flat);
/// ```
///
/// Nor a string state:
///
/// ```compile_fail,E0277
/// use thing_contract::CommandResult;
/// let _ = CommandResult::from("completed");
/// ```
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "gamecult.thing.command_result", schema = "gamecult.thing.command_result.v1")]
pub struct CommandResult {
#[cultcache(key = 0)]
invocation_id: String,
#[cultcache(key = 1)]
seq: i32,
#[cultcache(key = 2)]
outcome: Outcome,
}
impl CommandResult {
fn answering(invocation: &CommandInvocation, seq: i32, outcome: Outcome) -> Self {
Self { invocation_id: invocation.invocation_id.clone(), seq, outcome }
}
pub fn pending(invocation: &CommandInvocation, seq: i32, message: impl Into<String>) -> Self {
Self::answering(invocation, seq, Outcome::Pending(PendingOutcome { message: message.into() }))
}
pub fn completed(
invocation: &CommandInvocation,
seq: i32,
source_version: Option<i64>,
clear_drafts: bool,
effect: Option<Effect>,
) -> Self {
Self::answering(
invocation,
seq,
Outcome::Completed(CompletedOutcome { source_version, clear_drafts, effect }),
)
}
pub fn denied(
invocation: &CommandInvocation,
seq: i32,
reason: impl Into<String>,
node: Option<NodeId>,
) -> Self {
Self::answering(invocation, seq, Outcome::Denied(DeniedOutcome { reason: reason.into(), node }))
}
pub fn failed(invocation: &CommandInvocation, seq: i32, reason: impl Into<String>) -> Self {
Self::answering(invocation, seq, Outcome::Failed(FailedOutcome { reason: reason.into() }))
}
pub fn invocation_id(&self) -> &str {
&self.invocation_id
}
pub fn seq(&self) -> i32 {
self.seq
}
pub fn outcome(&self) -> &Outcome {
&self.outcome
}
pub fn record_key(&self) -> String {
result_key(&self.invocation_id, self.seq)
}
}