use std::{
collections::{BTreeSet, HashMap},
io,
path::Path,
};
use borsh::BorshSerialize;
use chrono::{DateTime, Utc};
use sha2::{Digest, Sha256};
use uuid::Uuid;
pub use crate::storage::markdown::LoadError;
use crate::{domain::Hrid, storage::markdown::MarkdownRequirement};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Requirement {
pub content: Content,
pub metadata: Metadata,
}
#[derive(Debug, BorshSerialize, Clone, PartialEq, Eq)]
pub struct Content {
pub title: String,
pub body: String,
pub tags: BTreeSet<String>,
}
impl Content {
#[must_use]
pub fn as_ref(&self) -> ContentRef<'_> {
ContentRef {
title: &self.title,
body: &self.body,
tags: &self.tags,
}
}
fn fingerprint(&self) -> String {
self.as_ref().fingerprint()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ContentRef<'a> {
pub title: &'a str,
pub body: &'a str,
pub tags: &'a BTreeSet<String>,
}
impl ContentRef<'_> {
#[must_use]
pub fn fingerprint(&self) -> String {
#[derive(BorshSerialize)]
struct FingerprintData<'a> {
body: &'a str,
tags: &'a BTreeSet<String>,
}
let data = FingerprintData {
body: self.body,
tags: self.tags,
};
let encoded = borsh::to_vec(&data).expect("this should never fail");
let hash = Sha256::digest(encoded);
format!("{hash:x}")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Metadata {
pub uuid: Uuid,
pub hrid: Hrid,
pub created: DateTime<Utc>,
pub parents: HashMap<Uuid, Parent>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Parent {
pub hrid: Hrid,
pub fingerprint: String,
}
impl Requirement {
#[must_use]
pub fn new(hrid: Hrid, title: String, body: String) -> Self {
Self::new_with_uuid(hrid, title, body, Uuid::new_v4())
}
pub(crate) fn new_with_uuid(hrid: Hrid, title: String, body: String, uuid: Uuid) -> Self {
let content = Content {
title,
body,
tags: BTreeSet::default(),
};
let metadata = Metadata {
uuid,
hrid,
created: Utc::now(),
parents: HashMap::new(),
};
Self { content, metadata }
}
#[must_use]
pub fn title(&self) -> &str {
&self.content.title
}
#[must_use]
pub fn body(&self) -> &str {
&self.content.body
}
#[must_use]
pub const fn tags(&self) -> &BTreeSet<String> {
&self.content.tags
}
pub fn set_tags(&mut self, tags: BTreeSet<String>) {
self.content.tags = tags;
}
pub fn add_tag(&mut self, tag: String) -> bool {
self.content.tags.insert(tag)
}
#[must_use]
pub const fn hrid(&self) -> &Hrid {
&self.metadata.hrid
}
#[must_use]
pub const fn uuid(&self) -> Uuid {
self.metadata.uuid
}
#[must_use]
pub const fn created(&self) -> DateTime<Utc> {
self.metadata.created
}
#[must_use]
pub fn fingerprint(&self) -> String {
self.content.fingerprint()
}
pub fn add_parent(&mut self, parent_id: Uuid, parent_info: Parent) -> Option<Parent> {
self.metadata.parents.insert(parent_id, parent_info)
}
pub fn parents(&self) -> impl Iterator<Item = (Uuid, &Parent)> {
self.metadata
.parents
.iter()
.map(|(&id, parent)| (id, parent))
}
pub fn parents_mut(&mut self) -> impl Iterator<Item = (Uuid, &mut Parent)> {
self.metadata
.parents
.iter_mut()
.map(|(&id, parent)| (id, parent))
}
pub fn load(
root: &Path,
hrid: &Hrid,
config: &crate::domain::Config,
) -> Result<Self, LoadError> {
Ok(MarkdownRequirement::load(root, hrid, config)?.try_into()?)
}
pub fn save(&self, root: &Path, config: &crate::domain::Config) -> io::Result<()> {
MarkdownRequirement::from(self.clone()).save(root, config)
}
pub fn save_to_path(&self, path: &Path) -> io::Result<()> {
MarkdownRequirement::from(self.clone()).save_to_path(path)
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::Content;
#[test]
fn fingerprint_does_not_panic() {
let content = Content {
title: "Title".to_string(),
body: "Some string".to_string(),
tags: ["tag1".to_string(), "tag2".to_string()].into(),
};
content.fingerprint();
}
#[test]
fn fingerprint_is_stable_with_tag_order() {
let content1 = Content {
title: "Title".to_string(),
body: "Some string".to_string(),
tags: ["tag1".to_string(), "tag2".to_string()].into(),
};
let content2 = Content {
title: "Title".to_string(),
body: "Some string".to_string(),
tags: ["tag2".to_string(), "tag1".to_string()].into(),
};
assert_eq!(content1.fingerprint(), content2.fingerprint());
}
#[test]
fn tags_affect_fingerprint() {
let content1 = Content {
title: "Title".to_string(),
body: "Some string".to_string(),
tags: ["tag1".to_string()].into(),
};
let content2 = Content {
title: "Title".to_string(),
body: "Some string".to_string(),
tags: ["tag1".to_string(), "tag2".to_string()].into(),
};
assert_ne!(content1.fingerprint(), content2.fingerprint());
}
#[test]
fn body_affects_fingerprint() {
let content1 = Content {
title: "Title".to_string(),
body: "Some string".to_string(),
tags: BTreeSet::default(),
};
let content2 = Content {
title: "Title".to_string(),
body: "Other string".to_string(),
tags: BTreeSet::default(),
};
assert_ne!(content1.fingerprint(), content2.fingerprint());
}
#[test]
fn title_does_not_affect_fingerprint() {
let content1 = Content {
title: "Title One".to_string(),
body: "Some string".to_string(),
tags: BTreeSet::default(),
};
let content2 = Content {
title: "Title Two".to_string(),
body: "Some string".to_string(),
tags: BTreeSet::default(),
};
assert_eq!(content1.fingerprint(), content2.fingerprint());
}
}