hyperlog/crates/hyperlog-tui/src/engine.rs

761 lines
19 KiB
Rust
Raw Normal View History

use std::{collections::BTreeMap, fmt::Display};
use anyhow::{anyhow, Context};
use hyperlog_core::log::{Graph, GraphItem, ItemState};
#[derive(Default)]
pub struct Engine {
graph: Graph,
}
impl Engine {
pub fn engine_from_str(input: &str) -> anyhow::Result<Self> {
let graph: Graph = serde_json::from_str(input)?;
Ok(Self { graph })
}
pub fn to_str(&self) -> anyhow::Result<String> {
serde_json::to_string_pretty(&self.graph).context("failed to serialize graph")
}
pub fn create_root(&mut self, root: &str) -> anyhow::Result<()> {
self.graph
.try_insert(root.to_string(), GraphItem::User(BTreeMap::default()))
.map_err(|_| anyhow!("entry was already found, aborting"))?;
Ok(())
}
pub fn create(&mut self, root: &str, path: &[&str], item: GraphItem) -> anyhow::Result<()> {
let graph = &mut self.graph;
let (last, items) = path.split_last().ok_or(anyhow!(
"path cannot be empty, must contain at least one item"
))?;
let root = graph
.get_mut(root)
.ok_or(anyhow!("root was missing a user, aborting"))?;
let mut current_item = root;
for section in items {
match current_item {
GraphItem::User(u) => match u.get_mut(section.to_owned()) {
Some(graph_item) => {
current_item = graph_item;
}
None => anyhow::bail!("path: {} section was not found", section),
},
GraphItem::Item { .. } => anyhow::bail!("path: {} was already found", section),
GraphItem::Section(s) => match s.get_mut(section.to_owned()) {
Some(graph_item) => {
current_item = graph_item;
}
None => anyhow::bail!("path: {} section was not found", section),
},
}
}
match current_item {
GraphItem::User(u) => {
u.insert(last.to_string(), item);
}
GraphItem::Section(s) => {
s.insert(last.to_string(), item);
}
GraphItem::Item { .. } => anyhow::bail!("cannot insert an item into an item"),
}
Ok(())
}
pub fn get(&self, root: &str, path: &[&str]) -> Option<&GraphItem> {
let root = self.graph.get(root)?;
root.get(path)
}
pub fn get_mut(&mut self, root: &str, path: &[&str]) -> Option<&mut GraphItem> {
let root = self.graph.get_mut(root)?;
root.get_mut(path)
}
pub fn take(&mut self, root: &str, path: &[&str]) -> Option<GraphItem> {
let root = self.graph.get_mut(root)?;
root.take(path)
}
pub fn section_move(
&mut self,
root: &str,
src_path: &[&str],
dest_path: &[&str],
) -> anyhow::Result<()> {
let src = self
.take(root, src_path)
.ok_or(anyhow!("failed to find source path"))?;
let dest = self
.get_mut(root, dest_path)
.ok_or(anyhow!("failed to find destination"))?;
let src_item = src_path
.last()
.ok_or(anyhow!("src path must have at least one item"))?;
match dest {
GraphItem::User(u) => {
u.try_insert(src_item.to_string(), src)
.map_err(|_e| anyhow!("key was already found, aborting: {}", src_item))?;
}
GraphItem::Section(s) => {
s.try_insert(src_item.to_string(), src)
.map_err(|_e| anyhow!("key was already found, aborting: {}", src_item))?;
}
GraphItem::Item { .. } => {
anyhow::bail!("failed to insert src at item, item doesn't support arbitrary items")
}
}
Ok(())
}
pub fn delete(&mut self, root: &str, path: &[&str]) -> anyhow::Result<()> {
self.take(root, path)
.map(|_| ())
.ok_or(anyhow!("item was not found"))
}
pub fn toggle_item(&mut self, root: &str, path: &[&str]) -> anyhow::Result<()> {
if let Some(item) = self.get_mut(root, path) {
match item {
GraphItem::Item { state, .. } => match state {
ItemState::NotDone => *state = ItemState::Done,
ItemState::Done => *state = ItemState::NotDone,
},
_ => {
anyhow::bail!("{}.{:?} is not an item", root, path)
}
}
}
Ok(())
}
pub fn update_item(
&mut self,
root: &str,
path: &[&str],
item: &GraphItem,
) -> anyhow::Result<()> {
if let Some((name, dest_last)) = path.split_last() {
if let Some(parent) = self.get_mut(root, dest_last) {
match parent {
GraphItem::User(s) | GraphItem::Section(s) => {
if let Some(mut existing) = s.remove(*name) {
match (&mut existing, item) {
(
GraphItem::Item {
title: ex_title,
description: ex_desc,
state: ex_state,
},
GraphItem::Item {
title,
description,
state,
},
) => {
ex_title.clone_from(title);
ex_desc.clone_from(description);
ex_state.clone_from(state);
let title = title.replace(".", "-");
s.insert(title, existing.clone());
}
_ => {
anyhow::bail!(
"path: {}.{} found is not an item",
root,
path.join(".")
)
}
}
}
}
GraphItem::Item { .. } => {
anyhow::bail!("cannot rename when item is placed in an item")
}
}
}
}
Ok(())
}
pub fn get_roots(&self) -> Option<Vec<String>> {
let items = self.graph.keys().cloned().collect::<Vec<_>>();
if items.is_empty() {
None
} else {
Some(items)
}
}
}
impl Display for Engine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let output = serde_json::to_string_pretty(&self.graph).unwrap();
f.write_str(&output)
}
}
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use hyperlog_core::log::{GraphItem, ItemState};
use similar_asserts::assert_eq;
use super::Engine;
#[test]
fn test_can_create_root() {
let mut engine = Engine::default();
engine.create_root("kjuulh").unwrap();
assert_eq!(
r#"{
"kjuulh": {
"type": "user"
}
}"#,
engine.to_string()
);
}
#[test]
fn test_can_create_section() {
let mut engine = Engine::default();
engine.create_root("kjuulh").unwrap();
engine
.create(
"kjuulh",
&["some-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
assert_eq!(
r#"{
"kjuulh": {
"type": "user",
"some-section": {
"type": "section"
}
}
}"#,
engine.to_string()
);
}
#[test]
fn test_can_create_subsection() {
let mut engine = Engine::default();
engine.create_root("kjuulh").unwrap();
engine
.create(
"kjuulh",
&["some-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
engine
.create(
"kjuulh",
&["some-section", "some-sub-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
assert_eq!(
r#"{
"kjuulh": {
"type": "user",
"some-section": {
"type": "section",
"some-sub-section": {
"type": "section"
}
}
}
}"#,
engine.to_string()
);
}
#[test]
fn test_can_create_item() {
let mut engine = Engine::default();
engine.create_root("kjuulh").unwrap();
engine
.create(
"kjuulh",
&["some-item"],
GraphItem::Item {
title: "some-title".to_string(),
description: "some-description".to_string(),
state: ItemState::NotDone,
},
)
.unwrap();
assert_eq!(
r#"{
"kjuulh": {
"type": "user",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}
}
}"#,
engine.to_string()
);
}
#[test]
fn test_can_create_nested_item() {
let mut engine = Engine::default();
engine.create_root("kjuulh").unwrap();
engine
.create(
"kjuulh",
&["some-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
engine
.create(
"kjuulh",
&["some-section", "some-item"],
GraphItem::Item {
title: "some-title".to_string(),
description: "some-description".to_string(),
state: ItemState::NotDone,
},
)
.unwrap();
assert_eq!(
r#"{
"kjuulh": {
"type": "user",
"some-section": {
"type": "section",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}
}
}
}"#,
engine.to_string()
);
}
#[test]
fn test_can_create_deeply_nested_item() {
let mut engine = Engine::default();
engine.create_root("kjuulh").unwrap();
engine
.create(
"kjuulh",
&["some-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
engine
.create(
"kjuulh",
&["some-section", "some-sub-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
engine
.create(
"kjuulh",
&["some-section", "some-sub-section", "sub-sub-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
engine
.create(
"kjuulh",
&[
"some-section",
"some-sub-section",
"sub-sub-section",
"some-item",
],
GraphItem::Item {
title: "some-title".to_string(),
description: "some-description".to_string(),
state: ItemState::NotDone,
},
)
.unwrap();
assert_eq!(
r#"{
"kjuulh": {
"type": "user",
"some-section": {
"type": "section",
"some-sub-section": {
"type": "section",
"sub-sub-section": {
"type": "section",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}
}
}
}
}
}"#,
engine.to_string()
);
}
#[test]
fn test_can_get_user() {
let engine = get_complex_graph();
let res = engine.get("kjuulh", &[]).unwrap();
let actual = serde_json::to_string_pretty(res).unwrap();
let expected = r#"{
"type": "user",
"some-section": {
"type": "section",
"some-sub-section": {
"type": "section",
"sub-sub-section": {
"type": "section",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}
}
}
}
}"#;
assert_eq!(expected, actual)
}
#[test]
fn test_can_get_first_section() {
let engine = get_complex_graph();
let res = engine.get("kjuulh", &["some-section"]).unwrap();
let actual = serde_json::to_string_pretty(res).unwrap();
let expected = r#"{
"type": "section",
"some-sub-section": {
"type": "section",
"sub-sub-section": {
"type": "section",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}
}
}
}"#;
assert_eq!(expected, actual)
}
#[test]
fn test_can_get_middle_section() {
let engine = get_complex_graph();
let res = engine
.get("kjuulh", &["some-section", "some-sub-section"])
.unwrap();
let actual = serde_json::to_string_pretty(res).unwrap();
let expected = r#"{
"type": "section",
"sub-sub-section": {
"type": "section",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}
}
}"#;
assert_eq!(expected, actual)
}
#[test]
fn test_can_get_last_section() {
let engine = get_complex_graph();
let res = engine
.get(
"kjuulh",
&["some-section", "some-sub-section", "sub-sub-section"],
)
.unwrap();
let actual = serde_json::to_string_pretty(res).unwrap();
let expected = r#"{
"type": "section",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}
}"#;
assert_eq!(expected, actual)
}
#[test]
fn test_can_get_item() {
let engine = get_complex_graph();
let res = engine
.get(
"kjuulh",
&[
"some-section",
"some-sub-section",
"sub-sub-section",
"some-item",
],
)
.unwrap();
let actual = serde_json::to_string_pretty(res).unwrap();
let expected = r#"{
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}"#;
assert_eq!(expected, actual)
}
#[test]
fn test_can_move_item() {
let mut engine = get_complex_graph();
engine
.section_move(
"kjuulh",
&[
"some-section",
"some-sub-section",
"sub-sub-section",
"some-item",
],
&["some-section"],
)
.unwrap();
let expected = r#"{
"kjuulh": {
"type": "user",
"some-section": {
"type": "section",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
},
"some-sub-section": {
"type": "section",
"sub-sub-section": {
"type": "section"
}
}
}
}
}"#;
assert_eq!(expected, engine.to_string())
}
#[test]
fn test_can_move_section() {
let mut engine = get_complex_graph();
engine
.section_move(
"kjuulh",
&["some-section", "some-sub-section", "sub-sub-section"],
&["some-section"],
)
.unwrap();
let expected = r#"{
"kjuulh": {
"type": "user",
"some-section": {
"type": "section",
"some-sub-section": {
"type": "section"
},
"sub-sub-section": {
"type": "section",
"some-item": {
"type": "item",
"title": "some-title",
"description": "some-description",
"state": "not-done"
}
}
}
}
}"#;
assert_eq!(expected, engine.to_string())
}
#[test]
fn test_can_delete_section() {
let mut engine = get_complex_graph();
let res = engine.delete("kjuulh", &["some-section", "some-sub-section"]);
assert!(res.is_ok());
assert_eq!(
r#"{
"kjuulh": {
"type": "user",
"some-section": {
"type": "section"
}
}
}"#,
&engine.to_string()
);
}
#[test]
fn test_can_delete_item() {
let mut engine = get_complex_graph();
let res = engine.delete(
"kjuulh",
&[
"some-section",
"some-sub-section",
"sub-sub-section",
"some-item",
],
);
assert!(res.is_ok());
assert_eq!(
r#"{
"kjuulh": {
"type": "user",
"some-section": {
"type": "section",
"some-sub-section": {
"type": "section",
"sub-sub-section": {
"type": "section"
}
}
}
}
}"#,
&engine.to_string()
);
}
fn get_complex_graph() -> Engine {
let mut engine = Engine::default();
engine.create_root("kjuulh").unwrap();
engine
.create(
"kjuulh",
&["some-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
engine
.create(
"kjuulh",
&["some-section", "some-sub-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
engine
.create(
"kjuulh",
&["some-section", "some-sub-section", "sub-sub-section"],
GraphItem::Section(BTreeMap::default()),
)
.unwrap();
engine
.create(
"kjuulh",
&[
"some-section",
"some-sub-section",
"sub-sub-section",
"some-item",
],
GraphItem::Item {
title: "some-title".to_string(),
description: "some-description".to_string(),
state: ItemState::NotDone,
},
)
.unwrap();
engine
}
}