#![allow(clippy::implicit_hasher)]
pub mod asymmetric;
pub mod combinators;
pub mod compose;
pub mod error;
pub mod laws;
pub mod symmetric;
pub use asymmetric::{Complement, get, put};
pub use combinators::{Combinator, from_combinators};
pub use compose::compose;
pub use error::{LawViolation, LensError};
pub use laws::{check_get_put, check_laws, check_put_get};
pub use symmetric::SymmetricLens;
use panproto_inst::CompiledMigration;
use panproto_schema::Schema;
pub struct Lens {
pub compiled: CompiledMigration,
pub src_schema: Schema,
pub tgt_schema: Schema,
}
#[cfg(test)]
pub(crate) mod tests {
use std::collections::HashMap;
use panproto_inst::value::{FieldPresence, Value};
use panproto_inst::{CompiledMigration, Node, WInstance};
use panproto_schema::{Edge, Schema, Vertex};
use smallvec::SmallVec;
use crate::Lens;
pub fn three_node_schema() -> Schema {
let mut vertices = HashMap::new();
vertices.insert(
"post:body".to_string(),
Vertex {
id: "post:body".to_string(),
kind: "object".to_string(),
nsid: None,
},
);
vertices.insert(
"post:body.text".to_string(),
Vertex {
id: "post:body.text".to_string(),
kind: "string".to_string(),
nsid: None,
},
);
vertices.insert(
"post:body.createdAt".to_string(),
Vertex {
id: "post:body.createdAt".to_string(),
kind: "string".to_string(),
nsid: None,
},
);
let edge_text = Edge {
src: "post:body".to_string(),
tgt: "post:body.text".to_string(),
kind: "prop".to_string(),
name: Some("text".to_string()),
};
let edge_created = Edge {
src: "post:body".to_string(),
tgt: "post:body.createdAt".to_string(),
kind: "prop".to_string(),
name: Some("createdAt".to_string()),
};
let mut edges = HashMap::new();
edges.insert(edge_text.clone(), "prop".to_string());
edges.insert(edge_created.clone(), "prop".to_string());
let mut outgoing: HashMap<String, SmallVec<Edge, 4>> = HashMap::new();
outgoing
.entry("post:body".to_string())
.or_default()
.push(edge_text.clone());
outgoing
.entry("post:body".to_string())
.or_default()
.push(edge_created.clone());
let mut incoming: HashMap<String, SmallVec<Edge, 4>> = HashMap::new();
incoming
.entry("post:body.text".to_string())
.or_default()
.push(edge_text.clone());
incoming
.entry("post:body.createdAt".to_string())
.or_default()
.push(edge_created.clone());
let mut between: HashMap<(String, String), SmallVec<Edge, 2>> = HashMap::new();
between
.entry(("post:body".to_string(), "post:body.text".to_string()))
.or_default()
.push(edge_text);
between
.entry(("post:body".to_string(), "post:body.createdAt".to_string()))
.or_default()
.push(edge_created);
Schema {
protocol: "test".to_string(),
vertices,
edges,
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
outgoing,
incoming,
between,
}
}
pub fn three_node_instance() -> WInstance {
let mut nodes = HashMap::new();
nodes.insert(0, Node::new(0, "post:body"));
nodes.insert(
1,
Node::new(1, "post:body.text")
.with_value(FieldPresence::Present(Value::Str("hello".into()))),
);
nodes.insert(
2,
Node::new(2, "post:body.createdAt")
.with_value(FieldPresence::Present(Value::Str("2024-01-01".into()))),
);
let arcs = vec![
(
0,
1,
Edge {
src: "post:body".into(),
tgt: "post:body.text".into(),
kind: "prop".into(),
name: Some("text".into()),
},
),
(
0,
2,
Edge {
src: "post:body".into(),
tgt: "post:body.createdAt".into(),
kind: "prop".into(),
name: Some("createdAt".into()),
},
),
];
WInstance::new(nodes, arcs, vec![], 0, "post:body".into())
}
pub fn identity_lens(schema: &Schema) -> Lens {
let surviving_verts = schema.vertices.keys().cloned().collect();
let surviving_edges = schema.edges.keys().cloned().collect();
let compiled = CompiledMigration {
surviving_verts,
surviving_edges,
vertex_remap: HashMap::new(),
edge_remap: HashMap::new(),
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
};
Lens {
compiled,
src_schema: schema.clone(),
tgt_schema: schema.clone(),
}
}
pub fn projection_lens(schema: &Schema, field_to_remove: &str) -> Lens {
let mut tgt_schema = schema.clone();
let edges_to_remove: Vec<Edge> = tgt_schema
.edges
.keys()
.filter(|e| e.name.as_deref() == Some(field_to_remove))
.cloned()
.collect();
let mut removed_vertices = Vec::new();
for edge in &edges_to_remove {
tgt_schema.edges.remove(edge);
tgt_schema.vertices.remove(&edge.tgt);
removed_vertices.push(edge.tgt.clone());
}
crate::combinators::rebuild_indices_pub(&mut tgt_schema);
let mut surviving_verts: std::collections::HashSet<String> =
schema.vertices.keys().cloned().collect();
let mut surviving_edges: std::collections::HashSet<Edge> =
schema.edges.keys().cloned().collect();
for v in &removed_vertices {
surviving_verts.remove(v);
}
for e in &edges_to_remove {
surviving_edges.remove(e);
}
let compiled = CompiledMigration {
surviving_verts,
surviving_edges,
vertex_remap: HashMap::new(),
edge_remap: HashMap::new(),
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
};
Lens {
compiled,
src_schema: schema.clone(),
tgt_schema,
}
}
#[test]
fn round_trip_get_then_put_recovers_original() {
let schema = three_node_schema();
let lens = identity_lens(&schema);
let instance = three_node_instance();
let (view, complement) =
crate::get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
let restored =
crate::put(&lens, &view, &complement).unwrap_or_else(|e| panic!("put failed: {e}"));
assert_eq!(restored.node_count(), instance.node_count());
assert_eq!(restored.root, instance.root);
assert_eq!(restored.schema_root, instance.schema_root);
for (&id, node) in &instance.nodes {
let restored_node = restored
.nodes
.get(&id)
.unwrap_or_else(|| panic!("node {id} missing from restored instance"));
assert_eq!(
node.anchor, restored_node.anchor,
"anchor mismatch for node {id}"
);
}
}
#[test]
fn modified_view_propagates_changes() {
let schema = three_node_schema();
let lens = identity_lens(&schema);
let instance = three_node_instance();
let (mut view, complement) =
crate::get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
if let Some(node) = view.nodes.get_mut(&1) {
node.value = Some(FieldPresence::Present(Value::Str("modified".into())));
}
let restored =
crate::put(&lens, &view, &complement).unwrap_or_else(|e| panic!("put failed: {e}"));
let node = restored
.nodes
.get(&1)
.unwrap_or_else(|| panic!("node 1 missing"));
assert_eq!(
node.value,
Some(FieldPresence::Present(Value::Str("modified".into()))),
"modification should be preserved"
);
}
#[test]
fn projection_lens_drops_field() {
let schema = three_node_schema();
let lens = projection_lens(&schema, "createdAt");
let instance = three_node_instance();
let (view, complement) =
crate::get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
assert_eq!(view.node_count(), 2, "projection should drop one node");
assert!(
!complement.dropped_nodes.is_empty(),
"complement should have dropped node"
);
}
#[test]
fn projection_get_then_put_restores_with_complement() {
let schema = three_node_schema();
let lens = projection_lens(&schema, "createdAt");
let instance = three_node_instance();
let (view, complement) =
crate::get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
let restored =
crate::put(&lens, &view, &complement).unwrap_or_else(|e| panic!("put failed: {e}"));
assert_eq!(
restored.node_count(),
instance.node_count(),
"restoration should bring back all nodes"
);
}
#[test]
fn compose_rename_then_identity_preserves_laws() {
let schema = three_node_schema();
let l1 = identity_lens(&schema);
let l2 = identity_lens(&schema);
let composed = crate::compose(&l1, &l2).unwrap_or_else(|e| panic!("compose failed: {e}"));
let instance = three_node_instance();
let result = crate::check_laws(&composed, &instance);
assert!(
result.is_ok(),
"composed identity lenses should satisfy laws: {result:?}"
);
}
}