1#![allow(clippy::implicit_hasher)]
29
30pub mod asymmetric;
31pub mod auto_lens;
32pub mod candidate;
33pub mod coercion_laws;
34pub mod complement_type;
35pub mod compose;
36pub mod cost;
37pub mod diff_to_protolens;
38pub mod edit_error;
39pub mod edit_laws;
40pub mod edit_lens;
41pub mod edit_pipeline;
42pub mod edit_provenance;
43pub mod enrichment_registry;
44pub mod error;
45pub mod fibration;
46pub mod graph;
47pub mod hint;
48pub mod laws;
49pub mod layout_complement;
50pub mod optic;
51pub mod protolens;
52pub mod symbolic;
53pub mod symmetric;
54
55pub use asymmetric::{Complement, get, put};
57pub use auto_lens::{
58 AutoLensConfig, AutoLensResult, Stringency, auto_generate, auto_generate_candidates,
59 auto_generate_candidates_with_hints, auto_generate_with_hints,
60};
61pub use candidate::{CandidateStep, LensCandidate};
62pub use complement_type::{
63 CapturedField, ComplementKind, ComplementSpec, DefaultRequirement, chain_complement_spec,
64 complement_spec_at,
65};
66pub use compose::compose;
67pub use cost::{chain_cost, complement_cost, verify_identity_cost, verify_subadditivity};
68pub use diff_to_protolens::{DiffSpec, KindChange, diff_to_lens, diff_to_protolens};
69pub use edit_error::EditLensError;
70pub use edit_lens::EditLens;
71pub use edit_pipeline::EditPipeline;
72pub use edit_provenance::EditProvenance;
73pub use error::{LawViolation, LensError};
74pub use graph::LensGraph;
75pub use laws::{check_get_put, check_laws, check_put_get};
76pub use optic::{OpticKind, classify_transform, refine_scoped_optic};
77pub use protolens::{
78 ComplementConstructor, FleetResult, Protolens, ProtolensChain, SchemaConstraint,
79 apply_to_fleet, combinators, elementary, horizontal_compose as protolens_horizontal,
80 lift_chain, lift_protolens, vertical_compose as protolens_vertical,
81};
82pub use symbolic::{SymbolicStep, simplify_steps};
83pub use symmetric::{CoherenceViolation, SymmetricLens};
84
85use panproto_inst::CompiledMigration;
86use panproto_schema::Schema;
87
88#[derive(Debug)]
95pub struct Lens {
96 pub compiled: CompiledMigration,
98 pub src_schema: Schema,
100 pub tgt_schema: Schema,
102}
103
104impl Lens {
105 #[must_use]
109 pub fn coercion_class(&self) -> panproto_gat::CoercionClass {
110 self.compiled.coercion_class()
111 }
112}
113
114#[cfg(test)]
115pub(crate) mod tests {
116 use std::collections::HashMap;
117
118 use panproto_gat::Name;
119 use panproto_inst::value::{FieldPresence, Value};
120 use panproto_inst::{CompiledMigration, Node, WInstance};
121 use panproto_schema::{Edge, Schema, Vertex};
122 use smallvec::SmallVec;
123
124 use crate::Lens;
125
126 pub fn three_node_schema() -> Schema {
129 let mut vertices = HashMap::new();
130 vertices.insert(
131 Name::from("post:body"),
132 Vertex {
133 id: "post:body".into(),
134 kind: "object".into(),
135 nsid: None,
136 },
137 );
138 vertices.insert(
139 Name::from("post:body.text"),
140 Vertex {
141 id: "post:body.text".into(),
142 kind: "string".into(),
143 nsid: None,
144 },
145 );
146 vertices.insert(
147 Name::from("post:body.createdAt"),
148 Vertex {
149 id: "post:body.createdAt".into(),
150 kind: "string".into(),
151 nsid: None,
152 },
153 );
154
155 let edge_text = Edge {
156 src: "post:body".into(),
157 tgt: "post:body.text".into(),
158 kind: "prop".into(),
159 name: Some("text".into()),
160 };
161 let edge_created = Edge {
162 src: "post:body".into(),
163 tgt: "post:body.createdAt".into(),
164 kind: "prop".into(),
165 name: Some("createdAt".into()),
166 };
167
168 let mut edges = HashMap::new();
169 edges.insert(edge_text.clone(), Name::from("prop"));
170 edges.insert(edge_created.clone(), Name::from("prop"));
171
172 let mut outgoing: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
173 outgoing
174 .entry("post:body".into())
175 .or_default()
176 .push(edge_text.clone());
177 outgoing
178 .entry("post:body".into())
179 .or_default()
180 .push(edge_created.clone());
181
182 let mut incoming: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
183 incoming
184 .entry("post:body.text".into())
185 .or_default()
186 .push(edge_text.clone());
187 incoming
188 .entry("post:body.createdAt".into())
189 .or_default()
190 .push(edge_created.clone());
191
192 let mut between: HashMap<(Name, Name), SmallVec<Edge, 2>> = HashMap::new();
193 between
194 .entry((Name::from("post:body"), Name::from("post:body.text")))
195 .or_default()
196 .push(edge_text);
197 between
198 .entry((Name::from("post:body"), Name::from("post:body.createdAt")))
199 .or_default()
200 .push(edge_created);
201
202 Schema {
203 protocol: "test".into(),
204 vertices,
205 edges,
206 hyper_edges: HashMap::new(),
207 constraints: HashMap::new(),
208 required: HashMap::new(),
209 nsids: HashMap::new(),
210 entries: Vec::new(),
211 variants: HashMap::new(),
212 orderings: HashMap::new(),
213 recursion_points: HashMap::new(),
214 spans: HashMap::new(),
215 usage_modes: HashMap::new(),
216 nominal: HashMap::new(),
217 coercions: HashMap::new(),
218 mergers: HashMap::new(),
219 defaults: HashMap::new(),
220 policies: HashMap::new(),
221 outgoing,
222 incoming,
223 between,
224 }
225 }
226
227 pub fn three_node_instance() -> WInstance {
229 let mut nodes = HashMap::new();
230 nodes.insert(0, Node::new(0, "post:body"));
231 nodes.insert(
232 1,
233 Node::new(1, "post:body.text")
234 .with_value(FieldPresence::Present(Value::Str("hello".into()))),
235 );
236 nodes.insert(
237 2,
238 Node::new(2, "post:body.createdAt")
239 .with_value(FieldPresence::Present(Value::Str("2024-01-01".into()))),
240 );
241
242 let arcs = vec![
243 (
244 0,
245 1,
246 Edge {
247 src: "post:body".into(),
248 tgt: "post:body.text".into(),
249 kind: "prop".into(),
250 name: Some("text".into()),
251 },
252 ),
253 (
254 0,
255 2,
256 Edge {
257 src: "post:body".into(),
258 tgt: "post:body.createdAt".into(),
259 kind: "prop".into(),
260 name: Some("createdAt".into()),
261 },
262 ),
263 ];
264
265 WInstance::new(nodes, arcs, vec![], 0, Name::from("post:body"))
266 }
267
268 pub fn identity_lens(schema: &Schema) -> Lens {
270 let surviving_verts = schema.vertices.keys().cloned().collect();
271 let surviving_edges = schema.edges.keys().cloned().collect();
272
273 let compiled = CompiledMigration {
274 surviving_verts,
275 surviving_edges,
276 vertex_remap: HashMap::new(),
277 edge_remap: HashMap::new(),
278 resolver: HashMap::new(),
279 hyper_resolver: HashMap::new(),
280 field_transforms: HashMap::new(),
281 conditional_survival: HashMap::new(),
282 expansion_path: HashMap::new(),
283 };
284
285 Lens {
286 compiled,
287 src_schema: schema.clone(),
288 tgt_schema: schema.clone(),
289 }
290 }
291
292 pub fn projection_lens(schema: &Schema, field_to_remove: &str) -> Lens {
294 let mut tgt_schema = schema.clone();
295
296 let edges_to_remove: Vec<Edge> = tgt_schema
298 .edges
299 .keys()
300 .filter(|e| e.name.as_deref() == Some(field_to_remove))
301 .cloned()
302 .collect();
303
304 let mut removed_vertices = Vec::new();
305 for edge in &edges_to_remove {
306 tgt_schema.edges.remove(edge);
307 tgt_schema.vertices.remove(&edge.tgt);
308 removed_vertices.push(edge.tgt.clone());
309 }
310
311 crate::protolens::rebuild_indices(&mut tgt_schema);
313
314 let mut surviving_verts: std::collections::HashSet<Name> =
315 schema.vertices.keys().cloned().collect();
316 let mut surviving_edges: std::collections::HashSet<Edge> =
317 schema.edges.keys().cloned().collect();
318
319 for v in &removed_vertices {
320 surviving_verts.remove(v);
321 }
322 for e in &edges_to_remove {
323 surviving_edges.remove(e);
324 }
325
326 let compiled = CompiledMigration {
327 surviving_verts,
328 surviving_edges,
329 vertex_remap: HashMap::new(),
330 edge_remap: HashMap::new(),
331 resolver: HashMap::new(),
332 hyper_resolver: HashMap::new(),
333 field_transforms: HashMap::new(),
334 conditional_survival: HashMap::new(),
335 expansion_path: HashMap::new(),
336 };
337
338 Lens {
339 compiled,
340 src_schema: schema.clone(),
341 tgt_schema,
342 }
343 }
344
345 #[test]
353 fn round_trip_get_then_put_recovers_original() {
354 let schema = three_node_schema();
355 let lens = identity_lens(&schema);
356 let instance = three_node_instance();
357
358 let (view, complement) =
359 crate::get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
360 let restored =
361 crate::put(&lens, &view, &complement).unwrap_or_else(|e| panic!("put failed: {e}"));
362
363 assert_eq!(restored.node_count(), instance.node_count());
364 assert_eq!(restored.root, instance.root);
365 assert_eq!(restored.schema_root, instance.schema_root);
366
367 for (&id, node) in &instance.nodes {
369 let restored_node = restored
370 .nodes
371 .get(&id)
372 .unwrap_or_else(|| panic!("node {id} missing from restored instance"));
373 assert_eq!(
374 node.anchor, restored_node.anchor,
375 "anchor mismatch for node {id}"
376 );
377 }
378 }
379
380 #[test]
381 fn modified_view_propagates_changes() {
382 let schema = three_node_schema();
383 let lens = identity_lens(&schema);
384 let instance = three_node_instance();
385
386 let (mut view, complement) =
388 crate::get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
389
390 if let Some(node) = view.nodes.get_mut(&1) {
392 node.value = Some(FieldPresence::Present(Value::Str("modified".into())));
393 }
394
395 let restored =
397 crate::put(&lens, &view, &complement).unwrap_or_else(|e| panic!("put failed: {e}"));
398
399 let node = restored
401 .nodes
402 .get(&1)
403 .unwrap_or_else(|| panic!("node 1 missing"));
404 assert_eq!(
405 node.value,
406 Some(FieldPresence::Present(Value::Str("modified".into()))),
407 "modification should be preserved"
408 );
409 }
410
411 #[test]
412 fn projection_lens_drops_field() {
413 let schema = three_node_schema();
414 let lens = projection_lens(&schema, "createdAt");
415 let instance = three_node_instance();
416
417 let (view, complement) =
418 crate::get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
419
420 assert_eq!(view.node_count(), 2, "projection should drop one node");
421 assert!(
422 !complement.dropped_nodes.is_empty(),
423 "complement should have dropped node"
424 );
425 }
426
427 #[test]
428 fn projection_get_then_put_restores_with_complement() {
429 let schema = three_node_schema();
430 let lens = projection_lens(&schema, "createdAt");
431 let instance = three_node_instance();
432
433 let (view, complement) =
434 crate::get(&lens, &instance).unwrap_or_else(|e| panic!("get failed: {e}"));
435
436 let restored =
437 crate::put(&lens, &view, &complement).unwrap_or_else(|e| panic!("put failed: {e}"));
438
439 assert_eq!(
440 restored.node_count(),
441 instance.node_count(),
442 "restoration should bring back all nodes"
443 );
444 }
445
446 #[test]
447 fn compose_rename_then_identity_preserves_laws() {
448 let schema = three_node_schema();
449 let l1 = identity_lens(&schema);
450 let l2 = identity_lens(&schema);
451
452 let composed = crate::compose(&l1, &l2).unwrap_or_else(|e| panic!("compose failed: {e}"));
453 let instance = three_node_instance();
454
455 let result = crate::check_laws(&composed, &instance);
456 assert!(
457 result.is_ok(),
458 "composed identity lenses should satisfy laws: {result:?}"
459 );
460 }
461}