use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow, bail};
use nomograph_claim::{Claim, ClaimId, ClaimType, Store as ClaimStore, View};
use serde_json::Value;
use crate::legacy::{find_legacy_v1_db, legacy_migration_error};
pub const CLAIMS_DIR: &str = "claims";
fn local_asserter() -> String {
let user = std::env::var("USER").unwrap_or_else(|_| "unknown".into());
format!("user:local:{user}")
}
pub struct Store {
inner: ClaimStore,
view: View,
asserted_by: String,
root: PathBuf,
}
impl Store {
pub fn discover() -> Result<Self> {
if let Ok(raw) = std::env::var("SYNTHESIST_DIR")
&& !raw.is_empty()
{
return Self::open_explicit(Path::new(&raw));
}
let cwd = std::env::current_dir().context("cwd")?;
Self::discover_from(&cwd)
}
fn open_explicit(dir: &Path) -> Result<Self> {
if !dir.exists() {
bail!(
"SYNTHESIST_DIR / --data-dir points at `{}` which does not exist",
dir.display()
);
}
if !dir.is_dir() {
bail!(
"SYNTHESIST_DIR / --data-dir points at `{}` which is not a directory",
dir.display()
);
}
let claims = dir.join(CLAIMS_DIR);
if !claims.join("genesis.amc").is_file() {
return Err(anyhow!(
"SYNTHESIST_DIR / --data-dir points at `{}` but no `{}/genesis.amc` is present there. \
Run `synthesist init` in that directory first, or unset the override to fall back \
to parent-directory discovery from cwd.",
dir.display(),
CLAIMS_DIR
));
}
Self::open_at(&claims)
}
pub fn discover_from(start: &Path) -> Result<Self> {
let mut cur = start.to_path_buf();
loop {
let candidate = cur.join(CLAIMS_DIR);
if candidate.join("genesis.amc").is_file() {
return Self::open_at(&candidate);
}
if !cur.pop() {
break;
}
}
if let Some(legacy) = find_legacy_v1_db(start) {
return Err(legacy_migration_error(&legacy));
}
Self::init_at(&start.join(CLAIMS_DIR))
}
pub fn open_at(claims_dir: &Path) -> Result<Self> {
let mut inner = ClaimStore::open(claims_dir)
.with_context(|| format!("open claim store at {}", claims_dir.display()))?;
let mut view = View::open(claims_dir)
.with_context(|| format!("open view at {}", claims_dir.display()))?;
view.sync(&mut inner).context("sync view on open")?;
Ok(Self {
inner,
view,
asserted_by: local_asserter(),
root: claims_dir.to_path_buf(),
})
}
pub fn init_at(claims_dir: &Path) -> Result<Self> {
let inner = ClaimStore::init(claims_dir)
.with_context(|| format!("init claim store at {}", claims_dir.display()))?;
let view = View::open(claims_dir)
.with_context(|| format!("open view at {}", claims_dir.display()))?;
Ok(Self {
inner,
view,
asserted_by: local_asserter(),
root: claims_dir.to_path_buf(),
})
}
pub fn discover_for(session: &Option<String>) -> Result<Self> {
let mut s = Self::discover()?;
if let Some(id) = session
&& !id.is_empty()
{
s.asserted_by = format!("{}:{}", s.asserted_by, id);
}
Ok(s)
}
#[allow(dead_code)]
pub fn with_asserter(mut self, asserted_by: impl Into<String>) -> Self {
self.asserted_by = asserted_by.into();
self
}
pub fn append(
&mut self,
claim_type: ClaimType,
props: Value,
supersedes: Option<ClaimId>,
) -> Result<ClaimId> {
let mut claim = Claim::new(claim_type, props, self.asserted_by.clone());
if let Some(prior) = supersedes {
claim = claim.with_supersedes(prior);
}
self.inner.append(&claim).context("append claim")?;
self.view
.sync(&mut self.inner)
.context("sync view after append")?;
Ok(claim.id)
}
pub fn query(&self, sql: &str, params: &[&dyn rusqlite::ToSql]) -> Result<Vec<Value>> {
self.view.query(sql, params).context("view query")
}
pub fn root(&self) -> &Path {
&self.root
}
#[allow(dead_code)]
pub fn sync_view(&mut self) -> Result<()> {
self.view.sync(&mut self.inner).context("sync view")?;
Ok(())
}
pub fn inner(&mut self) -> &mut ClaimStore {
&mut self.inner
}
}