use std::sync::{Mutex, PoisonError};
use serde::{Deserialize, Serialize};
use turnframe_core::ids::{OperationKey, TargetToken, WorkflowKey};
use turnframe_core::understanding::{ActId, NotUnderstoodReason, UnitId, UnitKind};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "step", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Step {
Reading {
words: usize,
},
Segmented {
analysis: String,
units: Vec<UnitSummary>,
},
Covered {
added: Vec<UnitSummary>,
},
CrossChecked {
round: u8,
findings: usize,
},
CrossCheckSkipped {
round: u8,
code: String,
},
Routed {
unit: UnitId,
to: Routing,
},
Located {
act: ActId,
record: Located,
},
Extracted {
act: ActId,
given: Vec<(String, String)>,
not_given: Vec<String>,
},
Verified {
act: ActId,
confirmed: bool,
reason: String,
at_fault: Vec<String>,
},
Repairing {
act: ActId,
because: String,
},
Checked {
act: ActId,
refused: Option<(String, String)>,
},
NotUnderstood {
unit: UnitId,
reason: NotUnderstoodReason,
},
Assembled {
ready: Vec<ActId>,
asking: Vec<ActId>,
held: Vec<ActId>,
questions: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnitSummary {
pub id: UnitId,
pub kind: UnitKind,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Routing {
Operation {
operation: OperationKey,
},
Start {
workflow: WorkflowKey,
},
Nothing,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Located {
Record {
token: TargetToken,
label: String,
},
New,
SameTurn {
act: ActId,
},
Card,
NotListed,
Ambiguous,
Nothing,
}
impl Step {
#[must_use]
pub fn describe(&self) -> String {
match self {
Self::Reading { words } => format!("Reading the message ({words} words)."),
Self::Segmented { analysis, units } => {
let listed: Vec<String> = units.iter().map(UnitSummary::describe).collect();
let units = format!("Units: {}.", listed.join("; "));
if analysis.is_empty() {
units
} else {
format!("{analysis} {units}")
}
}
Self::Covered { added } => {
let listed: Vec<String> = added.iter().map(UnitSummary::describe).collect();
format!("The check found more: {}.", listed.join("; "))
}
Self::CrossChecked { round, findings } => match findings {
0 => format!("Checking the whole message, round {round}: it all holds."),
_ => format!(
"Checking the whole message, round {round}: {findings} thing(s) to read again."
),
},
Self::CrossCheckSkipped { round, code } => {
format!("The whole message was not checked in round {round} ({code}).")
}
Self::Routed { unit, to } => match to {
Routing::Operation { operation } => format!("{unit} asks for {operation}."),
Routing::Start { workflow } => format!("{unit} starts a new {workflow}."),
Routing::Nothing => format!("{unit} asks for nothing on offer."),
},
Self::Located { act, record } => match record {
Located::Record { label, .. } => format!("{act} is about {label}."),
Located::New => format!("{act} creates a new record."),
Located::SameTurn { act: earlier } => {
format!("{act} is about the record {earlier} creates.")
}
Located::Card => format!("{act} is about the card on screen."),
Located::NotListed => format!("{act} names a record not in view."),
Located::Ambiguous => format!("{act} could be about more than one record."),
Located::Nothing => format!("{act} is about no record."),
},
Self::Extracted {
act,
given,
not_given,
} => {
let mut parts: Vec<String> = given
.iter()
.map(|(name, value)| format!("{name} = {value}"))
.collect();
parts.extend(not_given.iter().map(|name| format!("{name} not given")));
if parts.is_empty() {
format!("{act} takes no arguments.")
} else {
format!("{act}: {}.", parts.join(", "))
}
}
Self::Verified {
act,
confirmed,
reason,
..
} => {
if *confirmed {
format!("{act} checks out: {reason}")
} else {
format!("{act} does not check out: {reason}")
}
}
Self::Repairing { act, because } => format!("Reading {act} again: {because}"),
Self::Checked { act, refused } => match refused {
Some((argument, reason)) => format!("{act}: {argument} was refused: {reason}"),
None => format!("{act} passes the domain's checks."),
},
Self::NotUnderstood { unit, reason } => {
format!("{unit} was not understood: {}.", reason_text(reason))
}
Self::Assembled {
ready,
asking,
held,
questions,
} => format!(
"{} ready, {} asking for a value, {} held, {questions} questions.",
ready.len(),
asking.len(),
held.len()
),
}
}
}
impl UnitSummary {
fn describe(&self) -> String {
format!("{} {:?} «{}»", self.id, self.kind, self.text)
}
}
fn reason_text(reason: &NotUnderstoodReason) -> String {
match reason {
NotUnderstoodReason::NoOperation => "nothing on offer does it".to_owned(),
NotUnderstoodReason::Unclear => "the readings disagreed".to_owned(),
NotUnderstoodReason::NotRequested => "the user did not ask for it".to_owned(),
NotUnderstoodReason::TaskFailed { task, code } => format!("{task} failed ({code})"),
NotUnderstoodReason::KeptUnchanged { constraint } => {
format!("it would change what {constraint} keeps")
}
_ => "no reason recorded".to_owned(),
}
}
pub trait StepSink: Send + Sync {
fn step(&self, step: Step);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoSteps;
impl StepSink for NoSteps {
fn step(&self, _step: Step) {}
}
#[derive(Debug, Default)]
pub struct RecordedSteps {
steps: Mutex<Vec<Step>>,
}
impl RecordedSteps {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn steps(&self) -> Vec<Step> {
self.steps
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
}
impl StepSink for RecordedSteps {
fn step(&self, step: Step) {
self.steps
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(step);
}
}
#[derive(Debug, Clone)]
pub struct ChannelSteps(tokio::sync::mpsc::UnboundedSender<Step>);
impl ChannelSteps {
#[must_use]
pub fn channel() -> (Self, tokio::sync::mpsc::UnboundedReceiver<Step>) {
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
(Self(sender), receiver)
}
}
impl StepSink for ChannelSteps {
fn step(&self, step: Step) {
let _ = self.0.send(step);
}
}
impl<F: Fn(Step) + Send + Sync> StepSink for F {
fn step(&self, step: Step) {
self(step);
}
}