use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::hash::{Digest, HashError, canonical_digest};
use crate::ids::{OperationKey, OptionId, TargetToken, WorkflowKey};
use crate::plan::AnswerBasis;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UnitId(pub u16);
impl fmt::Display for UnitId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "u{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ActId {
pub unit: UnitId,
pub act: u16,
}
impl ActId {
#[must_use]
pub const fn new(unit: UnitId, act: u16) -> Self {
Self { unit, act }
}
}
impl fmt::Display for ActId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.a{}", self.unit, self.act)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("`{0}` is not an act identifier")]
pub struct ActIdError(pub String);
impl FromStr for ActId {
type Err = ActIdError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let invalid = || ActIdError(text.to_owned());
let (unit, act) = text.split_once(".a").ok_or_else(invalid)?;
let unit = unit.strip_prefix('u').ok_or_else(invalid)?;
Ok(Self {
unit: UnitId(unit.parse().map_err(|_| invalid())?),
act: act.parse().map_err(|_| invalid())?,
})
}
}
impl Serialize for ActId {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for ActId {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MessageRef {
Current,
Earlier {
index: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WordRange {
pub first: usize,
pub last: usize,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Excerpt {
pub message: MessageRef,
pub words: WordRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum UnitKind {
Request,
Question,
Constraint,
Correction,
Cancel,
CardAnswer,
Dispute,
ProvidesValue,
Chitchat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FoundBy {
Segment,
Coverage,
CrossCheck,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Unit {
pub id: UnitId,
pub kind: UnitKind,
pub words: WordRange,
pub workflow: Option<WorkflowKey>,
pub found_by: FoundBy,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ActAction {
Apply {
operation: OperationKey,
},
Start {
workflow: WorkflowKey,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ActTarget {
Record {
token: TargetToken,
},
New {
workflow: WorkflowKey,
},
SameTurn {
act: ActId,
},
Card,
NotListed {
workflow: WorkflowKey,
words: Option<WordRange>,
},
Ambiguous {
candidates: Vec<TargetToken>,
},
Nothing,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RecordValue {
Record {
token: TargetToken,
},
SameTurn {
act: ActId,
},
Named {
workflow: WorkflowKey,
named: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum ArgumentValue {
Json(serde_json::Value),
Record(RecordValue),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnderstoodArgument {
pub value: ArgumentValue,
pub excerpt: Option<Excerpt>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ActStatus {
Ready,
NeedsValue {
arguments: Vec<String>,
reason: Option<String>,
},
Held {
because: UnitId,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnderstoodAct {
pub id: ActId,
pub action: ActAction,
pub target: ActTarget,
pub arguments: BTreeMap<String, UnderstoodArgument>,
pub words: WordRange,
pub depends_on: Vec<ActId>,
pub status: ActStatus,
}
impl UnderstoodAct {
#[must_use]
pub const fn operation(&self) -> Option<&OperationKey> {
match &self.action {
ActAction::Apply { operation } => Some(operation),
ActAction::Start { .. } => None,
}
}
#[must_use]
pub const fn kind_name(&self) -> &'static str {
match self.action {
ActAction::Apply { .. } => "apply_operation",
ActAction::Start { .. } => "start_workflow",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Superseded {
pub act: ActId,
pub action: ActAction,
pub by: UnitId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnderstoodQuestion {
pub unit: UnitId,
pub words: WordRange,
pub workflow: Option<WorkflowKey>,
pub record: Option<TargetToken>,
pub subjects: Vec<String>,
pub basis: AnswerBasis,
#[serde(default)]
pub topic: QuestionTopic,
pub continues_previous: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum QuestionTopic {
#[default]
RecordState,
AcceptedValues,
Capabilities,
Knowledge,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ConstraintKind {
DoNotSubmit,
DoNotDelete,
DraftOnly,
AskBeforeApplying,
ApplyOnlyIf,
NoExternalEffects,
KeepUnchanged,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TurnConstraint {
pub unit: UnitId,
pub kind: ConstraintKind,
pub words: WordRange,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CardAnswer {
pub unit: UnitId,
pub option: OptionId,
pub words: WordRange,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Dispute {
pub unit: UnitId,
pub words: WordRange,
pub receipt: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum NotUnderstoodReason {
NoOperation,
Unclear,
NotRequested,
KeptUnchanged {
constraint: UnitId,
},
TaskFailed {
task: String,
code: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotUnderstood {
pub unit: UnitId,
pub words: WordRange,
pub reason: NotUnderstoodReason,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Unreadable {
Segmentation {
code: String,
},
LostConstraint,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Understanding {
pub units: Vec<Unit>,
pub acts: Vec<UnderstoodAct>,
pub superseded: Vec<Superseded>,
pub questions: Vec<UnderstoodQuestion>,
pub constraints: Vec<TurnConstraint>,
pub card_answer: Option<CardAnswer>,
pub disputes: Vec<Dispute>,
pub not_understood: Vec<NotUnderstood>,
pub unreadable: Option<Unreadable>,
}
impl Understanding {
#[must_use]
pub fn unreadable(reason: Unreadable) -> Self {
Self {
unreadable: Some(reason),
..Self::default()
}
}
#[must_use]
pub fn act(&self, id: ActId) -> Option<&UnderstoodAct> {
self.acts.iter().find(|act| act.id == id)
}
pub fn hash(&self) -> Result<Digest, HashError> {
canonical_digest(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn act_identifiers_read_and_parse_as_unit_and_position() {
let id = ActId::new(UnitId(2), 1);
assert_eq!(id.to_string(), "u2.a1");
assert_eq!("u2.a1".parse::<ActId>().unwrap(), id);
assert!("u2".parse::<ActId>().is_err());
assert!("x2.a1".parse::<ActId>().is_err());
let json = serde_json::to_value(id).unwrap();
assert_eq!(json, serde_json::json!("u2.a1"));
assert_eq!(serde_json::from_value::<ActId>(json).unwrap(), id);
}
}