use crate::runtime::control_plane::admission::AdmittedRelationGraph;
use crate::runtime::control_plane::receipts::to_hex;
use wasm4pm_compat::admission::{Admission, Admit, Refusal};
use wasm4pm_compat::evidence::Evidence;
use wasm4pm_compat::ocel::{OCELEvent, OCELObject, OCELRelationship, OCEL};
use wasm4pm_compat::receipt::{Digest, ReceiptEnvelope, ReceiptRefusal, ReplayHint};
use wasm4pm_compat::state::Raw;
use wasm4pm_compat::witness::{Ocel20, Wasm4pmBridge};
pub struct Ocel20GraphAdmitter;
impl Admit for Ocel20GraphAdmitter {
type Raw = AdmittedRelationGraph;
type Admitted = OCEL;
type Reason = String;
type Witness = Ocel20;
fn admit(
raw: Evidence<Self::Raw, Raw, Self::Witness>,
) -> Result<Admission<Self::Admitted, Self::Witness>, Refusal<Self::Reason, Self::Witness>>
{
let admitted_graph = raw.value;
let mut events = Vec::new();
let mut objects = Vec::new();
for quad in admitted_graph.store.iter().flatten() {
let s = quad.subject.to_string();
let p = quad.predicate.to_string();
let o = quad.object.to_string();
if p.contains("type") && o.contains("Event") {
let mut ev = OCELEvent::new(s.clone(), "ExtractedEvent");
ev.relationships
.push(OCELRelationship::new(s.clone(), "root_obj".to_string()));
events.push(ev);
} else if p.contains("type") && o.contains("Object") {
objects.push(OCELObject::new(s, "ExtractedObject"));
}
}
if objects.is_empty() && !events.is_empty() {
objects.push(OCELObject::new("root_obj".to_string(), "ExtractedObject"));
}
let ocel = OCEL {
event_types: vec![],
object_types: vec![],
events,
objects,
};
let report =
wasm4pm_compat::ocel::validate::validate(&ocel, &std::collections::HashMap::new());
if !report.valid {
let first_error = report
.errors
.first()
.map(|e| e.code.clone())
.unwrap_or_else(|| "UNKNOWN_VALIDATION_ERROR".to_string());
return Err(Refusal::new(first_error));
}
Ok(Admission::new(ocel))
}
}
pub struct Wasm4pmBridgeGraphAdmitter;
impl Admit for Wasm4pmBridgeGraphAdmitter {
type Raw = AdmittedRelationGraph;
type Admitted = ReceiptEnvelope;
type Reason = ReceiptRefusal;
type Witness = Wasm4pmBridge;
fn admit(
raw: Evidence<Self::Raw, Raw, Self::Witness>,
) -> Result<Admission<Self::Admitted, Self::Witness>, Refusal<Self::Reason, Self::Witness>>
{
let admitted_graph = raw.value;
let receipt = &admitted_graph.receipt;
let subject = match &admitted_graph.graph_name {
oxigraph::model::GraphName::NamedNode(node) => node.as_str().to_string(),
_ => "default-graph".to_string(),
};
if subject.is_empty() {
return Err(Refusal::new(ReceiptRefusal::MissingSubject));
}
if receipt.sequence == 0 {
return Err(Refusal::new(ReceiptRefusal::UnreplayableClaim));
}
let digest = Digest::new(format!("blake3:{}", to_hex(&receipt.consequence_hash.0)));
let replay_hint =
ReplayHint::new(format!("lsp-max://replay/sequence/{}", receipt.sequence));
let envelope =
match ReceiptEnvelope::try_from_parts(subject, "wasm4pm-bridge", digest, replay_hint) {
Ok(env) => env,
Err(refusal) => return Err(Refusal::new(refusal)),
};
Ok(Admission::new(envelope))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::control_plane::receipts::{Blake3Hash, CryptographicReceipt};
use oxigraph::model::{GraphName, NamedNode, Quad, Term};
use oxigraph::store::Store;
use uuid::Uuid;
fn setup_test_graph_and_receipt(
sequence: u64,
discipline_id: Uuid,
law_id: Uuid,
) -> AdmittedRelationGraph {
let store = Store::new().unwrap();
let graph_uri = "urn:project:local:snapshot:snap-test";
let graph_name = GraphName::NamedNode(NamedNode::new(graph_uri).unwrap());
store
.insert(&Quad::new(
NamedNode::new("urn:subject:1").unwrap(),
NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type").unwrap(),
Term::NamedNode(NamedNode::new("urn:object:1").unwrap()),
graph_name.clone(),
))
.unwrap();
let receipt = CryptographicReceipt {
prev_hash: Blake3Hash([0u8; 32]),
discipline_id,
law_id,
consequence_hash: Blake3Hash([1u8; 32]),
sequence,
signature: [2u8; 64],
};
AdmittedRelationGraph {
store,
graph_name,
receipt,
}
}
#[test]
fn test_ocel20_graph_admission_success() {
let graph = setup_test_graph_and_receipt(1, Uuid::new_v4(), Uuid::new_v4());
let raw_evidence = Evidence::raw(graph);
let admission_res = Ocel20GraphAdmitter::admit(raw_evidence);
assert!(admission_res.is_ok());
let admission = admission_res.unwrap();
let ocel_log = admission.value;
assert_eq!(ocel_log.events.len(), 0); assert_eq!(ocel_log.objects.len(), 0);
}
#[test]
fn test_ocel20_graph_admission_refusal_missing_objects() {
let store = Store::new().unwrap();
store
.insert(&Quad::new(
NamedNode::new("urn:subject:1").unwrap(),
NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type").unwrap(),
Term::NamedNode(NamedNode::new("urn:event-type:Event").unwrap()),
GraphName::NamedNode(
NamedNode::new("urn:project:local:snapshot:snap-test").unwrap(),
),
))
.unwrap();
let graph = AdmittedRelationGraph {
store,
graph_name: GraphName::NamedNode(
NamedNode::new("urn:project:local:snapshot:snap-test").unwrap(),
),
receipt: CryptographicReceipt {
prev_hash: Blake3Hash([0u8; 32]),
discipline_id: Uuid::new_v4(),
law_id: Uuid::new_v4(),
consequence_hash: Blake3Hash([1u8; 32]),
sequence: 1,
signature: [2u8; 64],
},
};
let raw_evidence = Evidence::raw(graph);
let admission_res = Ocel20GraphAdmitter::admit(raw_evidence);
assert!(admission_res.is_err());
}
#[test]
fn test_wasm4pm_bridge_graph_admission_success() {
let graph = setup_test_graph_and_receipt(1, Uuid::new_v4(), Uuid::new_v4());
let raw_evidence = Evidence::raw(graph);
let admission_res = Wasm4pmBridgeGraphAdmitter::admit(raw_evidence);
assert!(admission_res.is_ok());
let admission = admission_res.unwrap();
let envelope = admission.value;
assert_eq!(envelope.subject, "urn:project:local:snapshot:snap-test");
assert_eq!(envelope.witness, "wasm4pm-bridge");
assert!(envelope.is_well_shaped());
}
#[test]
fn test_wasm4pm_bridge_graph_admission_refusal_sequence_zero() {
let graph = setup_test_graph_and_receipt(0, Uuid::new_v4(), Uuid::new_v4());
let raw_evidence = Evidence::raw(graph);
let admission_res = Wasm4pmBridgeGraphAdmitter::admit(raw_evidence);
assert!(admission_res.is_err());
let refusal = admission_res.unwrap_err();
assert_eq!(refusal.reason, ReceiptRefusal::UnreplayableClaim);
}
}