use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use super::refs::Ref;
pub const DIR: &str = "links";
pub const PINNED: &str = "pinned";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Link {
pub from: Ref,
pub to: Ref,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
impl Link {
pub fn new(from: Ref, to: Ref, label: Option<String>) -> Result<Self> {
let link = Self { from, to, label };
link.check()?;
Ok(link)
}
fn check(&self) -> Result<()> {
if self.from == self.to {
bail!("a link joins two things, and {} is one", self.from);
}
if let Some(label) = &self.label {
if label.is_empty() || !label.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
bail!("a link's label is lowercase letters, digits and '-', not {label:?}");
}
}
Ok(())
}
pub fn id(&self) -> String {
use sha2::{Digest, Sha256};
let said = format!("{}\n{}\n{}", self.from, self.to, self.label.as_deref().unwrap_or(""));
let digest = Sha256::digest(said.as_bytes());
digest.iter().take(8).map(|byte| format!("{byte:02x}")).collect()
}
pub fn other_end(&self, end: &Ref) -> Option<&Ref> {
if &self.from == end {
Some(&self.to)
} else if &self.to == end {
Some(&self.from)
} else {
None
}
}
pub fn write(&self, root: &Path) -> Result<PathBuf> {
self.check()?;
let path = path_of(root, &self.id());
let mut text = serde_json::to_string_pretty(self)?;
text.push('\n');
super::write_file(&path, &text)?;
Ok(path)
}
pub fn all(root: &Path) -> Result<Vec<Self>> {
let mut links = Vec::new();
for (id, path) in super::files_with_extension(&dir_of(root), "json")? {
let text = std::fs::read_to_string(&path).with_context(|| format!("cannot read {}", path.display()))?;
let link: Self =
serde_json::from_str(&text).with_context(|| format!("{} is not a link wsb can read", path.display()))?;
link.check().with_context(|| format!("{} is not a link wsb can read", path.display()))?;
if link.id() != id {
bail!(
"{} holds the link {} → {}, whose file is {}.json: it was edited by hand; \
remove it and make the link again",
path.display(),
link.from,
link.to,
link.id()
);
}
links.push(link);
}
links.sort();
Ok(links)
}
}
pub fn dir_of(root: &Path) -> PathBuf {
wsb_prj::settings::dir_of(root).join(DIR)
}
pub fn path_of(root: &Path, id: &str) -> PathBuf {
dir_of(root).join(format!("{id}.json"))
}