use std::path::PathBuf;
use crate::identity::Id;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PeerLocation {
Path(PathBuf),
Url(String),
}
impl std::fmt::Display for PeerLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Path(path) => write!(f, "{}", path.display()),
Self::Url(url) => write!(f, "{url}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unconfirmed {
Unreadable,
Anonymous,
NotChecked,
}
impl std::fmt::Display for Unconfirmed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unreadable => f.write_str("no workspace could be read there"),
Self::Anonymous => f.write_str("that workspace does not name itself"),
Self::NotChecked => f.write_str("its name was not checked"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PeerLookup {
Confirmed(PeerLocation),
Unconfirmed {
location: PeerLocation,
why: Unconfirmed,
},
Mismatched {
location: PeerLocation,
declares: String,
},
Unknown,
}
impl PeerLookup {
pub fn confirm(asked: &str, location: PeerLocation, declares: &str) -> Self {
if declares.is_empty() {
Self::Unconfirmed {
location,
why: Unconfirmed::Anonymous,
}
} else if declares == asked {
Self::Confirmed(location)
} else {
Self::Mismatched {
location,
declares: declares.to_string(),
}
}
}
pub fn unreadable(location: PeerLocation) -> Self {
Self::Unconfirmed {
location,
why: Unconfirmed::Unreadable,
}
}
pub fn unchecked(location: PeerLocation) -> Self {
Self::Unconfirmed {
location,
why: Unconfirmed::NotChecked,
}
}
pub fn followable(&self) -> Option<&PeerLocation> {
match self {
Self::Confirmed(location) => Some(location),
_ => None,
}
}
pub fn followable_unverified(&self) -> Option<&PeerLocation> {
match self {
Self::Confirmed(location) | Self::Unconfirmed { location, .. } => Some(location),
Self::Mismatched { .. } | Self::Unknown => None,
}
}
pub fn location(&self) -> Option<&PeerLocation> {
match self {
Self::Confirmed(location)
| Self::Unconfirmed { location, .. }
| Self::Mismatched { location, .. } => Some(location),
Self::Unknown => None,
}
}
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
pub trait PeerResolver {
fn locate(&self, workspace: &str) -> PeerLookup;
fn locate_document(&self, workspace: &str, id: &Id) -> Option<PeerLocation> {
let _ = (workspace, id);
None
}
}
impl<T: PeerResolver + ?Sized> PeerResolver for &T {
fn locate(&self, workspace: &str) -> PeerLookup {
(**self).locate(workspace)
}
fn locate_document(&self, workspace: &str, id: &Id) -> Option<PeerLocation> {
(**self).locate_document(workspace, id)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoPeers;
impl PeerResolver for NoPeers {
fn locate(&self, _workspace: &str) -> PeerLookup {
PeerLookup::Unknown
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dir(path: &str) -> PeerLocation {
PeerLocation::Path(PathBuf::from(path))
}
#[test]
fn a_workspace_answering_to_the_name_asked_for_is_confirmed() {
assert_eq!(
PeerLookup::confirm("notes", dir("/vaults/notes"), "notes"),
PeerLookup::Confirmed(dir("/vaults/notes"))
);
}
#[test]
fn a_workspace_calling_itself_something_else_is_never_followable() {
let lookup = PeerLookup::confirm("notes", dir("/vaults/journal"), "journal");
assert_eq!(
lookup,
PeerLookup::Mismatched {
location: dir("/vaults/journal"),
declares: "journal".into(),
}
);
assert_eq!(lookup.followable(), None);
assert_eq!(lookup.followable_unverified(), None);
assert_eq!(lookup.location(), Some(&dir("/vaults/journal")));
}
#[test]
fn an_anonymous_peer_is_unconfirmed_rather_than_mismatched() {
let lookup = PeerLookup::confirm("notes", dir("/vaults/notes"), "");
assert_eq!(
lookup,
PeerLookup::Unconfirmed {
location: dir("/vaults/notes"),
why: Unconfirmed::Anonymous,
}
);
assert_eq!(lookup.followable(), None);
assert_eq!(lookup.followable_unverified(), Some(&dir("/vaults/notes")));
}
#[test]
fn an_unchecked_url_is_followable_only_unverified() {
let lookup = PeerLookup::unchecked(PeerLocation::Url("https://diaryx.org".into()));
assert_eq!(lookup.followable(), None);
assert!(lookup.followable_unverified().is_some());
assert!(!lookup.is_unknown());
}
#[test]
fn an_unknown_peer_yields_no_location_at_all() {
let lookup = PeerLookup::Unknown;
assert!(lookup.is_unknown());
assert_eq!(lookup.location(), None);
assert_eq!(lookup.followable_unverified(), None);
}
#[test]
fn no_peers_knows_nothing_and_offers_no_documents() {
let id = Id("ajp7eq".into());
assert_eq!(NoPeers.locate("notes"), PeerLookup::Unknown);
assert_eq!(NoPeers.locate_document("notes", &id), None);
}
#[test]
fn a_resolver_is_usable_through_a_trait_object() {
struct One;
impl PeerResolver for One {
fn locate(&self, workspace: &str) -> PeerLookup {
PeerLookup::confirm(workspace, dir("/vaults/notes"), "notes")
}
}
let erased: &dyn PeerResolver = &One;
assert!(erased.locate("notes").followable().is_some());
assert_eq!(erased.locate("other").followable(), None);
fn ask(peers: impl PeerResolver) -> bool {
peers.locate("notes").followable().is_some()
}
assert!(ask(erased));
assert!(ask(&One));
}
}