use std::collections::BTreeMap;
use zenith_core::{ActionDef, KdlAdapter, KdlSource, LibraryDef, ProvenanceDef};
use zenith_tx::{Transaction, TxResult, TxStatus, run_transaction};
use super::add::{
AddError, collect_all_ids, dependency_conflict, load_pack_document, unique_id,
unknown_package_error,
};
use super::registry::LibraryPack;
#[derive(Debug, Clone, PartialEq)]
pub struct ActionAddOutcome {
pub pkg_id: String,
pub item: String,
pub tx_result: TxResult,
pub final_source: Option<String>,
pub provenance_id: Option<String>,
pub warnings: Vec<String>,
}
pub fn materialize_action(
target_src: &str,
packs: &[LibraryPack],
pkg_id: &str,
action_id: &str,
) -> Result<ActionAddOutcome, AddError> {
let pack = packs
.iter()
.find(|p| p.id == pkg_id)
.ok_or_else(|| unknown_package_error(pkg_id, packs))?;
let pack_doc = load_pack_document(pack)?;
let pack_action = pack_doc
.actions
.iter()
.find(|a| a.id == action_id)
.ok_or_else(|| {
let available: Vec<&str> = pack_doc.actions.iter().map(|a| a.id.as_str()).collect();
AddError::new(format!(
"unknown action item '{}' in package '{}' (available: {})",
action_id,
pkg_id,
if available.is_empty() {
"none".to_owned()
} else {
available.join(", ")
}
))
})?;
let target_doc = KdlAdapter
.parse(target_src.as_bytes())
.map_err(|e| AddError::new(format!("error parsing target document: {}", e)))?;
let tx = Transaction::from_json(&pack_action.tx_json).map_err(|e| {
AddError::new(format!(
"malformed tx-script in action '{}': {}",
action_id, e
))
})?;
let tx_result = run_transaction(&target_doc, &tx)
.map_err(|e| AddError::new(format!("transaction error: {}", e)))?;
match &tx_result.status {
TxStatus::Rejected => {
return Ok(ActionAddOutcome {
pkg_id: pkg_id.to_owned(),
item: action_id.to_owned(),
tx_result,
final_source: None,
provenance_id: None,
warnings: vec![],
});
}
TxStatus::Accepted | TxStatus::AcceptedWithWarnings => {}
}
let mut result_doc = KdlAdapter
.parse(tx_result.source_after.as_bytes())
.map_err(|e| {
AddError::new(format!(
"internal error: could not re-parse transaction output: {}",
e
))
})?;
let mut warnings: Vec<String> = Vec::new();
match result_doc.actions.iter().find(|a| a.id == action_id) {
Some(existing) if existing.tx_json != pack_action.tx_json => {
warnings.push(dependency_conflict("action", action_id));
}
Some(_) => {}
None => result_doc.actions.push(ActionDef {
id: pack_action.id.clone(),
label: pack_action.label.clone(),
version: pack_action.version.clone(),
tx_json: pack_action.tx_json.clone(),
source_span: None,
unknown_props: BTreeMap::new(),
}),
}
if !result_doc.libraries.iter().any(|l| l.id == pkg_id) {
result_doc.libraries.push(LibraryDef {
id: pkg_id.to_owned(),
version: pack.version.clone(),
hash: None,
source_span: None,
unknown_props: BTreeMap::new(),
});
}
let all_ids = collect_all_ids(&result_doc);
let provenance_id = unique_id(&format!("prov.{}", action_id), &all_ids);
result_doc.provenance.push(ProvenanceDef {
id: provenance_id.clone(),
node: action_id.to_owned(),
library: pkg_id.to_owned(),
item: Some(action_id.to_owned()),
linked: Some(true),
source_span: None,
unknown_props: BTreeMap::new(),
});
let final_bytes = KdlAdapter
.format(&result_doc)
.map_err(|e| AddError::new(format!("error formatting result document: {}", e)))?;
let final_source = String::from_utf8(final_bytes)
.map_err(|e| AddError::new(format!("error encoding result document: {}", e)))?;
Ok(ActionAddOutcome {
pkg_id: pkg_id.to_owned(),
item: action_id.to_owned(),
tx_result,
final_source: Some(final_source),
provenance_id: Some(provenance_id),
warnings,
})
}