use panproto_gat::Name;
use panproto_inst::value::Value;
use panproto_schema::{Protocol, Schema};
use serde::{Deserialize, Serialize};
use crate::protolens::{ComplementConstructor, Protolens, ProtolensChain};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ComplementSpec {
pub kind: ComplementKind,
pub forward_defaults: Vec<DefaultRequirement>,
pub captured_data: Vec<CapturedField>,
pub summary: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComplementKind {
Empty,
DataCaptured,
DefaultsRequired,
Mixed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DefaultRequirement {
pub element_name: Name,
pub element_kind: String,
pub description: String,
pub suggested_default: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapturedField {
pub element_name: Name,
pub element_kind: String,
pub description: String,
}
#[must_use]
pub fn complement_spec_at(protolens: &Protolens, schema: &Schema) -> ComplementSpec {
spec_from_constructor(&protolens.complement_constructor, schema)
}
#[must_use]
pub fn chain_complement_spec(
chain: &ProtolensChain,
schema: &Schema,
protocol: &Protocol,
) -> ComplementSpec {
if chain.steps.is_empty() {
return ComplementSpec {
kind: ComplementKind::Empty,
forward_defaults: vec![],
captured_data: vec![],
summary: "Identity transformation, no complement needed.".into(),
};
}
let mut all_defaults = Vec::new();
let mut all_captured = Vec::new();
let mut current_schema = schema.clone();
for step in &chain.steps {
let spec = complement_spec_at(step, ¤t_schema);
all_defaults.extend(spec.forward_defaults);
all_captured.extend(spec.captured_data);
if let Ok(next) = step.target_schema(¤t_schema, protocol) {
current_schema = next;
}
}
let kind = classify(&all_defaults, &all_captured);
let summary = build_summary(&kind, &all_defaults, &all_captured);
ComplementSpec {
kind,
forward_defaults: all_defaults,
captured_data: all_captured,
summary,
}
}
fn spec_from_constructor(constructor: &ComplementConstructor, schema: &Schema) -> ComplementSpec {
match constructor {
ComplementConstructor::Empty => ComplementSpec {
kind: ComplementKind::Empty,
forward_defaults: vec![],
captured_data: vec![],
summary: "Lossless transformation.".into(),
},
ComplementConstructor::DroppedSortData { sort } => {
let count = schema.vertices.values().filter(|v| v.kind == *sort).count();
ComplementSpec {
kind: ComplementKind::DataCaptured,
forward_defaults: vec![],
captured_data: vec![CapturedField {
element_name: sort.clone(),
element_kind: "sort".into(),
description: format!(
"Data for {count} vertices of kind '{sort}' will be captured in the complement."
),
}],
summary: format!("Drops sort '{sort}': {count} vertices captured in complement."),
}
}
ComplementConstructor::DroppedOpData { op } => {
let count = schema.edges.keys().filter(|e| e.kind == *op).count();
ComplementSpec {
kind: ComplementKind::DataCaptured,
forward_defaults: vec![],
captured_data: vec![CapturedField {
element_name: op.clone(),
element_kind: "op".into(),
description: format!(
"{count} edges of kind '{op}' will be captured in the complement.",
),
}],
summary: format!("Drops operation '{op}': {count} edges captured."),
}
}
ComplementConstructor::DroppedEdge {
src,
tgt,
edge_name,
..
} => dropped_edge_spec(src, tgt, edge_name.as_ref()),
ComplementConstructor::AddedElement {
element_name,
element_kind,
default_value,
} => added_element_spec(element_name, element_kind, default_value.as_ref()),
ComplementConstructor::NatTransKernel { nat_trans_name } => ComplementSpec {
kind: ComplementKind::DataCaptured,
forward_defaults: vec![],
captured_data: vec![CapturedField {
element_name: nat_trans_name.clone(),
element_kind: "nat_trans".into(),
description: format!(
"Kernel of natural transformation '{nat_trans_name}' captured in complement.",
),
}],
summary: format!("Value conversion via '{nat_trans_name}': kernel captured."),
},
ComplementConstructor::CoercedSortData { sort, class } => {
coerced_sort_spec(sort, *class, schema)
}
ComplementConstructor::Composite(parts) => {
let mut all_defaults = Vec::new();
let mut all_captured = Vec::new();
for part in parts {
let sub = spec_from_constructor(part, schema);
all_defaults.extend(sub.forward_defaults);
all_captured.extend(sub.captured_data);
}
let kind = classify(&all_defaults, &all_captured);
let summary = build_summary(&kind, &all_defaults, &all_captured);
ComplementSpec {
kind,
forward_defaults: all_defaults,
captured_data: all_captured,
summary,
}
}
ComplementConstructor::Scoped { focus, inner } => {
let inner_spec = spec_from_constructor(inner, schema);
let kind = inner_spec.kind;
ComplementSpec {
kind,
forward_defaults: inner_spec.forward_defaults,
captured_data: inner_spec.captured_data,
summary: format!("Scoped at '{focus}': {}", inner_spec.summary),
}
}
ComplementConstructor::Enrichment { kind, enricher } => {
enrichment_spec(*kind, enricher, schema)
}
}
}
fn enrichment_spec(
kind: panproto_gat::EnrichmentKind,
enricher: &std::sync::Arc<str>,
schema: &Schema,
) -> ComplementSpec {
let count = schema
.constraints
.values()
.filter(|cs| cs.iter().any(|c| kind.is_member_sort(c.sort.as_ref())))
.count();
ComplementSpec {
kind: ComplementKind::DataCaptured,
forward_defaults: vec![],
captured_data: vec![CapturedField {
element_name: Name::from(format!("enrichment/{kind:?}/{enricher}")),
element_kind: "enrichment".into(),
description: format!(
"{count} vertices carry constraints in the {kind:?} \
enrichment fibre; the registered driver \
'{enricher}' is responsible for materialising \
them in the put direction."
),
}],
summary: format!(
"{kind:?} enrichment via driver '{enricher}'; \
per-vertex fibre handled by the driver, not the \
WInstance complement."
),
}
}
fn added_element_spec(
element_name: &Name,
element_kind: &str,
default_value: Option<&panproto_inst::value::Value>,
) -> ComplementSpec {
ComplementSpec {
kind: ComplementKind::DefaultsRequired,
forward_defaults: vec![DefaultRequirement {
element_name: element_name.clone(),
element_kind: element_kind.to_string(),
description: format!("Default value needed for added {element_kind} '{element_name}'."),
suggested_default: default_value.cloned(),
}],
captured_data: vec![],
summary: format!("Adds {element_kind} '{element_name}': default required."),
}
}
fn dropped_edge_spec(src: &Name, tgt: &Name, edge_name: Option<&Name>) -> ComplementSpec {
let label = edge_name.map_or_else(|| "unnamed".to_string(), ToString::to_string);
ComplementSpec {
kind: ComplementKind::DataCaptured,
forward_defaults: vec![],
captured_data: vec![CapturedField {
element_name: Name::from(format!("{src}--{label}-->{tgt}")),
element_kind: "edge".into(),
description: format!(
"Single edge '{src} --({label})--> {tgt}' captured in complement."
),
}],
summary: format!("Drops edge '{src} --({label})--> {tgt}': captured in complement."),
}
}
fn coerced_sort_spec(
sort: &Name,
class: panproto_gat::CoercionClass,
schema: &Schema,
) -> ComplementSpec {
let count = schema.vertices.values().filter(|v| v.kind == *sort).count();
let (kind, desc) = match class {
panproto_gat::CoercionClass::Iso => (
ComplementKind::Empty,
format!("Isomorphic coercion on sort '{sort}' ({count} vertices)."),
),
panproto_gat::CoercionClass::Retraction => (
ComplementKind::DataCaptured,
format!("Retraction coercion on sort '{sort}' ({count} vertices): residual captured."),
),
panproto_gat::CoercionClass::Projection => (
ComplementKind::Empty,
format!(
"Projection coercion on sort '{sort}' ({count} vertices): \
derived values re-computed by get, no complement storage needed."
),
),
panproto_gat::CoercionClass::Opaque | _ => (
ComplementKind::DataCaptured,
format!(
"Opaque coercion on sort '{sort}' ({count} vertices): original values captured."
),
),
};
ComplementSpec {
kind,
forward_defaults: vec![],
captured_data: if class.needs_complement_storage() {
vec![CapturedField {
element_name: sort.clone(),
element_kind: "coerced_sort".into(),
description: desc.clone(),
}]
} else {
vec![]
},
summary: desc,
}
}
const fn classify(defaults: &[DefaultRequirement], captured: &[CapturedField]) -> ComplementKind {
match (defaults.is_empty(), captured.is_empty()) {
(true, true) => ComplementKind::Empty,
(false, true) => ComplementKind::DefaultsRequired,
(true, false) => ComplementKind::DataCaptured,
(false, false) => ComplementKind::Mixed,
}
}
fn build_summary(
kind: &ComplementKind,
defaults: &[DefaultRequirement],
captured: &[CapturedField],
) -> String {
match kind {
ComplementKind::Empty => "Lossless transformation, no complement needed.".into(),
ComplementKind::DefaultsRequired => format!(
"{} default(s) required: {}",
defaults.len(),
defaults
.iter()
.map(|d| d.element_name.to_string())
.collect::<Vec<_>>()
.join(", ")
),
ComplementKind::DataCaptured => format!(
"{} field(s) captured in complement: {}",
captured.len(),
captured
.iter()
.map(|c| c.element_name.to_string())
.collect::<Vec<_>>()
.join(", ")
),
ComplementKind::Mixed => format!(
"{} default(s) required, {} field(s) captured in complement.",
defaults.len(),
captured.len()
),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::protolens::elementary;
use crate::tests::three_node_schema;
use panproto_inst::value::Value;
fn test_protocol() -> Protocol {
Protocol {
name: "test".into(),
schema_theory: "ThGraph".into(),
instance_theory: "ThWType".into(),
edge_rules: vec![],
obj_kinds: vec!["object".into(), "string".into(), "array".into()],
constraint_sorts: vec![],
..Protocol::default()
}
}
#[test]
fn complement_spec_wire_format_matches_ts_sdk() {
use serde_json::{Value as JsonValue, json};
let spec = ComplementSpec {
kind: ComplementKind::DataCaptured,
forward_defaults: vec![DefaultRequirement {
element_name: Name::from("field_a"),
element_kind: "sort".to_owned(),
description: "needs a default".to_owned(),
suggested_default: None,
}],
captured_data: vec![CapturedField {
element_name: Name::from("field_b"),
element_kind: "op".to_owned(),
description: "captured".to_owned(),
}],
summary: "mixed".to_owned(),
};
let value: JsonValue = serde_json::to_value(&spec).unwrap();
assert_eq!(
value,
json!({
"kind": "data_captured",
"forwardDefaults": [{
"elementName": "field_a",
"elementKind": "sort",
"description": "needs a default",
"suggestedDefault": null,
}],
"capturedData": [{
"elementName": "field_b",
"elementKind": "op",
"description": "captured",
}],
"summary": "mixed",
}),
"ComplementSpec wire format must match the TS SDK"
);
for (variant, wire) in [
(ComplementKind::Empty, "empty"),
(ComplementKind::DataCaptured, "data_captured"),
(ComplementKind::DefaultsRequired, "defaults_required"),
(ComplementKind::Mixed, "mixed"),
] {
assert_eq!(
serde_json::to_value(&variant).unwrap(),
JsonValue::String(wire.to_owned()),
"ComplementKind::{variant:?} must serialize as {wire:?}"
);
}
}
#[test]
fn rename_sort_has_empty_complement() {
let schema = three_node_schema();
let p = elementary::rename_sort("string", "text");
let spec = complement_spec_at(&p, &schema);
assert_eq!(spec.kind, ComplementKind::Empty);
assert!(spec.forward_defaults.is_empty());
assert!(spec.captured_data.is_empty());
}
#[test]
fn drop_sort_captures_data() {
let schema = three_node_schema();
let p = elementary::drop_sort("string");
let spec = complement_spec_at(&p, &schema);
assert_eq!(spec.kind, ComplementKind::DataCaptured);
assert!(spec.captured_data.len() == 1);
assert_eq!(&*spec.captured_data[0].element_name, "string");
}
#[test]
fn add_sort_has_defaults_required_complement() {
let schema = three_node_schema();
let p = elementary::add_sort("tags", "array", Value::Null);
let spec = complement_spec_at(&p, &schema);
assert_eq!(spec.kind, ComplementKind::DefaultsRequired);
assert_eq!(spec.forward_defaults.len(), 1);
assert_eq!(&*spec.forward_defaults[0].element_name, "tags");
}
#[test]
fn drop_op_captures_data() {
let schema = three_node_schema();
let p = elementary::drop_op("prop");
let spec = complement_spec_at(&p, &schema);
assert_eq!(spec.kind, ComplementKind::DataCaptured);
assert!(spec.captured_data.len() == 1);
assert_eq!(&*spec.captured_data[0].element_name, "prop");
}
#[test]
fn empty_chain_is_empty() {
let schema = three_node_schema();
let protocol = test_protocol();
let chain = crate::protolens::ProtolensChain::new(vec![]);
let spec = chain_complement_spec(&chain, &schema, &protocol);
assert_eq!(spec.kind, ComplementKind::Empty);
}
#[test]
fn chain_with_drop_has_data_captured() {
let schema = three_node_schema();
let protocol = test_protocol();
let chain = crate::protolens::ProtolensChain::new(vec![elementary::drop_sort("string")]);
let spec = chain_complement_spec(&chain, &schema, &protocol);
assert_eq!(spec.kind, ComplementKind::DataCaptured);
}
#[test]
fn chain_mixed() {
let schema = three_node_schema();
let protocol = test_protocol();
let chain = crate::protolens::ProtolensChain::new(vec![
elementary::add_sort("tags", "array", Value::Null),
elementary::drop_sort("string"),
]);
let spec = chain_complement_spec(&chain, &schema, &protocol);
assert_eq!(spec.kind, ComplementKind::Mixed);
}
#[test]
fn summary_describes_complement() {
let schema = three_node_schema();
let p = elementary::drop_sort("string");
let spec = complement_spec_at(&p, &schema);
assert!(spec.summary.contains("string"));
}
}