Skip to main content

ggen_graph/
interchangeable.rs

1//! Interchangeable parts architecture for ggen-graph.
2//!
3//! Provides the core abstractions:
4//! 1. `GenesisCore`: The embedded RDF graph storage core.
5//! 2. `OuterMembrane`: Admission control, validation, and sanitization boundary.
6//! 3. `AdapterLayer`: Protocol translation layer (e.g. JSON-RPC to internal deltas).
7//! 4. `ProjectionLayer`: Outward-facing state projectors (OCEL, PROV, receipts).
8
9use crate::delta::RdfDelta;
10use crate::graph::dataset::DeterministicGraph;
11use crate::ocel::{EvidenceProjector, OcelLog};
12use crate::receipt::GraphReceipt;
13use crate::GraphError;
14use oxigraph::model::Quad;
15use serde_json::Value;
16
17/// Genesis acts as the embedded core of the interchangeable part architecture.
18#[derive(Clone)]
19pub struct GenesisCore {
20    graph: DeterministicGraph,
21}
22
23impl GenesisCore {
24    /// Create a new instance of the Genesis core.
25    pub fn new() -> Result<Self, GraphError> {
26        Ok(Self {
27            graph: DeterministicGraph::new()?,
28        })
29    }
30
31    /// Access the underlying deterministic graph.
32    pub fn graph(&self) -> &DeterministicGraph {
33        &self.graph
34    }
35
36    /// Execute a transaction with the core using an RdfDelta.
37    pub fn execute_transaction(&self, delta: &RdfDelta) -> Result<GraphReceipt, GraphError> {
38        self.graph.apply_delta(delta, &[])
39    }
40}
41
42/// The Outer Membrane forms the boundary around the Genesis core, intercepting,
43/// validating, and sanitizing all inputs and operations.
44#[derive(Default)]
45pub struct OuterMembrane;
46
47impl OuterMembrane {
48    /// Create a new instance of the Outer Membrane.
49    pub fn new() -> Self {
50        Self
51    }
52
53    /// Admits and sanitizes an incoming SPARQL query or N-Quad string.
54    ///
55    /// # Errors
56    ///
57    /// Returns `GraphError` if the query is empty or contains injection attempts.
58    pub fn admit_input(&self, query: &str) -> Result<(), GraphError> {
59        let trimmed = query.trim();
60        if trimmed.is_empty() {
61            return Err(GraphError::Other(
62                "Empty input rejected by outer membrane".to_string(),
63            ));
64        }
65
66        // Active injection/sabotage scanner
67        let upper = trimmed.to_uppercase();
68        if upper.contains("DROP GRAPH") || upper.contains("DELETE WHERE") {
69            return Err(GraphError::Other(
70                "Security violation: injection pattern blocked by outer membrane".to_string(),
71            ));
72        }
73
74        Ok(())
75    }
76
77    /// Validates a list of quads before admitting them to the core.
78    ///
79    /// # Errors
80    ///
81    /// Returns `GraphError` if any quad fails validation checks.
82    pub fn validate_quads(&self, quads: &[Quad]) -> Result<(), GraphError> {
83        for q in quads {
84            let predicate_uri = q.predicate.as_str();
85            if predicate_uri.is_empty() {
86                return Err(GraphError::Other(
87                    "Invalid predicate rejected by outer membrane".to_string(),
88                ));
89            }
90            if q.subject.to_string().is_empty() {
91                return Err(GraphError::Other(
92                    "Invalid subject rejected by outer membrane".to_string(),
93                ));
94            }
95        }
96        Ok(())
97    }
98}
99
100/// The Adapter Layer bridges external protocols (like JSON-RPC) into core-native types.
101pub struct AdapterLayer;
102
103impl AdapterLayer {
104    /// Adapts a JSON-RPC-like request map to an internal `RdfDelta`.
105    ///
106    /// # Errors
107    ///
108    /// Returns `GraphError` if the request is invalid or missing required fields.
109    pub fn adapt_json_rpc(request: &Value) -> Result<RdfDelta, GraphError> {
110        let method = request
111            .get("method")
112            .and_then(|m| m.as_str())
113            .ok_or_else(|| GraphError::Other("Missing JSON-RPC method".to_string()))?;
114
115        if method != "apply_delta" {
116            return Err(GraphError::Other(format!("Unsupported method: {}", method)));
117        }
118
119        let params = request
120            .get("params")
121            .ok_or_else(|| GraphError::Other("Missing JSON-RPC params".to_string()))?;
122
123        let additions_arr = params
124            .get("additions")
125            .and_then(|a| a.as_array())
126            .ok_or_else(|| {
127                GraphError::Other("Missing or invalid additions in params".to_string())
128            })?;
129
130        let deletions_arr = params
131            .get("deletions")
132            .and_then(|d| d.as_array())
133            .ok_or_else(|| {
134                GraphError::Other("Missing or invalid deletions in params".to_string())
135            })?;
136
137        let mut additions = Vec::new();
138        for val in additions_arr {
139            let s = val
140                .as_str()
141                .ok_or_else(|| GraphError::Other("Non-string addition quad".to_string()))?;
142            additions.push(s.to_string());
143        }
144
145        let mut deletions = Vec::new();
146        for val in deletions_arr {
147            let s = val
148                .as_str()
149                .ok_or_else(|| GraphError::Other("Non-string deletion quad".to_string()))?;
150            deletions.push(s.to_string());
151        }
152
153        Ok(RdfDelta::new(additions, deletions))
154    }
155}
156
157/// The Projection Layer projects the internal core state or transitions into external formats.
158pub struct ProjectionLayer;
159
160impl ProjectionLayer {
161    /// Projects the core state into an `OcelLog` format.
162    pub fn project_state_to_ocel(core: &GenesisCore) -> Result<OcelLog, GraphError> {
163        EvidenceProjector::extract_ocel(core.graph())
164    }
165
166    /// Projects an `OcelLog` back into the core state.
167    pub fn project_ocel_to_state(core: &GenesisCore, log: &OcelLog) -> Result<(), GraphError> {
168        EvidenceProjector::project_ocel(core.graph(), log)
169    }
170}