#![deny(unused, missing_docs, private_in_public)]
use std::{collections::HashMap, path::Path, str::FromStr};
#[cfg(feature = "cbor")]
use std::io;
pub use error::ParseError;
use error::SocraticError;
pub use lexing::Atom;
use lexing::AtomOr;
use serde::{Deserialize, Serialize};
use tracing::{info, info_span, instrument};
mod error;
mod lexing;
mod parsing;
#[derive(Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize, Clone)]
pub struct Atoms<T = String>(pub Vec<Atom<T>>);
impl Atoms<String> {
fn new<I, S>(input: &Vec<AtomOr<String, I>>, state: &mut S) -> Self
where
S: DialogState<Interpolation = I>,
{
let mut atoms = Vec::new();
for atom in input {
match atom {
AtomOr::Atom(a) => atoms.push(a.clone()),
AtomOr::Interpolate(i) => atoms.push(Atom::Text(state.interpolate(i))),
}
}
Self(atoms)
}
}
impl<T: std::fmt::Display> std::fmt::Display for Atoms<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.iter().try_for_each(|a| write!(f, "{a}"))
}
}
impl<T> Atoms<T> {
pub fn iter(&self) -> std::slice::Iter<Atom<T>> {
self.0.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<Atom<T>> {
self.0.iter_mut()
}
}
impl<T> IntoIterator for Atoms<T> {
type Item = Atom<T>;
type IntoIter = std::vec::IntoIter<Atom<T>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a, T> IntoIterator for &'a Atoms<T> {
type Item = &'a Atom<T>;
type IntoIter = std::slice::Iter<'a, Atom<T>>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a, T> IntoIterator for &'a mut Atoms<T> {
type Item = &'a mut Atom<T>;
type IntoIter = std::slice::IterMut<'a, Atom<T>>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename = "d")]
pub struct Dialog<DA, IF, TE> {
#[serde(rename = "s")]
sections: HashMap<String, DialogTree<DA, IF, TE>>,
}
impl<DA, IF, TE> Default for Dialog<DA, IF, TE> {
fn default() -> Self {
Self {
sections: Default::default(),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename = "dt")]
struct DialogTree<DA, IF, TE> {
#[serde(rename = "n")]
nodes: Vec<DialogNode<DA, IF, TE>>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename = "dn")]
enum DialogNode<DA, IF, TE> {
#[serde(rename = "cs")]
CharacterSays(String, Vec<AtomOr<String, TE>>),
#[serde(rename = "m")]
Message(Vec<AtomOr<String, TE>>),
#[serde(rename = "gt")]
GoTo(String),
#[serde(rename = "r")]
#[allow(clippy::type_complexity)]
Responses(Vec<(Vec<AtomOr<String, TE>>, Option<IF>, DialogTree<DA, IF, TE>)>),
#[serde(rename = "da")]
DoAction(DA),
#[serde(rename = "c")]
Conditional(Vec<(Option<IF>, DialogTree<DA, IF, TE>)>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DialogItem {
CharacterSays(String, Atoms),
Message(Atoms),
GoTo(String),
Responses(Vec<Atoms>),
}
impl std::fmt::Display for DialogItem {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use DialogItem::*;
match self {
CharacterSays(ch, atoms) => write!(f, "{ch}: {atoms}"),
Message(atoms) => write!(f, "{atoms}"),
GoTo(gt) => write!(f, "=> {gt}"),
Responses(resp) => write!(f, "Responses: [{resp:?}]"),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct SubIndex {
index: usize,
response: Option<usize>,
inner: Box<Option<SubIndex>>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct DialogIndex {
section: String,
sub: Option<SubIndex>,
}
impl std::fmt::Display for DialogIndex {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.section)?;
if let Some(ref sub) = self.sub {
write!(f, ".{sub}")?;
}
Ok(())
}
}
impl std::fmt::Display for SubIndex {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.index)?;
if let Some(response) = self.response {
write!(f, "[{response}]")?;
}
if let Some(sub) = self.inner.as_ref() {
write!(f, ".{sub}")?;
}
Ok(())
}
}
impl SubIndex {
fn set_response(&mut self, r: usize) {
match self.inner.as_mut() {
Some(ref mut i) => i.set_response(r),
None => self.response = Some(r),
}
}
}
impl DialogIndex {
pub fn set_response(&mut self, r: usize) {
self.sub
.as_mut()
.expect("sub index to not be None")
.set_response(r);
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, thiserror::Error)]
#[error("found duplicate section key: {0}")]
pub struct DuplicateSectionKey(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ValidationError {
#[error("found redirect (=>) that refers to a non existent section `{0}`")]
UnknownSectionGoTo(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub struct ValidationErrors(Vec<ValidationError>);
impl std::fmt::Display for ValidationErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"encountered {} validation error{}:",
self.0.len(),
if self.0.len() == 1 { "" } else { "s" }
)?;
for err in &self.0 {
write!(f, "\n\t{err}")?;
}
Ok(())
}
}
impl<DA, IF, TE> Dialog<DA, IF, TE> {
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> Result<(), ValidationErrors> {
let sections = self.sections.keys().collect::<Vec<_>>();
let mut errors = Vec::new();
self.walk(|node| {
if let DialogNode::GoTo(gt) = node {
if !sections.contains(>) {
errors.push(ValidationError::UnknownSectionGoTo(gt.into()));
}
}
});
if errors.is_empty() {
Ok(())
} else {
Err(ValidationErrors(errors))
}
}
pub fn merge(&mut self, other: Self) -> Result<(), DuplicateSectionKey> {
for (section, data) in other.sections {
if self.sections.contains_key(§ion) {
return Err(DuplicateSectionKey(section));
}
self.sections.insert(section, data);
}
Ok(())
}
#[allow(clippy::type_complexity)]
pub fn parse_str(s: &str) -> Result<Self, SocraticError<DA::Err, IF::Err, TE::Err>>
where
DA: FromStr,
IF: FromStr,
TE: FromStr,
{
Ok(parsing::dialog::<DA, IF, TE>(s)?)
}
#[allow(clippy::type_complexity)]
pub fn parse_from_reader<R>(
mut reader: R,
) -> Result<Self, SocraticError<DA::Err, IF::Err, TE::Err>>
where
R: std::io::Read,
DA: FromStr,
IF: FromStr,
TE: FromStr,
{
let mut s = String::new();
reader.read_to_string(&mut s)?;
Dialog::parse_str(&s)
}
#[allow(clippy::type_complexity)]
pub fn parse_from_file<P>(path: P) -> Result<Self, SocraticError<DA::Err, IF::Err, TE::Err>>
where
P: AsRef<Path>,
DA: FromStr,
IF: FromStr,
TE: FromStr,
{
let f = std::fs::File::open(path)?;
Dialog::parse_from_reader(f)
}
#[cfg(feature = "cbor")]
pub fn packed_to_writer<W>(&self, writer: W) -> Result<(), ciborium::ser::Error<W::Error>>
where
W: ciborium_io::Write,
W::Error: core::fmt::Debug,
DA: Serialize,
IF: Serialize,
TE: Serialize,
{
ciborium::ser::into_writer(&self.sections, writer)
}
#[cfg(feature = "cbor")]
pub fn packed_to_file<P: AsRef<Path>>(
&self,
path: P,
) -> Result<(), ciborium::ser::Error<io::Error>>
where
DA: Serialize,
IF: Serialize,
TE: Serialize,
{
let f = std::fs::File::create(path)?;
self.packed_to_writer(f)
}
#[cfg(feature = "cbor")]
pub fn packed_from_reader<R>(reader: R) -> Result<Self, ciborium::de::Error<R::Error>>
where
R: ciborium_io::Read,
R::Error: core::fmt::Debug,
DA: serde::de::DeserializeOwned,
IF: serde::de::DeserializeOwned,
TE: serde::de::DeserializeOwned,
{
let sections: HashMap<String, DialogTree<DA, IF, TE>> = ciborium::de::from_reader(reader)?;
Ok(Dialog { sections })
}
#[cfg(feature = "cbor")]
pub fn packed_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ciborium::de::Error<io::Error>>
where
DA: serde::de::DeserializeOwned,
IF: serde::de::DeserializeOwned,
TE: serde::de::DeserializeOwned,
{
let f = std::fs::File::open(path)?;
Self::packed_from_reader(f)
}
}
pub trait DialogState {
type DoAction;
type IF;
type Interpolation;
fn do_action(&mut self, command: &Self::DoAction);
fn check_condition(&self, command: &Self::IF) -> bool;
fn interpolate(&self, command: &Self::Interpolation) -> String;
}
impl DialogState for () {
type DoAction = String;
type IF = String;
type Interpolation = String;
fn do_action(&mut self, _command: &String) {}
fn check_condition(&self, _command: &String) -> bool {
true
}
fn interpolate(&self, command: &String) -> String {
command.into()
}
}
impl<DA, IF, TE> Dialog<DA, IF, TE> {
#[instrument(skip(self, state), fields(index = %index))]
pub fn get<S: DialogState<DoAction = DA, IF = IF, Interpolation = TE>>(
&self,
mut index: DialogIndex,
state: &mut S,
) -> Option<(DialogItem, DialogIndex)>
where
DA: std::fmt::Debug,
{
let tree = self.sections.get(&index.section)?;
let (item, sub_index) = tree.get(index.sub, state)?;
index.sub = Some(sub_index);
Some((item, index))
}
#[instrument(skip(self, state))]
pub fn begin<S: DialogState<DoAction = DA, IF = IF, Interpolation = TE>>(
&self,
section: &str,
state: &mut S,
) -> Option<(DialogItem, DialogIndex)>
where
DA: std::fmt::Debug,
{
self.get(
DialogIndex {
section: section.into(),
sub: None,
},
state,
)
}
fn walk<F>(&self, mut cb: F)
where
F: FnMut(&DialogNode<DA, IF, TE>),
{
for tree in self.sections.values() {
tree.walk(&mut cb);
}
}
}
impl<DA, IF, TE> DialogTree<DA, IF, TE> {
fn walk<F>(&self, cb: &mut F)
where
F: FnMut(&DialogNode<DA, IF, TE>),
{
for node in &self.nodes {
cb(node);
match node {
DialogNode::Conditional(parts) => {
for (_, tree) in parts {
tree.walk(cb);
}
}
DialogNode::Responses(responses) => {
for (_, _, tree) in responses {
tree.walk(cb);
}
}
_ => {}
}
}
}
fn get<S: DialogState<DoAction = DA, IF = IF, Interpolation = TE>>(
&self,
index: Option<SubIndex>,
state: &mut S,
) -> Option<(DialogItem, SubIndex)>
where
DA: std::fmt::Debug,
{
let span = match &index {
Some(ref i) => info_span!("get", index = %i),
None => info_span!("get", index = %"None"),
};
let _enter = span.enter();
let mut index = index.unwrap_or_default();
match self.nodes.get(index.index)? {
DialogNode::CharacterSays(character, says) => {
let says = Atoms::new(says, state);
info!("CharacterSays({character}, {says})");
index.index += 1;
Some((DialogItem::CharacterSays(character.clone(), says), index))
}
DialogNode::Message(msg) => {
let msg = Atoms::new(msg, state);
info!("Message({msg})");
index.index += 1;
Some((DialogItem::Message(msg), index))
}
DialogNode::GoTo(gt) => {
info!("GoTo({gt})");
index.index += 1;
Some((DialogItem::GoTo(gt.clone()), index))
}
DialogNode::Responses(responses) => {
let responses = responses
.iter()
.filter(|(_, i, _)| {
i.as_ref().map(|i| state.check_condition(i)).unwrap_or(true)
})
.collect::<Vec<_>>();
info!("Ask...");
if let Some(resp) = index.response {
let response_tree = &responses.get(resp)?.2;
match response_tree.get(*index.inner, state) {
None => {
index.index += 1;
index.response = None;
index.inner = Box::new(None);
self.get(Some(index), state)
}
Some((item, inner)) => {
*index.inner = Some(inner);
Some((item, index))
}
}
} else {
Some((
DialogItem::Responses(
responses
.iter()
.map(|(q, _, _)| Atoms::new(q, state))
.collect(),
),
index,
))
}
}
DialogNode::DoAction(cmd) => {
info!("DoAction({cmd:?})");
index.index += 1;
state.do_action(cmd);
self.get(Some(index), state)
}
DialogNode::Conditional(conditions) => {
if index.response.is_none() {
for (i, (check, _)) in conditions.iter().enumerate() {
if let Some(c) = check {
if state.check_condition(c) {
index.response = Some(i);
}
} else {
index.response = Some(i)
}
}
if index.response.is_none() {
index.index += 1;
return self.get(Some(index), state);
}
}
let resp = index.response.expect("to be not none");
let response_tree = &conditions.get(resp)?.1;
match response_tree.get(*index.inner, state) {
None => {
index.index += 1;
index.response = None;
index.inner = Box::new(None);
self.get(Some(index), state)
}
Some((item, inner)) => {
*index.inner = Some(inner);
Some((item, index))
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
#[cfg(feature = "cbor")]
#[test]
fn test_socrates() -> Result<(), anyhow::Error> {
let s = Dialog::parse_str(
r#"
:: section_name
doot
- hi
test
- hello
test2
- trust issues => section_name
boot
=> dingle
:: dingle
bingle"#,
)?;
let (line, ix) = s.begin("section_name", &mut ()).unwrap();
info!("1 {line} {ix:?}");
let (line, mut ix) = s.get(ix, &mut ()).unwrap();
info!("2 {line} {ix:?}");
ix.set_response(2);
info!("3 {ix:?}");
let (line, ix) = s.get(ix, &mut ()).unwrap();
info!("4 {line} {ix:?}");
let (line, ix) = s.get(ix, &mut ()).unwrap();
info!("5 {line} {ix:?}");
s.packed_to_file("test.txt").unwrap();
let s2 = Dialog::packed_from_file("test.txt").unwrap();
println!("{:?}", s2);
assert_eq!(s, s2);
Ok(())
}
}