use std::collections::HashMap;
use panproto_inst::CompiledMigration;
use panproto_inst::value::Value;
use panproto_schema::{Edge, Protocol, Schema, Vertex};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::Lens;
use crate::error::LensError;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Combinator {
RenameField {
old: String,
new: String,
},
AddField {
name: String,
vertex_kind: String,
default: Value,
},
RemoveField {
name: String,
},
WrapInObject {
field_name: String,
},
HoistField {
host: String,
field: String,
},
CoerceType {
from_kind: String,
to_kind: String,
},
Compose(Box<Self>, Box<Self>),
}
pub fn from_combinators(
src: &Schema,
combinators: &[Combinator],
_protocol: &Protocol,
) -> Result<Lens, LensError> {
if combinators.is_empty() {
return Ok(Lens {
compiled: identity_compiled(src),
src_schema: src.clone(),
tgt_schema: src.clone(),
});
}
let mut current_schema = src.clone();
let mut composed_migration: Option<CompiledMigration> = None;
for combinator in combinators {
let next_schema = apply_combinator(¤t_schema, combinator)?;
let step_migration = build_compiled_migration(
¤t_schema,
&next_schema,
std::slice::from_ref(combinator),
);
composed_migration = Some(match composed_migration {
Some(prev) => crate::compose::compose_compiled_migrations(&prev, &step_migration),
None => step_migration,
});
current_schema = next_schema;
}
let compiled = composed_migration.unwrap_or_else(|| identity_compiled(src));
Ok(Lens {
compiled,
src_schema: src.clone(),
tgt_schema: current_schema,
})
}
fn apply_combinator(schema: &Schema, combinator: &Combinator) -> Result<Schema, LensError> {
match combinator {
Combinator::RenameField { old, new } => apply_rename(schema, old, new),
Combinator::AddField {
name, vertex_kind, ..
} => apply_add_field(schema, name, vertex_kind),
Combinator::RemoveField { name } => apply_remove_field(schema, name),
Combinator::WrapInObject { field_name } => apply_wrap_in_object(schema, field_name),
Combinator::HoistField { host, field } => apply_hoist_field(schema, host, field),
Combinator::CoerceType { from_kind, to_kind } => {
apply_coerce_type(schema, from_kind, to_kind)
}
Combinator::Compose(first, second) => {
let intermediate = apply_combinator(schema, first)?;
apply_combinator(&intermediate, second)
}
}
}
fn apply_rename(schema: &Schema, old: &str, new: &str) -> Result<Schema, LensError> {
let has_match = schema.edges.keys().any(|e| e.name.as_deref() == Some(old));
if !has_match {
return Err(LensError::FieldNotFound(old.to_string()));
}
let mut result = schema.clone();
let edges_to_update: Vec<Edge> = result
.edges
.keys()
.filter(|e| e.name.as_deref() == Some(old))
.cloned()
.collect();
for edge in edges_to_update {
let kind = result.edges.remove(&edge).unwrap_or_default();
let mut new_edge = edge.clone();
new_edge.name = Some(new.to_string());
result.edges.insert(new_edge, kind);
}
rebuild_indices(&mut result);
Ok(result)
}
fn apply_add_field(schema: &Schema, name: &str, vertex_kind: &str) -> Result<Schema, LensError> {
let mut result = schema.clone();
let root_id = find_root_vertex(schema)?;
let new_vertex_id = format!("{root_id}.{name}");
result.vertices.insert(
new_vertex_id.clone(),
Vertex {
id: new_vertex_id.clone(),
kind: vertex_kind.to_string(),
nsid: None,
},
);
let new_edge = Edge {
src: root_id,
tgt: new_vertex_id,
kind: "prop".to_string(),
name: Some(name.to_string()),
};
result.edges.insert(new_edge, "prop".to_string());
rebuild_indices(&mut result);
Ok(result)
}
fn apply_remove_field(schema: &Schema, name: &str) -> Result<Schema, LensError> {
let matching_edges: Vec<Edge> = schema
.edges
.keys()
.filter(|e| e.name.as_deref() == Some(name))
.cloned()
.collect();
if matching_edges.is_empty() {
return Err(LensError::FieldNotFound(name.to_string()));
}
let mut result = schema.clone();
for edge in &matching_edges {
result.vertices.remove(&edge.tgt);
result.edges.remove(edge);
result.constraints.remove(&edge.tgt);
result.required.remove(&edge.tgt);
let removed_vertex = &edge.tgt;
let to_remove: Vec<Edge> = result
.edges
.keys()
.filter(|e| e.src == *removed_vertex || e.tgt == *removed_vertex)
.cloned()
.collect();
for e in to_remove {
result.edges.remove(&e);
}
}
rebuild_indices(&mut result);
Ok(result)
}
fn apply_wrap_in_object(schema: &Schema, field_name: &str) -> Result<Schema, LensError> {
let root_id = find_root_vertex(schema)?;
let mut result = schema.clone();
let wrapper_id = format!("{root_id}.{field_name}");
result.vertices.insert(
wrapper_id.clone(),
Vertex {
id: wrapper_id.clone(),
kind: "object".to_string(),
nsid: None,
},
);
let wrapper_edge = Edge {
src: root_id.clone(),
tgt: wrapper_id.clone(),
kind: "prop".to_string(),
name: Some(field_name.to_string()),
};
result.edges.insert(wrapper_edge, "prop".to_string());
let root_edges: Vec<Edge> = result
.edges
.keys()
.filter(|e| e.src == root_id && e.name.as_deref() != Some(field_name))
.cloned()
.collect();
for edge in root_edges {
let kind = result.edges.remove(&edge).unwrap_or_default();
let mut new_edge = edge;
new_edge.src.clone_from(&wrapper_id);
result.edges.insert(new_edge, kind);
}
rebuild_indices(&mut result);
Ok(result)
}
fn apply_hoist_field(schema: &Schema, host: &str, field: &str) -> Result<Schema, LensError> {
let field_edge = schema
.edges
.keys()
.find(|e| e.src == host && e.name.as_deref() == Some(field))
.cloned()
.ok_or_else(|| LensError::FieldNotFound(format!("{host}.{field}")))?;
let parent_edge = schema
.edges
.keys()
.find(|e| e.tgt == host)
.cloned()
.ok_or_else(|| LensError::VertexNotFound(format!("parent of {host}")))?;
let mut result = schema.clone();
let kind = result.edges.remove(&field_edge).unwrap_or_default();
let new_edge = Edge {
src: parent_edge.src,
tgt: field_edge.tgt,
kind: kind.clone(),
name: field_edge.name,
};
result.edges.insert(new_edge, kind);
rebuild_indices(&mut result);
Ok(result)
}
fn apply_coerce_type(schema: &Schema, from_kind: &str, to_kind: &str) -> Result<Schema, LensError> {
let has_match = schema.vertices.values().any(|v| v.kind == from_kind);
if !has_match {
return Err(LensError::IncompatibleCoercion {
from: from_kind.to_string(),
to: to_kind.to_string(),
});
}
let mut result = schema.clone();
for vertex in result.vertices.values_mut() {
if vertex.kind == from_kind {
vertex.kind = to_kind.to_string();
}
}
Ok(result)
}
fn find_root_vertex(schema: &Schema) -> Result<String, LensError> {
let mut roots: Vec<&String> = schema
.vertices
.keys()
.filter(|id| !schema.edges.keys().any(|e| &e.tgt == *id))
.collect();
roots.sort();
if let Some(root) = roots.first() {
return Ok((*root).clone());
}
let mut all_keys: Vec<&String> = schema.vertices.keys().collect();
all_keys.sort();
all_keys
.first()
.map(|k| (*k).clone())
.ok_or_else(|| LensError::VertexNotFound("root".to_string()))
}
#[cfg(test)]
pub(crate) fn rebuild_indices_pub(schema: &mut Schema) {
rebuild_indices(schema);
}
fn rebuild_indices(schema: &mut Schema) {
let mut outgoing: HashMap<String, SmallVec<Edge, 4>> = HashMap::new();
let mut incoming: HashMap<String, SmallVec<Edge, 4>> = HashMap::new();
let mut between: HashMap<(String, String), SmallVec<Edge, 2>> = HashMap::new();
for edge in schema.edges.keys() {
outgoing
.entry(edge.src.clone())
.or_default()
.push(edge.clone());
incoming
.entry(edge.tgt.clone())
.or_default()
.push(edge.clone());
between
.entry((edge.src.clone(), edge.tgt.clone()))
.or_default()
.push(edge.clone());
}
schema.outgoing = outgoing;
schema.incoming = incoming;
schema.between = between;
}
fn identity_compiled(schema: &Schema) -> CompiledMigration {
let surviving_verts = schema.vertices.keys().cloned().collect();
let surviving_edges = schema.edges.keys().cloned().collect();
CompiledMigration {
surviving_verts,
surviving_edges,
vertex_remap: HashMap::new(),
edge_remap: HashMap::new(),
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
}
}
fn build_compiled_migration(
src: &Schema,
tgt: &Schema,
combinators: &[Combinator],
) -> CompiledMigration {
let mut surviving_verts = std::collections::HashSet::new();
let mut surviving_edges = std::collections::HashSet::new();
let mut vertex_remap = HashMap::new();
let mut edge_remap = HashMap::new();
for src_id in src.vertices.keys() {
if tgt.vertices.contains_key(src_id) {
surviving_verts.insert(src_id.clone());
}
}
for src_edge in src.edges.keys() {
if tgt.edges.contains_key(src_edge) {
surviving_edges.insert(src_edge.clone());
}
}
for combinator in combinators {
match combinator {
Combinator::RenameField { old, new } => {
for src_edge in src.edges.keys() {
if src_edge.name.as_deref() == Some(old.as_str()) {
let mut new_edge = src_edge.clone();
new_edge.name = Some(new.clone());
edge_remap.insert(src_edge.clone(), new_edge);
surviving_edges.insert(src_edge.clone());
}
}
}
Combinator::RemoveField { name } => {
for src_edge in src.edges.keys() {
if src_edge.name.as_deref() == Some(name.as_str()) {
surviving_verts.remove(&src_edge.tgt);
surviving_edges.remove(src_edge);
}
}
}
Combinator::AddField { .. } | Combinator::CoerceType { .. } => {
}
Combinator::WrapInObject { field_name } => {
if let Ok(root_id) = find_root_vertex(src) {
for src_edge in src.edges.keys() {
if src_edge.src == root_id
&& src_edge.name.as_deref() != Some(field_name.as_str())
{
surviving_edges.remove(src_edge);
vertex_remap.insert(src_edge.tgt.clone(), src_edge.tgt.clone());
}
}
}
}
Combinator::HoistField { host, field } => {
for src_edge in src.edges.keys() {
if src_edge.src == *host && src_edge.name.as_deref() == Some(field.as_str()) {
surviving_edges.remove(src_edge);
if let Some(parent_edge) = src.edges.keys().find(|e| e.tgt == *host) {
let new_edge = Edge {
src: parent_edge.src.clone(),
tgt: src_edge.tgt.clone(),
kind: src_edge.kind.clone(),
name: src_edge.name.clone(),
};
edge_remap.insert(src_edge.clone(), new_edge);
}
}
}
}
Combinator::Compose(first, second) => {
let intermediate = apply_combinator(src, first).unwrap_or_else(|_| src.clone());
let m1 = build_compiled_migration(src, &intermediate, std::slice::from_ref(first));
let m2 = build_compiled_migration(&intermediate, tgt, std::slice::from_ref(second));
let composed = crate::compose::compose_compiled_migrations(&m1, &m2);
surviving_verts = composed.surviving_verts;
surviving_edges = composed.surviving_edges;
vertex_remap = composed.vertex_remap;
edge_remap = composed.edge_remap;
}
}
}
let mut resolver = HashMap::new();
for edge in tgt.edges.keys() {
if surviving_verts.contains(&edge.src) || vertex_remap.values().any(|v| v == &edge.src) {
let src_key = vertex_remap.get(&edge.src).unwrap_or(&edge.src).clone();
let tgt_key = vertex_remap.get(&edge.tgt).unwrap_or(&edge.tgt).clone();
resolver.insert((src_key, tgt_key), edge.clone());
}
}
CompiledMigration {
surviving_verts,
surviving_edges,
vertex_remap,
edge_remap,
resolver,
hyper_resolver: HashMap::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::three_node_schema;
#[test]
fn rename_field_updates_edge_label() {
let schema = three_node_schema();
let result = apply_rename(&schema, "text", "content");
assert!(result.is_ok(), "rename should succeed");
let new_schema = result.unwrap_or_else(|e| panic!("rename failed: {e}"));
let has_old = new_schema
.edges
.keys()
.any(|e| e.name.as_deref() == Some("text"));
let has_new = new_schema
.edges
.keys()
.any(|e| e.name.as_deref() == Some("content"));
assert!(!has_old, "old name should be gone");
assert!(has_new, "new name should be present");
}
#[test]
fn add_field_creates_vertex_and_edge() {
let schema = three_node_schema();
let result = apply_add_field(&schema, "likes", "integer");
assert!(result.is_ok(), "add_field should succeed");
let new_schema = result.unwrap_or_else(|e| panic!("add_field failed: {e}"));
assert!(
new_schema.vertices.values().any(|v| v.id.contains("likes")),
"new vertex should exist"
);
assert!(
new_schema
.edges
.keys()
.any(|e| e.name.as_deref() == Some("likes")),
"new edge should exist"
);
}
#[test]
fn remove_field_drops_vertex_and_edge() {
let schema = three_node_schema();
let result = apply_remove_field(&schema, "text");
assert!(result.is_ok(), "remove_field should succeed");
let new_schema = result.unwrap_or_else(|e| panic!("remove_field failed: {e}"));
assert_eq!(
new_schema.vertex_count(),
schema.vertex_count() - 1,
"one vertex should be removed"
);
}
#[test]
fn rename_nonexistent_field_fails() {
let schema = three_node_schema();
let result = apply_rename(&schema, "nonexistent", "new_name");
assert!(result.is_err(), "renaming nonexistent field should fail");
}
#[test]
fn coerce_type_changes_vertex_kind() {
let schema = three_node_schema();
let result = apply_coerce_type(&schema, "string", "text");
assert!(result.is_ok(), "coerce should succeed");
let new_schema = result.unwrap_or_else(|e| panic!("coerce failed: {e}"));
assert!(
new_schema.vertices.values().all(|v| v.kind != "string"),
"no vertices should have old kind"
);
}
}