use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use prov_graph::error::{Error, Result};
use prov_graph::identity::{Id, NoIdentity};
use prov_graph::index::{IdIndex, NoIndex};
use prov_graph::link;
use prov_graph::peer::{PeerLocation, PeerLookup, PeerResolver, Unconfirmed};
use prov_graph::{Node as GraphNode, NodeKind};
use prov_store::fs::Storage;
use prov_store::index::{FileIndex, IndexStore};
use crate::discovery::{Discovered, Discovery, discover};
use crate::workspace::{Settings, Workspace};
pub const DEFAULT_DEPTH: usize = 8;
#[derive(Debug)]
pub struct Peer<FS> {
pub name: String,
pub location: PeerLocation,
pub discovered: Discovered,
pub workspace: Workspace<FS, NoIdentity, FileIndex>,
}
impl<FS> Peer<FS> {
pub fn declares(&self) -> &str {
&self.discovered.config.workspace_id
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Crossing<FS> {
Opened(Peer<FS>),
Refused(Refusal),
}
impl<FS> Crossing<FS> {
pub fn opened(self) -> Option<Peer<FS>> {
match self {
Self::Opened(peer) => Some(peer),
Self::Refused(_) => None,
}
}
pub fn refusal(&self) -> Option<&Refusal> {
match self {
Self::Refused(refusal) => Some(refusal),
Self::Opened(_) => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Trust {
#[default]
Confirmed,
Unverified,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refusal {
Unknown,
Unconfirmed {
location: PeerLocation,
why: Unconfirmed,
},
Mismatched {
location: PeerLocation,
declares: String,
},
Url(String),
Unopenable {
location: PeerLocation,
reason: String,
},
Unregistered {
workspace: String,
id: Id,
},
Cycle {
workspace: String,
root_dir: PathBuf,
},
TooDeep,
}
impl std::fmt::Display for Refusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unknown => f.write_str("no workspace of that name is on record"),
Self::Unconfirmed { location, why } => write!(f, "{location} is unconfirmed: {why}"),
Self::Mismatched { location, declares } => {
write!(f, "{location} calls itself `{declares}`")
}
Self::Url(url) => write!(f, "{url} is a URL, and prov reads nothing over the network"),
Self::Unopenable { location, reason } => {
write!(f, "{location} could not be opened: {reason}")
}
Self::Unregistered { workspace, id } => {
write!(f, "`{workspace}` has no document registered as `{id}`")
}
Self::Cycle {
workspace,
root_dir,
} => write!(
f,
"`{workspace}` at {} is already on the path being descended",
root_dir.display()
),
Self::TooDeep => f.write_str("the descent's crossing bound was reached"),
}
}
}
pub async fn open_discovered<FS: Storage + Clone>(
fs: &FS,
discovered: &Discovered,
) -> Result<Workspace<FS, NoIdentity, FileIndex>> {
let index = if discovered.config.id_storage.keeps_registry() {
match &discovered.registry {
Some(rel) => {
let text = match fs.read_to_string(&discovered.root_dir.join(rel)).await {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(e) => return Err(Error::Io(e)),
};
FileIndex::parse(rel, &text)?
}
None => FileIndex::new(discovered.config.default_embed_format),
}
} else {
let probe: Workspace<FS, NoIdentity, NoIndex> = Workspace::builder(fs.clone())
.root(&discovered.root_dir)
.build();
let mut index = FileIndex::new(discovered.config.default_embed_format);
for (id, path) in probe.scan_ids().await? {
index.register(&id, &path);
}
index.mark_clean();
index
};
Ok(Workspace::builder(fs.clone())
.root(&discovered.root_dir)
.settings(Settings::from(&discovered.config))
.index(index)
.build())
}
pub async fn open_peer<FS: Storage + Clone>(
fs: &FS,
peers: &dyn PeerResolver,
workspace: &str,
trust: Trust,
) -> Result<Crossing<FS>> {
let lookup = peers.locate(workspace);
let followable = match trust {
Trust::Confirmed => lookup.followable(),
Trust::Unverified => lookup.followable_unverified(),
};
let Some(location) = followable.cloned() else {
return Ok(Crossing::Refused(match lookup {
PeerLookup::Unknown => Refusal::Unknown,
PeerLookup::Unconfirmed { location, why } => Refusal::Unconfirmed { location, why },
PeerLookup::Mismatched { location, declares } => {
Refusal::Mismatched { location, declares }
}
PeerLookup::Confirmed(location) => Refusal::Unconfirmed {
location,
why: Unconfirmed::NotChecked,
},
}));
};
let root = match &location {
PeerLocation::Url(url) => return Ok(Crossing::Refused(Refusal::Url(url.clone()))),
PeerLocation::Path(root) => root.clone(),
};
let discovered = match discover(fs, &root).await {
Ok(Discovery::Found(discovered)) => discovered,
Ok(Discovery::Ambiguous { dir, candidates }) => {
return Ok(unopenable(
location,
format!(
"ambiguous workspace root in {}: {}",
dir.display(),
candidates.join(", ")
),
));
}
Ok(Discovery::NotFound) => {
return Ok(unopenable(location, "no workspace root there".to_string()));
}
Err(e) => return Ok(unopenable(location, e.to_string())),
};
if link::normalize(&discovered.root_dir) != link::normalize(&root) {
return Ok(unopenable(
location,
format!(
"that directory is not a workspace root (the nearest one is {})",
discovered.root_dir.display()
),
));
}
let declares = discovered.config.workspace_id.clone();
if !declares.is_empty() && declares != workspace {
return Ok(Crossing::Refused(Refusal::Mismatched {
location,
declares,
}));
}
if declares.is_empty() && trust == Trust::Confirmed {
return Ok(Crossing::Refused(Refusal::Unconfirmed {
location,
why: Unconfirmed::Anonymous,
}));
}
let opened = open_discovered(fs, &discovered).await?;
Ok(Crossing::Opened(Peer {
name: workspace.to_string(),
location,
discovered,
workspace: opened,
}))
}
fn unopenable<FS>(location: PeerLocation, reason: String) -> Crossing<FS> {
Crossing::Refused(Refusal::Unopenable { location, reason })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Descent {
pub trust: Trust,
pub depth: usize,
}
impl Default for Descent {
fn default() -> Self {
Self {
trust: Trust::default(),
depth: DEFAULT_DEPTH,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reached {
pub name: String,
pub declares: String,
pub root_dir: PathBuf,
pub root_doc: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Boundary {
Followed {
into: usize,
},
Refused(Refusal),
}
#[derive(Debug, Clone)]
pub struct Node {
pub workspace: usize,
pub path: PathBuf,
pub title: Option<String>,
pub label: Option<String>,
pub kind: NodeKind,
pub children: Vec<Node>,
pub boundary: Option<Boundary>,
}
#[derive(Debug, Clone)]
pub struct Federation {
pub tree: Node,
pub workspaces: Vec<Reached>,
}
pub async fn descend<FS, IdP, Ix>(
ws: &Workspace<FS, IdP, Ix>,
start: &Path,
peers: &dyn PeerResolver,
options: &Descent,
) -> Result<Federation>
where
FS: Storage + Clone,
Ix: IdIndex,
{
let origin_root = ws.root().to_path_buf();
let declares = ws.workspace_id().to_string();
let root_doc = ws
.root_document()
.await?
.unwrap_or_else(|| start.to_path_buf());
let mut walk = Walk {
fs: ws.fs(),
peers,
options,
workspaces: vec![Reached {
name: declares.clone(),
declares: declares.clone(),
root_dir: origin_root.clone(),
root_doc,
}],
opened: vec![None],
memo: BTreeMap::new(),
trail: vec![(link::normalize(&origin_root), declares)],
};
let tree = ws.tree(start).await?;
let tree = walk.convert(tree, 0, 0).await?;
Ok(Federation {
tree,
workspaces: walk.workspaces,
})
}
struct Walk<'a, FS> {
fs: &'a FS,
peers: &'a dyn PeerResolver,
options: &'a Descent,
workspaces: Vec<Reached>,
opened: Vec<Option<Peer<FS>>>,
memo: BTreeMap<String, std::result::Result<usize, Refusal>>,
trail: Vec<(PathBuf, String)>,
}
impl<'w, FS: Storage + Clone> Walk<'w, FS> {
fn convert<'a>(
&'a mut self,
node: GraphNode,
ws: usize,
crossings: usize,
) -> Pin<Box<dyn Future<Output = Result<Node>> + 'a>> {
Box::pin(async move {
if let NodeKind::Foreign { workspace, id } = &node.kind {
let (name, id) = (workspace.clone(), id.clone());
return self.cross(node, name, id, ws, crossings).await;
}
let GraphNode {
path,
title,
label,
kind,
children: graph_children,
} = node;
let mut children = Vec::with_capacity(graph_children.len());
for child in graph_children {
children.push(self.convert(child, ws, crossings).await?);
}
Ok(Node {
workspace: ws,
path,
title,
label,
kind,
children,
boundary: None,
})
})
}
fn cross<'a>(
&'a mut self,
node: GraphNode,
name: String,
id: Id,
ws: usize,
crossings: usize,
) -> Pin<Box<dyn Future<Output = Result<Node>> + 'a>> {
Box::pin(async move {
if crossings >= self.options.depth {
return Ok(refused(node, ws, Refusal::TooDeep));
}
let peer = match self.reach(&name).await? {
Ok(peer) => peer,
Err(refusal) => return Ok(refused(node, ws, refusal)),
};
let key = (
link::normalize(&self.workspaces[peer].root_dir),
self.workspaces[peer].declares.clone(),
);
if self.on_trail(&key) {
return Ok(refused(
node,
ws,
Refusal::Cycle {
workspace: name,
root_dir: self.workspaces[peer].root_dir.clone(),
},
));
}
let Some(path) = self.opened[peer]
.as_ref()
.expect("a reached peer was opened")
.workspace
.index()
.resolve(&id)
else {
return Ok(refused(
node,
ws,
Refusal::Unregistered {
workspace: name,
id,
},
));
};
let subtree = self.opened[peer]
.as_ref()
.expect("a reached peer was opened")
.workspace
.tree(&path)
.await?;
self.trail.push(key);
let crossed = self.convert(subtree, peer, crossings + 1).await;
self.trail.pop();
let mut crossed = crossed?;
crossed.label = node.label;
crossed.boundary = Some(Boundary::Followed { into: peer });
Ok(crossed)
})
}
fn on_trail(&self, (dir, declares): &(PathBuf, String)) -> bool {
self.trail.iter().any(|(seen_dir, seen_declares)| {
seen_dir == dir || (!declares.is_empty() && seen_declares == declares)
})
}
async fn reach(&mut self, name: &str) -> Result<std::result::Result<usize, Refusal>> {
if let Some(outcome) = self.memo.get(name) {
return Ok(outcome.clone());
}
let outcome = match open_peer(self.fs, self.peers, name, self.options.trust).await? {
Crossing::Refused(refusal) => Err(refusal),
Crossing::Opened(peer) => {
let dir = link::normalize(&peer.discovered.root_dir);
let declares = peer.declares().to_string();
let already = self.workspaces.iter().position(|reached| {
link::normalize(&reached.root_dir) == dir
|| (!declares.is_empty() && reached.declares == declares)
});
match already {
Some(at) => {
self.opened[at].get_or_insert(peer);
Ok(at)
}
None => {
self.workspaces.push(Reached {
name: name.to_string(),
declares,
root_dir: peer.discovered.root_dir.clone(),
root_doc: peer.discovered.root_doc.clone(),
});
self.opened.push(Some(peer));
Ok(self.workspaces.len() - 1)
}
}
}
};
self.memo.insert(name.to_string(), outcome.clone());
Ok(outcome)
}
}
fn refused(node: GraphNode, ws: usize, refusal: Refusal) -> Node {
Node {
workspace: ws,
path: node.path,
title: node.title,
label: node.label,
kind: node.kind,
children: Vec::new(),
boundary: Some(Boundary::Refused(refusal)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use prov_graph::NoPeers;
use prov_graph::exec::block_on;
use prov_graph::fs::StdFs;
use prov_testkit::write;
fn tmp(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("prov-crossing-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn org(dir: &Path, contents: &[&str]) {
write(
dir,
"org/prov.yaml",
"workspace_id: org\nroot: README.md\nid_storage: frontmatter\n",
);
let mut doc = String::from("---\nid: org1\ntitle: Org\ncontents:\n");
for entry in contents {
doc.push_str(&format!("- '{entry}'\n"));
}
doc.push_str("---\n");
write(dir, "org/README.md", doc);
}
fn sub(dir: &Path, at: &str, declares: Option<&str>, id: &str, title: &str, contents: &[&str]) {
let mut node = String::from("root: README.md\nid_storage: frontmatter\n");
if let Some(name) = declares {
node.push_str(&format!("workspace_id: {name}\n"));
}
write(dir, &format!("{at}/prov.yaml"), node);
let mut doc = format!("---\nid: {id}\ntitle: {title}\npart_of: 'id:org/org1'\n");
if !contents.is_empty() {
doc.push_str("contents:\n");
for entry in contents {
doc.push_str(&format!("- '{entry}'\n"));
}
}
doc.push_str("---\n");
write(dir, &format!("{at}/README.md"), doc);
}
fn federation(tag: &str) -> PathBuf {
let dir = tmp(tag);
org(&dir, &["id:alpha/alpha1", "id:beta/beta1"]);
sub(
&dir,
"alpha",
Some("alpha"),
"alpha1",
"Alpha",
&["notes.md"],
);
write(
&dir,
"alpha/notes.md",
"---\nid: alpha2\ntitle: Alpha notes\npart_of: README.md\n---\n",
);
sub(&dir, "beta", Some("beta"), "beta1", "Beta", &[]);
dir
}
fn open(dir: &Path) -> Workspace<StdFs, NoIdentity, FileIndex> {
match block_on(discover(&StdFs, dir)).unwrap() {
Discovery::Found(found) => block_on(open_discovered(&StdFs, &found)).unwrap(),
other => panic!("expected a workspace at {}, got {other:?}", dir.display()),
}
}
enum At {
Dir(PathBuf),
Claimed(PathBuf),
Url { url: String, confirmed: bool },
}
struct Peers(BTreeMap<String, At>);
fn peers(entries: Vec<(&str, At)>) -> Peers {
Peers(
entries
.into_iter()
.map(|(name, at)| (name.to_string(), at))
.collect(),
)
}
impl PeerResolver for Peers {
fn locate(&self, workspace: &str) -> PeerLookup {
match self.0.get(workspace) {
None => PeerLookup::Unknown,
Some(At::Claimed(root)) => PeerLookup::Confirmed(PeerLocation::Path(root.clone())),
Some(At::Url { url, confirmed }) => {
let location = PeerLocation::Url(url.clone());
if *confirmed {
PeerLookup::Confirmed(location)
} else {
PeerLookup::unchecked(location)
}
}
Some(At::Dir(root)) => {
let location = PeerLocation::Path(root.clone());
match block_on(discover(&StdFs, root)) {
Ok(Discovery::Found(found)) => {
PeerLookup::confirm(workspace, location, &found.config.workspace_id)
}
_ => PeerLookup::unreadable(location),
}
}
}
}
}
fn names(federation: &Federation) -> Vec<&str> {
federation
.workspaces
.iter()
.map(|reached| reached.name.as_str())
.collect()
}
fn same_shape(plain: &GraphNode, crossed: &Node) {
assert_eq!(plain.path, crossed.path);
assert_eq!(plain.title, crossed.title);
assert_eq!(plain.label, crossed.label);
assert_eq!(plain.kind, crossed.kind);
assert_eq!(plain.children.len(), crossed.children.len());
for (plain, crossed) in plain.children.iter().zip(&crossed.children) {
same_shape(plain, crossed);
}
}
#[test]
fn no_peers_refuses_every_foreign_leaf_and_changes_nothing_else() {
let dir = federation("no-peers");
let ws = open(&dir.join("org"));
let plain = block_on(ws.tree("README.md")).unwrap();
let federated = block_on(descend(
&ws,
Path::new("README.md"),
&NoPeers,
&Descent::default(),
))
.unwrap();
same_shape(&plain, &federated.tree);
assert_eq!(names(&federated), ["org"]);
assert_eq!(federated.workspaces[0].root_dir, dir.join("org"));
assert_eq!(federated.tree.children.len(), 2);
for child in &federated.tree.children {
assert!(matches!(child.kind, NodeKind::Foreign { .. }));
assert_eq!(child.workspace, 0);
assert_eq!(child.boundary, Some(Boundary::Refused(Refusal::Unknown)));
}
}
#[test]
fn a_confirmed_peer_hangs_its_own_subtree_where_the_reference_was() {
let dir = federation("follow");
let ws = open(&dir.join("org"));
let map = peers(vec![
("alpha", At::Dir(dir.join("alpha"))),
("beta", At::Dir(dir.join("beta"))),
]);
let federated = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent::default(),
))
.unwrap();
assert_eq!(names(&federated), ["org", "alpha", "beta"]);
assert_eq!(federated.workspaces[1].root_dir, dir.join("alpha"));
assert_eq!(federated.workspaces[1].root_doc, Path::new("README.md"));
assert_eq!(federated.workspaces[1].declares, "alpha");
assert_eq!(federated.workspaces[2].root_dir, dir.join("beta"));
let alpha = &federated.tree.children[0];
assert_eq!(alpha.boundary, Some(Boundary::Followed { into: 1 }));
assert_eq!(alpha.workspace, 1);
assert_eq!(alpha.path, Path::new("README.md"));
assert_eq!(alpha.title.as_deref(), Some("Alpha"));
assert_eq!(alpha.kind, NodeKind::Doc);
assert_eq!(alpha.children.len(), 1);
assert_eq!(alpha.children[0].path, Path::new("notes.md"));
assert_eq!(alpha.children[0].workspace, 1);
assert_eq!(alpha.children[0].boundary, None);
let beta = &federated.tree.children[1];
assert_eq!(beta.boundary, Some(Boundary::Followed { into: 2 }));
assert_eq!(beta.workspace, 2);
assert_eq!(beta.title.as_deref(), Some("Beta"));
assert!(beta.children.is_empty());
}
#[test]
fn an_org_that_names_two_documents_of_one_peer_renders_both() {
let dir = tmp("two-documents");
org(&dir, &["id:alpha/alpha1", "id:alpha/alpha2"]);
sub(
&dir,
"alpha",
Some("alpha"),
"alpha1",
"Alpha",
&["notes.md"],
);
write(
&dir,
"alpha/notes.md",
"---\nid: alpha2\ntitle: Alpha notes\npart_of: README.md\n---\n",
);
let ws = open(&dir.join("org"));
let map = peers(vec![("alpha", At::Dir(dir.join("alpha")))]);
let federated = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent::default(),
))
.unwrap();
assert_eq!(names(&federated), ["org", "alpha"]);
assert_eq!(federated.tree.children.len(), 2);
let readme = &federated.tree.children[0];
assert_eq!(readme.boundary, Some(Boundary::Followed { into: 1 }));
assert_eq!(readme.path, Path::new("README.md"));
assert_eq!(readme.children.len(), 1);
assert_eq!(readme.children[0].path, Path::new("notes.md"));
let notes = &federated.tree.children[1];
assert_eq!(notes.boundary, Some(Boundary::Followed { into: 1 }));
assert_eq!(notes.workspace, 1);
assert_eq!(notes.path, Path::new("notes.md"));
assert_eq!(notes.title.as_deref(), Some("Alpha notes"));
assert!(notes.children.is_empty());
}
#[test]
fn a_peer_that_lists_the_org_back_stops_there() {
let dir = tmp("links-back");
org(&dir, &["id:alpha/alpha1"]);
sub(
&dir,
"alpha",
Some("alpha"),
"alpha1",
"Alpha",
&["id:org/org1"],
);
let ws = open(&dir.join("org"));
let map = peers(vec![
("alpha", At::Dir(dir.join("alpha"))),
("org", At::Dir(dir.join("org"))),
]);
let federated = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent::default(),
))
.unwrap();
assert_eq!(names(&federated), ["org", "alpha"]);
let back = &federated.tree.children[0].children[0];
assert!(matches!(back.kind, NodeKind::Foreign { .. }));
assert_eq!(back.workspace, 1);
assert_eq!(
back.boundary,
Some(Boundary::Refused(Refusal::Cycle {
workspace: "org".into(),
root_dir: dir.join("org"),
}))
);
}
#[test]
fn two_names_for_one_directory_are_refused_under_both_trust_levels() {
let dir = federation("two-names");
let map = peers(vec![("alpha2", At::Dir(dir.join("alpha")))]);
for trust in [Trust::Confirmed, Trust::Unverified] {
let outcome = block_on(open_peer(&StdFs, &map, "alpha2", trust)).unwrap();
assert_eq!(
outcome.refusal(),
Some(&Refusal::Mismatched {
location: PeerLocation::Path(dir.join("alpha")),
declares: "alpha".into(),
}),
"under {trust:?}"
);
}
}
#[test]
fn a_resolver_that_did_not_check_is_caught_at_the_open() {
let dir = federation("unchecked-claim");
let map = peers(vec![("alpha", At::Claimed(dir.join("beta")))]);
let outcome = block_on(open_peer(&StdFs, &map, "alpha", Trust::Confirmed)).unwrap();
assert_eq!(
outcome.refusal(),
Some(&Refusal::Mismatched {
location: PeerLocation::Path(dir.join("beta")),
declares: "beta".into(),
})
);
}
#[test]
fn an_anonymous_peer_is_refused_by_default_and_followed_on_insistence() {
let dir = tmp("anonymous");
org(&dir, &["id:gamma/gamma1"]);
sub(&dir, "gamma", None, "gamma1", "Gamma", &[]);
let ws = open(&dir.join("org"));
let map = peers(vec![("gamma", At::Dir(dir.join("gamma")))]);
let strict = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent::default(),
))
.unwrap();
assert_eq!(names(&strict), ["org"]);
assert_eq!(
strict.tree.children[0].boundary,
Some(Boundary::Refused(Refusal::Unconfirmed {
location: PeerLocation::Path(dir.join("gamma")),
why: Unconfirmed::Anonymous,
}))
);
let insistent = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent {
trust: Trust::Unverified,
..Descent::default()
},
))
.unwrap();
assert_eq!(names(&insistent), ["org", "gamma"]);
assert_eq!(insistent.workspaces[1].declares, "");
assert_eq!(
insistent.tree.children[0].boundary,
Some(Boundary::Followed { into: 1 })
);
assert_eq!(insistent.tree.children[0].title.as_deref(), Some("Gamma"));
}
#[test]
fn an_anonymous_peer_that_is_the_reader_is_caught_by_the_directory() {
let dir = tmp("anonymous-self");
write(
&dir,
"solo/prov.yaml",
"root: README.md\nid_storage: frontmatter\n",
);
write(
&dir,
"solo/README.md",
"---\nid: solo1\ntitle: Solo\ncontents:\n- 'id:mirror/solo1'\n---\n",
);
let ws = open(&dir.join("solo"));
let map = peers(vec![("mirror", At::Dir(dir.join("solo")))]);
let federated = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent {
trust: Trust::Unverified,
..Descent::default()
},
))
.unwrap();
assert_eq!(federated.workspaces.len(), 1);
assert_eq!(federated.workspaces[0].name, "");
assert_eq!(
federated.tree.children[0].boundary,
Some(Boundary::Refused(Refusal::Cycle {
workspace: "mirror".into(),
root_dir: dir.join("solo"),
}))
);
}
#[test]
fn a_url_peer_is_never_opened() {
let dir = tmp("url");
org(&dir, &["id:ark/x", "id:web/x"]);
let ws = open(&dir.join("org"));
let map = peers(vec![
(
"ark",
At::Url {
url: "https://diaryx.org/ark:/12345/x".into(),
confirmed: true,
},
),
(
"web",
At::Url {
url: "https://example.org/notes".into(),
confirmed: false,
},
),
]);
let strict = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent::default(),
))
.unwrap();
assert_eq!(
strict.tree.children[0].boundary,
Some(Boundary::Refused(Refusal::Url(
"https://diaryx.org/ark:/12345/x".into()
)))
);
assert_eq!(
strict.tree.children[1].boundary,
Some(Boundary::Refused(Refusal::Unconfirmed {
location: PeerLocation::Url("https://example.org/notes".into()),
why: Unconfirmed::NotChecked,
}))
);
let insistent = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent {
trust: Trust::Unverified,
..Descent::default()
},
))
.unwrap();
assert_eq!(names(&insistent), ["org"]);
for (child, url) in insistent.tree.children.iter().zip([
"https://diaryx.org/ark:/12345/x",
"https://example.org/notes",
]) {
assert_eq!(
child.boundary,
Some(Boundary::Refused(Refusal::Url(url.into())))
);
}
}
#[test]
fn an_id_the_peer_never_registered_is_a_leaf_with_the_id_named() {
let dir = tmp("unregistered");
org(&dir, &["id:alpha/nosuch"]);
sub(&dir, "alpha", Some("alpha"), "alpha1", "Alpha", &[]);
let ws = open(&dir.join("org"));
let map = peers(vec![("alpha", At::Dir(dir.join("alpha")))]);
let federated = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent::default(),
))
.unwrap();
assert_eq!(
federated.tree.children[0].boundary,
Some(Boundary::Refused(Refusal::Unregistered {
workspace: "alpha".into(),
id: Id("nosuch".into()),
}))
);
assert_eq!(names(&federated), ["org", "alpha"]);
}
#[test]
fn the_bound_counts_crossings_rather_than_tree_levels() {
let dir = tmp("depth");
org(&dir, &["id:alpha/alpha1"]);
sub(
&dir,
"alpha",
Some("alpha"),
"alpha1",
"Alpha",
&["id:beta/beta1"],
);
sub(&dir, "beta", Some("beta"), "beta1", "Beta", &[]);
let ws = open(&dir.join("org"));
let map = peers(vec![
("alpha", At::Dir(dir.join("alpha"))),
("beta", At::Dir(dir.join("beta"))),
]);
let none = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent {
depth: 0,
..Descent::default()
},
))
.unwrap();
assert_eq!(names(&none), ["org"]);
assert_eq!(
none.tree.children[0].boundary,
Some(Boundary::Refused(Refusal::TooDeep))
);
let one = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent {
depth: 1,
..Descent::default()
},
))
.unwrap();
assert_eq!(names(&one), ["org", "alpha"]);
assert_eq!(
one.tree.children[0].boundary,
Some(Boundary::Followed { into: 1 })
);
assert_eq!(
one.tree.children[0].children[0].boundary,
Some(Boundary::Refused(Refusal::TooDeep))
);
let two = block_on(descend(
&ws,
Path::new("README.md"),
&map,
&Descent {
depth: 2,
..Descent::default()
},
))
.unwrap();
assert_eq!(names(&two), ["org", "alpha", "beta"]);
}
#[test]
fn a_directory_that_is_not_a_workspace_root_is_unopenable() {
let dir = tmp("unopenable");
std::fs::create_dir_all(dir.join("ghost")).unwrap();
let map = peers(vec![("ghost", At::Dir(dir.join("ghost")))]);
assert_eq!(
block_on(open_peer(&StdFs, &map, "ghost", Trust::Confirmed))
.unwrap()
.refusal(),
Some(&Refusal::Unconfirmed {
location: PeerLocation::Path(dir.join("ghost")),
why: Unconfirmed::Unreadable,
})
);
let insistent = block_on(open_peer(&StdFs, &map, "ghost", Trust::Unverified)).unwrap();
assert!(
matches!(insistent.refusal(), Some(Refusal::Unopenable { .. })),
"expected Unopenable, got {:?}",
insistent.refusal()
);
}
#[test]
fn an_unknown_name_is_refused_without_being_looked_for() {
let dir = federation("unknown");
let map = peers(vec![]);
assert_eq!(
block_on(open_peer(&StdFs, &map, "alpha", Trust::Confirmed))
.unwrap()
.refusal(),
Some(&Refusal::Unknown)
);
assert!(dir.join("alpha/README.md").exists());
}
#[test]
fn an_opened_workspace_reads_the_registry_its_root_declares() {
let dir = tmp("registry");
write(
&dir,
"index.md",
"---\ntitle: Home\nregistry: registry.yaml\n---\n",
);
write(&dir, "registry.yaml", "registry:\n r1: page.md\n");
write(
&dir,
"page.md",
"---\ntitle: Page\npart_of: index.md\n---\n",
);
let ws = open(&dir);
assert_eq!(
ws.index().resolve(&Id("r1".into())),
Some(PathBuf::from("page.md"))
);
std::fs::remove_file(dir.join("registry.yaml")).unwrap();
let ws = open(&dir);
assert_eq!(ws.index().resolve(&Id("r1".into())), None);
}
}