use prov_graph::document::Document;
use prov_graph::fs::{DirEntry, Metadata};
use prov_graph::graph::{Backlink, CensusEntry, Graph, Node, ReadSettings, TreeOptions, Walk};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use crate::change::{ChangeSet, FileOp};
use crate::config::{Fixity, History, IdStorage};
use crate::fixity::FixityCache;
use crate::identity::{IdentityPolicy, NoIdentity, Trigger};
use prov_graph::document::EmbedStyle;
use prov_graph::error::{Error, Result};
use prov_graph::fs::ReadStorage;
use prov_graph::graph::Target;
use prov_graph::index::{Collision, IdIndex, NoIndex};
use prov_graph::link::{self, Addressing, Link, LinkStyle, ReferenceStyle, Wrapper};
use prov_graph::memo::{ReadScope, lock};
use prov_graph::meta::Value;
use prov_graph::relation::RelationSet;
use prov_graph::title::TitleIndex;
use prov_store::fs::Storage;
use prov_store::index::IndexStore;
#[derive(Debug, Clone)]
pub struct Settings {
pub relations: RelationSet,
pub link_style: LinkStyle,
pub id_links: bool,
pub reference_style: Option<ReferenceStyle>,
pub default_embed_format: fig::Format,
pub embed_style: EmbedStyle,
pub fixity: Fixity,
pub history: History,
pub id_storage: IdStorage,
pub workspace_id: String,
}
impl Default for Settings {
fn default() -> Self {
Self {
relations: RelationSet::diaryx(),
link_style: LinkStyle::default(),
id_links: false,
reference_style: None,
default_embed_format: fig::Format::Yaml,
embed_style: EmbedStyle::Delimited,
fixity: Fixity::Payloads,
history: History::Off,
id_storage: IdStorage::Registry,
workspace_id: String::new(),
}
}
}
impl From<&crate::config::WorkspaceConfig> for Settings {
fn from(config: &crate::config::WorkspaceConfig) -> Self {
Self {
relations: config.relation_set(),
link_style: config.link_format(),
reference_style: Some(config.reference_style()),
default_embed_format: config.default_embed_format,
embed_style: config.embed_style,
fixity: config.fixity,
history: config.history,
id_storage: config.id_storage,
workspace_id: config.workspace_id.clone(),
..Self::default()
}
}
}
#[derive(Debug)]
pub struct Workspace<FS, Id = NoIdentity, Ix = NoIndex> {
graph: Graph<FS, Ix>,
identity: Id,
settings: Settings,
pending_stamps: Vec<(PathBuf, prov_graph::identity::Id)>,
fixity_cache: Mutex<Option<FixityCache>>,
}
impl<FS: Clone, Id: Clone, Ix: Clone> Clone for Workspace<FS, Id, Ix> {
fn clone(&self) -> Self {
Self {
graph: self.graph.clone(),
identity: self.identity.clone(),
settings: self.settings.clone(),
pending_stamps: self.pending_stamps.clone(),
fixity_cache: Mutex::new(lock(&self.fixity_cache).clone()),
}
}
}
impl<FS> Workspace<FS, NoIdentity, NoIndex> {
pub fn builder(fs: FS) -> WorkspaceBuilder<FS, NoIdentity, NoIndex> {
WorkspaceBuilder {
fs,
root: PathBuf::from("."),
identity: NoIdentity,
index: NoIndex,
settings: Settings::default(),
fixity_cache: None,
}
}
}
impl<FS, Id, Ix> Workspace<FS, Id, Ix> {
pub fn history_store(&self) -> crate::history::HistoryStore<&Self> {
crate::history::HistoryStore::new(self)
}
pub fn history_store_mut(&mut self) -> crate::history::HistoryStore<&mut Self> {
crate::history::HistoryStore::new(self)
}
}
impl<FS: Storage, Id, Ix: IndexStore> prov_history::HistoryReadHost for Workspace<FS, Id, Ix> {
type Fs = FS;
type Ix = Ix;
fn graph(&self) -> &Graph<Self::Fs, Self::Ix> {
self.graph()
}
fn embed_style(&self) -> EmbedStyle {
self.embed_style()
}
fn default_embed_format(&self) -> fig::Format {
self.default_embed_format()
}
fn history_captures(&self) -> bool {
self.history().captures()
}
fn history_relation(&self) -> Option<&str> {
self.relations().history_relation()
}
fn history_link_style(&self) -> LinkStyle {
match self.relations().history_relation() {
Some(relation) => self.reference_style_for(relation).path_style,
None => self.link_style(),
}
}
async fn history_path(&self, root_doc: &Path) -> Result<Option<PathBuf>> {
self.history_path(root_doc).await
}
async fn reachable_files(&self, root_doc: &Path) -> Result<BTreeSet<PathBuf>> {
self.reachable_files(root_doc).await
}
async fn history_exclusions(&self, root_doc: &Path) -> Result<Vec<PathBuf>> {
let mut excluded = Vec::new();
if let Some(index) = self.recycle_bin_path(root_doc).await? {
excluded.push(crate::history::store_dir(&index).join("items"));
}
if let Some(about) = self.about_path(root_doc).await? {
excluded.push(about);
}
Ok(excluded)
}
fn registration_conflict(
&self,
id: &prov_graph::identity::Id,
path: &Path,
) -> Option<Collision> {
self.registration_conflict(id, path)
}
}
impl<FS: Storage, Id, Ix: IndexStore> prov_history::HistoryWriteHost for Workspace<FS, Id, Ix> {
fn change(&mut self) -> ChangeSet {
self.change()
}
async fn commit(&mut self, cs: ChangeSet) -> Result<()> {
self.commit(cs).await
}
fn fixity_cached(&self, path: &Path, meta: &Metadata) -> Option<String> {
self.fixity_cached(path, meta)
}
fn fixity_remember(&self, path: &Path, meta: &Metadata, hash: &str) {
self.fixity_remember(path, meta, hash)
}
}
impl<FS, Id, Ix> Workspace<FS, Id, Ix> {
pub fn graph(&self) -> &Graph<FS, Ix> {
&self.graph
}
pub fn root(&self) -> &Path {
self.graph.root()
}
pub fn fs_path(&self, rel: impl AsRef<Path>) -> PathBuf {
self.graph.fs_path(rel)
}
pub fn relations(&self) -> &RelationSet {
&self.settings.relations
}
#[must_use = "the scope ends the moment its guard is dropped"]
pub fn read_scope(&self) -> ReadScope {
self.graph.read_scope()
}
pub fn set_fixity_cache(&mut self, cache: Option<FixityCache>) {
*lock(&self.fixity_cache) = cache;
}
pub fn take_fixity_cache(&mut self) -> Option<FixityCache> {
lock(&self.fixity_cache).take()
}
pub(crate) fn fixity_cached(
&self,
path: &Path,
meta: &prov_graph::fs::Metadata,
) -> Option<String> {
lock(&self.fixity_cache)
.as_ref()?
.get(path, meta)
.map(str::to_string)
}
pub(crate) fn fixity_remember(&self, path: &Path, meta: &prov_graph::fs::Metadata, hash: &str) {
if let Some(cache) = lock(&self.fixity_cache).as_mut() {
cache.put(path, meta, hash);
}
}
fn forget_written(&self, cs: &ChangeSet) {
let mut memo = self.graph.memo_lock();
let mut cache = lock(&self.fixity_cache);
let mut forget = |path: &Path| {
memo.forget(path);
if let Some(cache) = cache.as_mut() {
cache.forget(path);
}
};
for op in cs.ops() {
match op {
FileOp::Write { path, .. }
| FileOp::Remove { path }
| FileOp::CopyFrom { path, .. } => forget(path),
FileOp::Rename { from, to } => {
forget(from);
forget(to);
}
}
}
}
pub fn identity(&self) -> &Id {
&self.identity
}
pub fn index(&self) -> &Ix {
self.graph.index()
}
pub fn link_style(&self) -> LinkStyle {
self.settings.link_style
}
pub fn id_links(&self) -> bool {
self.reference_style().registers()
}
pub fn fixity(&self) -> Fixity {
self.settings.fixity
}
pub fn history(&self) -> History {
self.settings.history
}
pub fn embed_style(&self) -> EmbedStyle {
self.settings.embed_style
}
pub fn id_storage(&self) -> IdStorage {
self.settings.id_storage
}
pub fn workspace_id(&self) -> &str {
&self.settings.workspace_id
}
pub fn reference_style(&self) -> ReferenceStyle {
self.settings.reference_style.unwrap_or(ReferenceStyle {
wrapper: Wrapper::Markdown,
addressing: if self.settings.id_links {
Addressing::Id
} else {
Addressing::Path
},
label: false,
path_style: self.settings.link_style,
})
}
pub fn reference_style_for(&self, relation: &str) -> ReferenceStyle {
self.settings
.relations
.style_for(relation)
.unwrap_or_else(|| self.reference_style())
}
pub fn default_embed_format(&self) -> fig::Format {
self.settings.default_embed_format
}
pub fn index_mut(&mut self) -> &mut Ix {
self.graph.index_mut()
}
}
impl<FS, Id, Ix: IndexStore> Workspace<FS, Id, Ix> {
pub fn registration_conflict(
&self,
id: &prov_graph::identity::Id,
path: &Path,
) -> Option<Collision> {
if let Some(held_by) = self.graph.index().resolve(id)
&& held_by != path
{
return Some(Collision::Id {
id: id.clone(),
held_by,
});
}
if let Some(held) = self.graph.index().id_for_path(path)
&& held != *id
{
return Some(Collision::Path {
path: path.to_path_buf(),
held,
});
}
None
}
pub(crate) fn move_conflict(
&self,
id: &prov_graph::identity::Id,
dest: &Path,
) -> Option<Collision> {
let held = self.graph.index().id_for_path(dest)?;
(held != *id).then(|| Collision::Path {
path: dest.to_path_buf(),
held,
})
}
}
impl<FS: ReadStorage, Id, Ix: IdIndex> Workspace<FS, Id, Ix> {
pub async fn registry_path(&self, root_doc: &Path) -> Result<Option<PathBuf>> {
match self.relations().registry_relation() {
Some(relation) => self.pointer_target(root_doc, relation).await,
None => Ok(None),
}
}
pub async fn config_path(&self, root_doc: &Path) -> Result<Option<PathBuf>> {
match self.relations().config_relation() {
Some(relation) => self.pointer_target(root_doc, relation).await,
None => Ok(None),
}
}
pub async fn recycle_bin_path(&self, root_doc: &Path) -> Result<Option<PathBuf>> {
match self.relations().recycle_relation() {
Some(relation) => self.pointer_target(root_doc, relation).await,
None => Ok(None),
}
}
pub async fn history_path(&self, root_doc: &Path) -> Result<Option<PathBuf>> {
match self.relations().history_relation() {
Some(relation) => self.pointer_target(root_doc, relation).await,
None => Ok(None),
}
}
pub(crate) async fn parked_dirs(&self, root_doc: &Path) -> Result<Vec<PathBuf>> {
let mut dirs = Vec::new();
if let Some(index) = self.history_path(root_doc).await? {
dirs.push(crate::history::store_dir(&index).join(crate::history::EVENTS_DIR));
dirs.push(crate::history::store_dir(&index).join(crate::history::BLOBS_DIR));
}
if let Some(index) = self.recycle_bin_path(root_doc).await? {
dirs.push(crate::history::store_dir(&index).join("items"));
}
Ok(dirs)
}
pub async fn about_path(&self, root_doc: &Path) -> Result<Option<PathBuf>> {
match self.relations().about_relation() {
Some(relation) => self.pointer_target(root_doc, relation).await,
None => Ok(None),
}
}
pub async fn config_get(
&self,
root_doc: &Path,
key: &str,
) -> Result<Option<prov_graph::meta::Value>> {
let Some(config_doc) = self.config_path(root_doc).await? else {
return Ok(None);
};
let (_, doc) = self.load(&config_doc).await?;
Ok(doc.meta.get(key).cloned())
}
pub async fn effective_config(
&self,
root_doc: &Path,
) -> Result<crate::config::WorkspaceConfig> {
let mut config = crate::config::WorkspaceConfig::default();
if let Ok((_, root)) = self.load(root_doc).await
&& let Some(block) = root.meta.get(crate::config::ROOT_CONFIG_KEY)
{
config.apply(block);
}
if let Some(config_doc) = self.config_path(root_doc).await? {
let (_, doc) = self.load(&config_doc).await?;
config.apply(&doc.meta);
}
Ok(config)
}
pub fn vocabulary_path(&self, root_doc: &Path, pointer: &str) -> Option<PathBuf> {
match self.resolve_link(&link::normalize(root_doc), &Link::parse(pointer)) {
Target::Path(path) => Some(path),
_ => None,
}
}
pub async fn load_vocabulary(
&self,
root_doc: &Path,
pointer: &str,
) -> Result<Option<crate::vocabulary::Vocabulary>> {
let Some(path) = self.vocabulary_path(root_doc, pointer) else {
return Ok(None);
};
let (_, doc) = self.load(&path).await?;
if let Some(carrier) = doc.carrier {
prov_graph::document::require_whole_file(&path, carrier)?;
}
Ok(crate::vocabulary::Vocabulary::from_meta(&doc.meta))
}
async fn pointer_target(&self, root_doc: &Path, relation: &str) -> Result<Option<PathBuf>> {
let root_doc = link::normalize(root_doc);
let (_, doc) = self.load(&root_doc).await?;
let Some(raw) = doc
.meta
.get(relation)
.map(prov_graph::meta::Value::link_strings)
.and_then(|targets| targets.into_iter().next())
else {
return Ok(None);
};
match self.resolve_link(&root_doc, &Link::parse(&raw)) {
Target::Path(path) => Ok(Some(path)),
_ => Ok(None),
}
}
}
impl<FS: Storage, Id: IdentityPolicy, Ix: IndexStore> Workspace<FS, Id, Ix> {
pub async fn register(
&mut self,
path: &Path,
event: Trigger,
) -> Result<prov_graph::identity::Id> {
let path = link::normalize(path);
if let Some(id) = self.graph.index().id_for_path(&path) {
return Ok(id);
}
if !self.identity.registration().fires_on(event) {
return Err(Error::Structure(format!(
"identity policy does not register on {event:?}"
)));
}
if !self.exists(&path).await? {
return Err(Error::NotFound(path.to_path_buf()));
}
let id = self.mint_unique(&path);
self.graph.index_mut().register(&id, &path);
self.queue_stamp(&path, &id);
Ok(id)
}
pub(crate) fn mint_unique(&mut self, path: &Path) -> prov_graph::identity::Id {
loop {
let id = self.identity.mint(path);
if !self.graph.index().is_known(&id) {
return id;
}
}
}
pub(crate) async fn authored_target(
&mut self,
relation: &str,
from: &Path,
to: &Path,
title: &str,
target_exists: bool,
) -> Result<String> {
let style = self.reference_style_for(relation);
let id = if style.registers() && self.identity.registration().fires_on(Trigger::Link) {
Some(if target_exists {
self.register(to, Trigger::Link).await?
} else {
self.register_for_authoring(to)
})
} else {
None
};
Ok(link::format_reference(style, from, to, id.as_ref(), title))
}
pub(crate) fn register_for_authoring(&mut self, path: &Path) -> prov_graph::identity::Id {
let path = link::normalize(path);
if let Some(id) = self.graph.index().id_for_path(&path) {
return id;
}
let id = self.mint_unique(&path);
self.graph.index_mut().register(&id, &path);
self.queue_stamp(&path, &id);
id
}
fn queue_stamp(&mut self, path: &Path, id: &prov_graph::identity::Id) {
if self.settings.id_storage.stamps_frontmatter() {
self.pending_stamps.push((path.to_path_buf(), id.clone()));
}
}
}
impl<FS, Id, Ix> Workspace<FS, Id, Ix> {
pub fn fs(&self) -> &FS {
self.graph.fs()
}
}
impl<FS: Storage, IdP, Ix: IndexStore> Workspace<FS, IdP, Ix> {
pub(crate) fn change(&mut self) -> ChangeSet {
self.graph.index_mut().rollback();
self.graph.index_mut().checkpoint();
ChangeSet::new()
}
pub(crate) async fn load_staged(
&self,
cs: &ChangeSet,
path: &Path,
) -> Result<(String, prov_graph::document::Document)> {
let Some(bytes) = cs.staged(path) else {
return self.load(path).await;
};
let text = String::from_utf8(bytes.to_vec())
.map_err(|e| Error::Structure(format!("{} is not valid UTF-8: {e}", path.display())))?;
let doc = prov_graph::document::Document::parse(path, &text)?;
Ok((text, doc))
}
pub(crate) async fn commit(&mut self, mut cs: ChangeSet) -> Result<()> {
if let Err(e) = self.stage_pending_stamps(&mut cs).await {
self.pending_stamps.clear();
self.graph.index_mut().rollback();
return Err(e);
}
if let Err(e) = self.graph.index_mut().rebase(&cs) {
self.graph.index_mut().rollback();
return Err(e);
}
let staged_index = match self.graph.index_mut().pending_write() {
Ok(Some((path, text))) => {
cs.write(path, text);
true
}
Ok(None) => false,
Err(e) => {
self.graph.index_mut().rollback();
return Err(e);
}
};
self.forget_written(&cs);
match cs.apply(self.graph.fs(), self.graph.root()).await {
Ok(()) => {
self.graph.index_mut().committed(staged_index);
Ok(())
}
Err(e) => {
self.graph.index_mut().rollback();
Err(e)
}
}
}
async fn stage_pending_stamps(&mut self, cs: &mut ChangeSet) -> Result<()> {
for (path, id) in std::mem::take(&mut self.pending_stamps) {
if cs.renamed_to(&path).is_some() {
continue;
}
let text = match cs.staged(&path) {
Some(bytes) => match std::str::from_utf8(bytes) {
Ok(text) => text.to_string(),
Err(_) => continue,
},
None => match self.read_text(&path).await {
Ok(text) => text,
Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e),
},
};
let doc = prov_graph::document::Document::parse(&path, &text)?;
if doc.meta.get("id").and_then(Value::as_str) == Some(id.0.as_str()) {
continue;
}
let updated = prov_store::edit::set_in_text(
&text,
doc.carrier,
"id",
fig::Value::Str(id.0.clone()),
)?;
cs.write(&path, updated);
}
Ok(())
}
pub async fn link_sidecar(
&self,
root_doc: &Path,
pointer: &str,
sidecar: &Path,
seed: &prov_graph::meta::Mapping,
format: fig::Format,
) -> Result<bool> {
let mut cs = ChangeSet::new();
let created = !self.exists(sidecar).await?;
if created {
cs.write(sidecar, prov_graph::meta::serialize_mapping(seed, format)?);
}
let (text, doc) = self.load(root_doc).await?;
let updated = prov_store::edit::set_in_text(
&text,
doc.carrier,
pointer,
prov_store::edit::infer_scalar(&sidecar.to_string_lossy()),
)?;
cs.write(root_doc, updated);
cs.apply(self.graph.fs(), self.graph.root()).await?;
Ok(created)
}
}
impl<FS: ReadStorage, Id, Ix: IdIndex> Workspace<FS, Id, Ix> {
pub(crate) async fn load(&self, path: &Path) -> Result<(String, Document)> {
self.graph.load(path).await
}
pub async fn document(&self, path: impl AsRef<Path>) -> Result<Document> {
self.graph.document(path).await
}
pub async fn exists(&self, path: &Path) -> Result<bool> {
self.graph.exists(path).await
}
pub async fn read_bytes(&self, path: &Path) -> Result<Vec<u8>> {
self.graph.read_bytes(path).await
}
pub async fn read_text(&self, path: &Path) -> Result<String> {
self.graph.read_text(path).await
}
pub async fn listing(&self, path: &Path) -> Result<Vec<DirEntry>> {
self.graph.listing(path).await
}
pub async fn stat(&self, path: &Path) -> Result<Metadata> {
self.graph.stat(path).await
}
pub fn resolve_link(&self, doc: &Path, link: &Link) -> Target {
self.graph.resolve_link(doc, link)
}
pub fn resolve_link_with(
&self,
doc: &Path,
link: &Link,
titles: Option<&TitleIndex>,
) -> Target {
self.graph.resolve_link_with(doc, link, titles)
}
pub async fn census(&self, start: impl AsRef<Path>) -> Result<Vec<CensusEntry>> {
let _scope = self.read_scope();
let start = start.as_ref();
let parked = self.parked_dirs(start).await?;
self.graph.census_within(start, &parked).await
}
pub(crate) async fn walk(&self, start: &Path) -> Result<Walk> {
let _scope = self.read_scope();
let parked = self.parked_dirs(start).await?;
self.graph.walk(start, &parked).await
}
pub async fn backlinks(
&self,
start: impl AsRef<Path>,
) -> Result<BTreeMap<PathBuf, Vec<Backlink>>> {
Ok(prov_graph::graph::invert(self.census(start).await?))
}
pub async fn backlinks_to(
&self,
start: impl AsRef<Path>,
target: impl AsRef<Path>,
) -> Result<Vec<Backlink>> {
Ok(prov_graph::graph::inbound(
self.census(start).await?,
target.as_ref(),
))
}
pub async fn reachable_files(&self, start: impl AsRef<Path>) -> Result<BTreeSet<PathBuf>> {
let _scope = self.read_scope();
let start = start.as_ref();
let parked = self.parked_dirs(start).await?;
self.graph.reachable_files_within(start, &parked).await
}
pub async fn reachable_documents(
&self,
start: &Path,
census: &[CensusEntry],
content_bodies: &[PathBuf],
) -> Result<BTreeSet<PathBuf>> {
self.graph
.reachable_documents(start, census, content_bodies)
.await
}
pub async fn tree(&self, start: impl AsRef<Path>) -> Result<Node> {
self.tree_with(start, TreeOptions::default()).await
}
pub async fn tree_with(&self, start: impl AsRef<Path>, options: TreeOptions) -> Result<Node> {
let start = start.as_ref();
let parked = self.parked_dirs(start).await?;
self.graph.tree_within(start, options, &parked).await
}
pub async fn title_index(&self) -> Result<TitleIndex> {
self.graph.title_index().await
}
pub async fn title_index_scoped(&self, start: &Path) -> Result<TitleIndex> {
let parked = self.parked_dirs(start).await?;
self.graph.title_index_scoped(start, &parked).await
}
pub async fn scan_ids(&self) -> Result<Vec<(prov_graph::identity::Id, PathBuf)>> {
self.graph.scan_ids().await
}
pub async fn content_documents(&self) -> Result<Vec<PathBuf>> {
self.graph.content_documents().await
}
pub(crate) async fn direct_child_files(
&self,
dirs: &BTreeSet<PathBuf>,
) -> Result<Vec<PathBuf>> {
self.graph.direct_child_files(dirs).await
}
pub(crate) fn reached_dirs(reachable: &BTreeSet<PathBuf>) -> BTreeSet<PathBuf> {
Graph::<FS, Ix>::reached_dirs(reachable)
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceBuilder<FS, Id, Ix> {
fs: FS,
root: PathBuf,
identity: Id,
index: Ix,
settings: Settings,
fixity_cache: Option<FixityCache>,
}
impl<FS, Id, Ix> WorkspaceBuilder<FS, Id, Ix> {
pub fn fixity_cache(mut self, cache: FixityCache) -> Self {
self.fixity_cache = Some(cache);
self
}
pub fn root(mut self, root: impl Into<PathBuf>) -> Self {
self.root = root.into();
self
}
pub fn relations(mut self, relations: RelationSet) -> Self {
self.settings.relations = relations;
self
}
pub fn link_style(mut self, link_style: LinkStyle) -> Self {
self.settings.link_style = link_style;
self
}
pub fn id_links(mut self, id_links: bool) -> Self {
self.settings.id_links = id_links;
self
}
pub fn fixity(mut self, fixity: Fixity) -> Self {
self.settings.fixity = fixity;
self
}
pub fn history(mut self, history: History) -> Self {
self.settings.history = history;
self
}
pub fn embed_style(mut self, embed_style: EmbedStyle) -> Self {
self.settings.embed_style = embed_style;
self
}
pub fn id_storage(mut self, id_storage: IdStorage) -> Self {
self.settings.id_storage = id_storage;
self
}
pub fn workspace_id(mut self, name: impl Into<String>) -> Self {
self.settings.workspace_id = name.into();
self
}
pub fn reference_style(mut self, style: ReferenceStyle) -> Self {
self.settings.reference_style = Some(style);
self
}
pub fn default_embed_format(mut self, format: fig::Format) -> Self {
self.settings.default_embed_format = format;
self
}
pub fn settings(mut self, settings: Settings) -> Self {
self.settings = settings;
self
}
pub fn identity<Id2>(self, identity: Id2) -> WorkspaceBuilder<FS, Id2, Ix> {
WorkspaceBuilder {
fs: self.fs,
root: self.root,
identity,
index: self.index,
settings: self.settings,
fixity_cache: self.fixity_cache,
}
}
pub fn index<Ix2>(self, index: Ix2) -> WorkspaceBuilder<FS, Id, Ix2> {
WorkspaceBuilder {
fs: self.fs,
root: self.root,
identity: self.identity,
index,
settings: self.settings,
fixity_cache: self.fixity_cache,
}
}
pub fn build(self) -> Workspace<FS, Id, Ix> {
let read = ReadSettings {
relations: self.settings.relations.clone(),
workspace_id: self.settings.workspace_id.clone(),
id_storage: self.settings.id_storage,
};
Workspace {
graph: Graph::new(self.fs, self.root, self.index, read),
identity: self.identity,
settings: self.settings,
pending_stamps: Vec::new(),
fixity_cache: Mutex::new(self.fixity_cache),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::{IdentityPolicy, Minter};
use prov_store::index::InMemoryIndex;
#[derive(Clone)]
struct DummyFs;
#[test]
fn paths_only_by_default() {
let ws = Workspace::builder(DummyFs).root("vault").build();
assert_eq!(ws.root(), Path::new("vault"));
assert_eq!(ws.relations().spanning_relation(), Some("contents"));
assert!(!ws.identity().registration().is_active());
}
#[test]
fn fs_path_joins_a_workspace_relative_path_onto_the_root() {
let ws = Workspace::builder(DummyFs).root("vault").build();
assert_eq!(
ws.fs_path(Path::new("notes/a.md")),
Path::new("vault/notes/a.md")
);
}
#[test]
fn identity_opts_in_via_one_builder_line() {
let ws = Workspace::builder(DummyFs)
.root("vault")
.identity(Minter::lazy(1))
.index(InMemoryIndex::new())
.build();
assert!(ws.identity().registration().on_link);
assert!(ws.index().is_empty());
}
#[test]
fn every_setting_survives_the_builder_type_flips() {
let settings = Settings {
relations: RelationSet::diaryx(),
link_style: LinkStyle::PlainRelative,
id_links: true,
reference_style: None,
default_embed_format: fig::Format::Json,
embed_style: EmbedStyle::CodeBlock,
fixity: Fixity::Off,
history: History::Manual,
id_storage: IdStorage::Frontmatter,
workspace_id: "notes".into(),
};
let ws = Workspace::builder(DummyFs)
.root("vault")
.settings(settings)
.identity(Minter::lazy(1))
.index(InMemoryIndex::new())
.build();
assert_eq!(ws.link_style(), LinkStyle::PlainRelative);
assert_eq!(ws.default_embed_format(), fig::Format::Json);
assert_eq!(ws.embed_style(), EmbedStyle::CodeBlock);
assert_eq!(ws.fixity(), Fixity::Off);
assert_eq!(ws.history(), History::Manual);
assert_eq!(ws.id_storage(), IdStorage::Frontmatter);
assert_eq!(ws.workspace_id(), "notes");
assert_eq!(ws.relations().spanning_relation(), Some("contents"));
assert!(ws.id_links());
assert_eq!(ws.reference_style().addressing, Addressing::Id);
let copy = ws.clone();
assert_eq!(copy.id_storage(), IdStorage::Frontmatter);
assert_eq!(copy.workspace_id(), "notes");
}
#[test]
fn a_config_becomes_the_workspaces_settings() {
let config = crate::config::WorkspaceConfig {
id_storage: IdStorage::Frontmatter,
history: History::Manual,
fixity: Fixity::Off,
embed_style: EmbedStyle::CodeBlock,
default_embed_format: fig::Format::Json,
workspace_id: "notes".into(),
..Default::default()
};
let ws = Workspace::builder(DummyFs)
.root("vault")
.settings(Settings::from(&config))
.build();
assert_eq!(ws.id_storage(), IdStorage::Frontmatter);
assert_eq!(ws.history(), History::Manual);
assert_eq!(ws.fixity(), Fixity::Off);
assert_eq!(ws.embed_style(), EmbedStyle::CodeBlock);
assert_eq!(ws.default_embed_format(), fig::Format::Json);
assert_eq!(ws.workspace_id(), "notes");
assert_eq!(ws.reference_style(), config.reference_style());
}
}