use std::collections::BTreeSet;
use std::path::Path;
use serde_json::{Map, Value};
use thiserror::Error;
use weavatrix_rust::{Weavatrix, operations};
use wvq_domain::{IdError, RevisionId};
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum IntelligenceError {
#[error("weavatrix engine: {0}")]
Engine(String),
#[error("weavatrix result is missing revision identity")]
MissingRevision,
#[error("ambiguous revision identity: snapshot `{expected}`, result `{found}`")]
AmbiguousRevision {
expected: String,
found: String,
},
#[error("invalid revision: {0}")]
InvalidRevision(#[from] IdError),
#[error("evidence JSON: {0}")]
Json(String),
#[error("invalid weavatrix evidence: {0}")]
InvalidEvidence(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoEvidence {
pub repository: String,
pub revision: RevisionId,
pub node_count: u64,
pub edge_count: u64,
pub generator: String,
}
pub trait CodeEvidenceProvider {
fn analyze(&self, repo: &Path) -> Result<RepoEvidence, IntelligenceError>;
fn operation(&self, repo: &Path, name: &str, args: &Value) -> Result<Value, IntelligenceError>;
fn indexed_files(&self, repo: &Path) -> Result<BTreeSet<String>, IntelligenceError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct WeavatrixProvider;
impl CodeEvidenceProvider for WeavatrixProvider {
fn analyze(&self, repo: &Path) -> Result<RepoEvidence, IntelligenceError> {
let engine =
Weavatrix::open(repo).map_err(|err| IntelligenceError::Engine(err.to_string()))?;
evidence_from_engine(&engine)
}
fn operation(&self, repo: &Path, name: &str, args: &Value) -> Result<Value, IntelligenceError> {
let mut engine =
Weavatrix::open(repo).map_err(|err| IntelligenceError::Engine(err.to_string()))?;
let snapshot = engine.state().snapshot();
let repository = snapshot.repository.clone();
let revision = require_revision(&snapshot.revision)?;
let report = operations::call(&mut engine, name, to_engine_value(args)?)
.map_err(IntelligenceError::Engine)?;
let mut value = from_engine_value(&report)?;
attach_identity(&mut value, &repository, revision.as_str())?;
Ok(value)
}
fn indexed_files(&self, repo: &Path) -> Result<BTreeSet<String>, IntelligenceError> {
let engine =
Weavatrix::open(repo).map_err(|err| IntelligenceError::Engine(err.to_string()))?;
Ok(engine
.state()
.snapshot()
.nodes
.iter()
.filter_map(|node| {
node.id
.as_str()
.strip_prefix("file:")
.map(ToOwned::to_owned)
})
.collect())
}
}
fn evidence_from_engine(engine: &Weavatrix) -> Result<RepoEvidence, IntelligenceError> {
let snapshot = engine.state().snapshot();
Ok(RepoEvidence {
repository: snapshot.repository.clone(),
revision: require_revision(&snapshot.revision)?,
node_count: u64::try_from(snapshot.nodes.len()).unwrap_or(u64::MAX),
edge_count: u64::try_from(snapshot.edges.len()).unwrap_or(u64::MAX),
generator: snapshot.generator.clone(),
})
}
fn require_revision(raw: &str) -> Result<RevisionId, IntelligenceError> {
if raw.is_empty() {
return Err(IntelligenceError::MissingRevision);
}
Ok(RevisionId::new(raw)?)
}
fn attach_identity(
value: &mut Value,
repository: &str,
revision: &str,
) -> Result<(), IntelligenceError> {
if !value.is_object() {
let inner = value.take();
*value = Value::Object(Map::from_iter([("value".to_owned(), inner)]));
}
let object = value
.as_object_mut()
.ok_or(IntelligenceError::MissingRevision)?;
bind_field(object, "repository", repository)?;
bind_field(object, "revision", revision)?;
Ok(())
}
fn bind_field(
object: &mut Map<String, Value>,
key: &str,
expected: &str,
) -> Result<(), IntelligenceError> {
match object.get(key) {
None => {
object.insert(key.to_owned(), Value::String(expected.to_owned()));
Ok(())
}
Some(Value::String(found)) if found == expected => Ok(()),
Some(found) => Err(IntelligenceError::AmbiguousRevision {
expected: expected.to_owned(),
found: found.to_string(),
}),
}
}
fn to_engine_value(value: &Value) -> Result<blazingly_json::Value, IntelligenceError> {
let raw =
serde_json::to_string(value).map_err(|err| IntelligenceError::Json(err.to_string()))?;
blazingly_json::from_str(&raw).map_err(|err| IntelligenceError::Json(err.to_string()))
}
fn from_engine_value(value: &blazingly_json::Value) -> Result<Value, IntelligenceError> {
let raw =
blazingly_json::to_string(value).map_err(|err| IntelligenceError::Json(err.to_string()))?;
serde_json::from_str(&raw).map_err(|err| IntelligenceError::Json(err.to_string()))
}