mod decisions;
mod tree;
use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::{Value, json};
pub use tree::ContentHash;
use crate::FORMAT;
use crate::canonical::{Digest, Domain, canonical_bytes, digest, digest_array};
use crate::document::{DerivedFrom, Document, Edge, EdgeTarget, SubjectRole};
use crate::identity::{Key, Label};
use crate::ids::{DecisionId, ElementId, NodeId};
use crate::index::{IndexErrors, Indexed};
use crate::manifest::ALGORITHM;
pub(crate) type NodeIndex = BTreeMap<Key, Digest>;
pub(crate) fn node_hash(index: &NodeIndex, id: NodeId) -> Digest {
*index
.get(&id.key())
.expect("an indexed document holds every node it names")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Roots {
pub edges: Digest,
pub tags: Digest,
pub categories: Digest,
pub decisions: Digest,
pub content: Digest,
pub bindings: Digest,
pub state: Digest,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentHashes {
pub nodes: BTreeMap<NodeId, Digest>,
pub decisions: BTreeMap<DecisionId, Digest>,
pub products: Vec<Digest>,
pub components: Vec<Digest>,
pub roots: Roots,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Views {
pub nodes: BTreeMap<NodeId, Value>,
pub decisions: BTreeMap<DecisionId, Value>,
pub roots: Vec<(&'static str, Domain, Value)>,
}
struct Trees {
nodes: BTreeMap<NodeId, Digest>,
products: Vec<Digest>,
components: Vec<Digest>,
}
impl Trees {
fn index(&self) -> NodeIndex {
self.nodes
.iter()
.map(|(id, hash)| (id.key(), *hash))
.collect()
}
}
fn hash_trees(document: &Document, mut views: Option<&mut BTreeMap<NodeId, Value>>) -> Trees {
let mut nodes = BTreeMap::new();
let mut visit = |id: Option<NodeId>, view: &tree::NodeView<'_>, hash: Digest| {
if let Some(id) = id {
nodes.insert(id, hash);
if let Some(views) = views.as_deref_mut() {
views.insert(id, view.to_value());
}
}
};
let products = document
.products
.iter()
.map(|product| tree::hash_with(product.into(), &mut visit))
.collect();
let mut components: Vec<Digest> = document
.components
.iter()
.map(|root| tree::hash_with((&root.component).into(), &mut visit))
.collect();
components.sort_unstable();
Trees {
nodes,
products,
components,
}
}
pub fn hash_document(document: &Document) -> Result<DocumentHashes, IndexErrors> {
Ok(hash_indexed(&document.index()?))
}
#[must_use]
pub fn hash_indexed(indexed: &Indexed<'_>) -> DocumentHashes {
hash(indexed, None)
}
#[must_use]
pub fn hash_indexed_with_views(indexed: &Indexed<'_>) -> (DocumentHashes, Views) {
let mut views = Views::default();
let hashes = hash(indexed, Some(&mut views));
(hashes, views)
}
fn root<T: Serialize>(
views: &mut Option<&mut Views>,
name: &'static str,
domain: Domain,
view: &T,
) -> Digest {
if let Some(views) = views {
let value = serde_json::to_value(view).expect("a root view serialises");
views.roots.push((name, domain, value));
}
digest(domain, view)
}
fn hash(indexed: &Indexed<'_>, mut views: Option<&mut Views>) -> DocumentHashes {
let document = indexed.document();
let trees = hash_trees(document, views.as_deref_mut().map(|views| &mut views.nodes));
let index = trees.index();
let (decisions, decision_views) =
decisions::DecisionHasher::new(&document.decisions, &index).hash_all();
let resolve = |element: ElementId| match element {
ElementId::Node(id) => node_hash(&index, id),
ElementId::Decision(id) => decisions[&id],
};
let edges = edges_root(&document.edges, &resolve, &mut views);
let tag_rows = document
.tags
.iter()
.map(|tag| [tag.name.as_str(), tag.definition.as_str()]);
let tags = root(
&mut views,
"tags",
Domain::Registry,
®istry_view("tags", tag_rows),
);
let category_rows = document
.categories
.iter()
.map(|category| [category.name.as_str(), category.definition.as_str()]);
let categories = root(
&mut views,
"categories",
Domain::Registry,
®istry_view("categories", category_rows),
);
let mut decision_hashes: Vec<Digest> = decisions.values().copied().collect();
decision_hashes.sort_unstable();
let decisions_root = root(&mut views, "decisions", Domain::Decisions, &decision_hashes);
let content_view = (
&trees.products,
&trees.components,
edges,
tags,
categories,
decisions_root,
);
let content = root(&mut views, "content", Domain::Content, &content_view);
let bindings = root(
&mut views,
"bindings",
Domain::Bindings,
&bindings_view(document, &resolve),
);
let state = root(
&mut views,
"state",
Domain::State,
&state_view(document, content, bindings),
);
if let Some(views) = views {
views.decisions = decision_views;
}
DocumentHashes {
nodes: trees.nodes,
decisions,
products: trees.products,
components: trees.components,
roots: Roots {
edges,
tags,
categories,
decisions: decisions_root,
content,
bindings,
state,
},
}
}
pub(crate) fn former(label: Label) -> Value {
json!({ "formerLabel": label })
}
fn edges_root(
edges: &[Edge],
resolve: &impl Fn(ElementId) -> Digest,
views: &mut Option<&mut Views>,
) -> Digest {
let mut tuples: Vec<Vec<u8>> = edges
.iter()
.map(|edge| {
let target = match edge.target() {
EdgeTarget::Element(element) => json!(resolve(element)),
EdgeTarget::Former(label) => former(label),
};
canonical_bytes(&(
resolve(edge.source()),
edge.kind().as_str(),
target,
edge.role().map(SubjectRole::as_str),
))
})
.collect();
tuples.sort_unstable();
if let Some(views) = views {
let parse = |bytes: &Vec<u8>| -> Value {
serde_json::from_slice(bytes).expect("canonical bytes are JSON")
};
let value = Value::Array(tuples.iter().map(parse).collect());
views.roots.push(("edges", Domain::Edges, value));
}
digest_array(Domain::Edges, &tuples)
}
fn registry_view<'a>(
name: &'a str,
entries: impl Iterator<Item = [&'a str; 2]>,
) -> (&'a str, Vec<[&'a str; 2]>) {
let mut rows: Vec<[&str; 2]> = entries.collect();
rows.sort_unstable();
(name, rows)
}
fn bindings_view<'a>(
document: &'a Document,
resolve: &impl Fn(ElementId) -> Digest,
) -> Vec<(Key, Label, &'a [Label], Digest)> {
document
.bindings
.iter()
.map(|binding| {
(
binding.element.key(),
binding.label,
binding.aliases.as_slice(),
resolve(binding.element),
)
})
.collect()
}
fn state_view(document: &Document, content: Digest, bindings: Digest) -> Value {
let derived_from = document.derived_from.as_ref().map(
|DerivedFrom {
authored_state_id,
content_address,
publish_sequence,
}| {
json!({
"authoredStateId": authored_state_id,
"contentAddress": content_address,
"publishSequence": publish_sequence,
})
},
);
json!([
FORMAT,
ALGORITHM,
document.id,
content,
bindings,
derived_from
])
}