1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 ArtifactFormat, BinocResult, DataAccess, Diagnostic, ExtractResult, GlobalClaim,
8 IdentityExtractor, IdentityFailurePolicy, IdentityToken, ItemRef, Segment, Summary,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[serde(rename_all = "snake_case")]
15pub enum TreeSide {
16 Left,
17 Right,
18}
19
20impl TreeSide {
21 pub fn label(self) -> &'static str {
22 match self {
23 TreeSide::Left => "left",
24 TreeSide::Right => "right",
25 }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32pub struct NodeId {
33 pub side: TreeSide,
34 pub index: u32,
35}
36
37#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40pub struct ProjectionHint {
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub action: Option<String>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub item_type: Option<String>,
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub tags: Vec<String>,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 pub retract_tags: Vec<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub summary: Option<Summary>,
58}
59
60pub fn projection_hint_is_default(hint: &ProjectionHint) -> bool {
61 hint == &ProjectionHint::default()
62}
63
64impl ProjectionHint {
65 pub fn action(mut self, action: impl Into<String>) -> Self {
66 self.action = Some(action.into());
67 self
68 }
69
70 pub fn item_type(mut self, item_type: impl Into<String>) -> Self {
71 self.item_type = Some(item_type.into());
72 self
73 }
74
75 pub fn tag(mut self, tag: impl Into<String>) -> Self {
76 self.tags.push(tag.into());
77 self
78 }
79
80 pub fn retract_tag(mut self, tag: impl Into<String>) -> Self {
84 self.retract_tags.push(tag.into());
85 self
86 }
87
88 pub fn summary(mut self, summary: impl Into<Summary>) -> Self {
89 self.summary = Some(summary.into());
90 self
91 }
92
93 pub fn merge_from(&mut self, other: &ProjectionHint) {
94 if self.action.is_none() {
95 self.action = other.action.clone();
96 }
97 if self.item_type.is_none() {
98 self.item_type = other.item_type.clone();
99 }
100 if self.summary.is_none() {
101 self.summary = other.summary.clone();
102 }
103 self.merge_tags(other);
104 }
105
106 fn merge_tags(&mut self, other: &ProjectionHint) {
110 self.tags.extend(other.tags.iter().cloned());
111 self.tags.sort();
112 self.tags.dedup();
113 self.retract_tags.extend(other.retract_tags.iter().cloned());
114 self.retract_tags.sort();
115 self.retract_tags.dedup();
116 if !self.retract_tags.is_empty() {
117 self.tags.retain(|tag| !self.retract_tags.contains(tag));
118 }
119 }
120
121 pub fn overlay_from(&mut self, other: &ProjectionHint) {
124 if other.action.is_some() {
125 self.action = other.action.clone();
126 }
127 if other.item_type.is_some() {
128 self.item_type = other.item_type.clone();
129 }
130 if other.summary.is_some() {
131 self.summary = other.summary.clone();
132 }
133 self.merge_tags(other);
134 }
135}
136
137pub struct ProjectionAnnotationContext<'a> {
138 pub action: &'a str,
139 pub item_type: &'a str,
140 pub path: &'a str,
141 pub source_path: Option<&'a str>,
142 pub source_item_type: Option<&'a str>,
149 pub evidence: Option<&'a str>,
150 pub edits: &'a [Edit],
151 pub container: bool,
152 pub unlinked_side: Option<TreeSide>,
153}
154
155pub trait ProjectionAnnotator: Send + Sync {
156 fn name(&self) -> &str;
157 fn annotate(&self, ctx: &ProjectionAnnotationContext<'_>) -> ProjectionHint;
158}
159
160#[derive(Clone)]
162pub enum CoreRule {
163 Expand(Arc<dyn ExpandRule>),
164 Parse(Arc<dyn ParseRule>),
165 Pair(Arc<dyn PairRule>),
166}
167
168impl CoreRule {
169 pub fn name(&self) -> String {
170 match self {
171 CoreRule::Expand(rule) => rule.descriptor().name,
172 CoreRule::Parse(rule) => rule.descriptor().name,
173 CoreRule::Pair(rule) => rule.descriptor().name,
174 }
175 }
176}
177
178#[derive(Default, Clone)]
184pub struct CorrespondenceEngineConfig {
185 pub rules: Vec<CoreRule>,
186 pub writers: Vec<Arc<dyn EditListWriter>>,
187 pub compaction: Vec<Arc<dyn CompactionRule>>,
188 pub annotators: Vec<Arc<dyn ProjectionAnnotator>>,
189 pub identity_extractors: Vec<Arc<dyn IdentityExtractor>>,
195 pub row_keys: BTreeMap<String, Vec<String>>,
196 pub row_identity_policies: BTreeMap<String, RowIdentityPolicies>,
197 pub root_projection: ProjectionHint,
198 pub dataset_configurator: Option<Arc<dyn CorrespondenceDatasetConfigurator>>,
199}
200
201pub trait CorrespondenceDatasetConfigurator: Send + Sync {
202 fn configure(
203 &self,
204 config: &mut CorrespondenceEngineConfig,
205 dataset: &serde_json::Value,
206 left_root: &ItemRef,
207 right_root: &ItemRef,
208 data: &dyn DataAccess,
209 ) -> BinocResult<Vec<Diagnostic>>;
210}
211
212#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
214#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
215pub struct NodeMatch {
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub is_dir: Option<bool>,
218 #[serde(default, skip_serializing_if = "Vec::is_empty")]
219 pub extensions: Vec<String>,
220 #[serde(default, skip_serializing_if = "Vec::is_empty")]
221 pub media_types: Vec<String>,
222}
223
224impl NodeMatch {
225 pub fn matches(&self, item: &ItemRef) -> bool {
226 if let Some(expected) = self.is_dir {
227 if item.is_dir != expected {
228 return false;
229 }
230 }
231 if !self.extensions.is_empty() {
232 let ext = item.extension();
233 if !ext
234 .as_ref()
235 .is_some_and(|ext| self.extensions.iter().any(|candidate| candidate == ext))
236 {
237 return false;
238 }
239 }
240 if !self.media_types.is_empty() {
241 let media_type = item.media_type.as_deref().unwrap_or("");
242 if !self
243 .media_types
244 .iter()
245 .any(|candidate| candidate == media_type)
246 {
247 return false;
248 }
249 }
250 true
251 }
252}
253
254#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
256#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
257#[serde(rename_all = "snake_case")]
258pub enum ShapeFilter {
259 #[default]
260 Any,
261 Container,
262 Leaf,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
267pub struct ExpandDescriptor {
268 pub name: String,
269 pub input: NodeMatch,
270 #[serde(default)]
271 pub fires_beneath_settled: bool,
272}
273
274pub trait ExpandRule: Send + Sync {
275 fn descriptor(&self) -> ExpandDescriptor;
276 fn expand(&self, item: &ItemRef, data: &dyn DataAccess) -> BinocResult<ExpandOutput>;
277}
278
279#[derive(Debug, Clone, Default, Serialize, Deserialize)]
280#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
281pub struct ExpandOutput {
282 pub children: Vec<ItemRef>,
283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
284 pub diagnostics: Vec<Diagnostic>,
285}
286
287impl From<Vec<ItemRef>> for ExpandOutput {
288 fn from(children: Vec<ItemRef>) -> Self {
289 Self {
290 children,
291 diagnostics: Vec::new(),
292 }
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
306pub struct MemberMatch {
307 #[serde(rename = "match")]
308 pub matcher: NodeMatch,
309 #[serde(default)]
310 pub required: bool,
311}
312
313impl MemberMatch {
314 pub fn required(matcher: NodeMatch) -> Self {
316 Self {
317 matcher,
318 required: true,
319 }
320 }
321
322 pub fn optional(matcher: NodeMatch) -> Self {
324 Self {
325 matcher,
326 required: false,
327 }
328 }
329}
330
331impl From<NodeMatch> for MemberMatch {
334 fn from(matcher: NodeMatch) -> Self {
335 MemberMatch::required(matcher)
336 }
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
351#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
352#[serde(rename_all = "snake_case")]
353pub enum Correlation {
354 #[default]
356 SharedStem,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
361pub struct ParseDescriptor {
362 pub name: String,
363 pub input: NodeMatch,
371 pub output: ArtifactFormat,
372 #[serde(default)]
373 pub fires_beneath_settled: bool,
374}
375
376#[derive(Debug, Clone)]
383pub struct ParseGroup {
384 pub anchor: ItemRef,
385 pub members: Vec<Option<ItemRef>>,
386}
387
388impl ParseGroup {
389 pub fn single(anchor: ItemRef) -> Self {
391 Self {
392 members: vec![Some(anchor.clone())],
393 anchor,
394 }
395 }
396
397 pub fn member(&self, index: usize) -> Option<&ItemRef> {
399 self.members.get(index).and_then(Option::as_ref)
400 }
401
402 pub fn present(&self) -> impl Iterator<Item = &ItemRef> {
404 self.members.iter().filter_map(Option::as_ref)
405 }
406}
407
408pub trait ParseRule: Send + Sync {
409 fn descriptor(&self) -> ParseDescriptor;
410
411 fn parse(&self, item: &ItemRef, data: &dyn DataAccess) -> BinocResult<ParseOutput>;
415
416 fn extra_members(&self) -> Vec<MemberMatch> {
422 Vec::new()
423 }
424
425 fn correlation(&self) -> Correlation {
428 Correlation::SharedStem
429 }
430
431 fn parse_group(&self, group: &ParseGroup, data: &dyn DataAccess) -> BinocResult<ParseOutput> {
438 self.parse(&group.anchor, data)
439 }
440}
441
442pub fn member_set(rule: &dyn ParseRule) -> Vec<MemberMatch> {
447 let mut members = vec![MemberMatch::required(rule.descriptor().input)];
448 members.extend(rule.extra_members());
449 members
450}
451
452pub fn parse_arity(rule: &dyn ParseRule) -> usize {
455 1 + rule.extra_members().len()
456}
457
458#[derive(Debug, Clone, Default, Serialize, Deserialize)]
459#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
460pub struct ParseOutput {
461 pub bytes: Vec<u8>,
462 #[serde(default, skip_serializing_if = "Vec::is_empty")]
463 pub diagnostics: Vec<Diagnostic>,
464 #[serde(default, skip_serializing_if = "Vec::is_empty")]
465 pub children: Vec<ParsedChild>,
466 #[serde(default, skip_serializing_if = "Vec::is_empty")]
473 pub artifacts: Vec<ParsedArtifact>,
474 #[serde(default, skip_serializing_if = "projection_hint_is_default")]
481 pub projection: ProjectionHint,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
485#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
486pub struct ParsedChild {
487 pub item: ItemRef,
488 #[serde(default, skip_serializing_if = "Vec::is_empty")]
489 pub artifacts: Vec<ParsedArtifact>,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize)]
493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
494pub struct ParsedArtifact {
495 pub format: ArtifactFormat,
496 pub bytes: Vec<u8>,
497}
498
499impl From<Vec<u8>> for ParseOutput {
500 fn from(bytes: Vec<u8>) -> Self {
501 Self {
502 bytes,
503 diagnostics: Vec::new(),
504 children: Vec::new(),
505 artifacts: Vec::new(),
506 projection: ProjectionHint::default(),
507 }
508 }
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
512#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
513pub struct PairDescriptor {
514 pub name: String,
515 #[serde(default)]
516 pub emits: Vec<String>,
517 #[serde(default)]
525 pub reads: Vec<ArtifactFormat>,
526 #[serde(default)]
527 pub sees_beneath_settled: bool,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
531#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
532pub struct LinkProposal {
533 pub left: u32,
534 pub right: u32,
535 pub evidence: String,
536 #[serde(default)]
537 pub settled: bool,
538 #[serde(default)]
539 pub projection: ProjectionHint,
540}
541
542pub trait PairRule: Send + Sync {
543 fn descriptor(&self) -> PairDescriptor;
544 fn propose(&self, view: &dyn EngineView, data: &dyn DataAccess) -> BinocResult<PairOutput>;
545 fn final_diagnostics(
546 &self,
547 _view: &dyn EngineView,
548 _data: &dyn DataAccess,
549 ) -> BinocResult<Vec<Diagnostic>> {
550 Ok(Vec::new())
551 }
552
553 fn final_claims(
560 &self,
561 _view: &dyn EngineView,
562 _data: &dyn DataAccess,
563 ) -> BinocResult<Vec<GlobalClaim>> {
564 Ok(Vec::new())
565 }
566}
567
568#[derive(Debug, Clone, Default, Serialize, Deserialize)]
569#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
570pub struct PairOutput {
571 pub proposals: Vec<LinkProposal>,
572 #[serde(default, skip_serializing_if = "Vec::is_empty")]
573 pub diagnostics: Vec<Diagnostic>,
574}
575
576impl From<Vec<LinkProposal>> for PairOutput {
577 fn from(proposals: Vec<LinkProposal>) -> Self {
578 Self {
579 proposals,
580 diagnostics: Vec::new(),
581 }
582 }
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize)]
586#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
587pub struct LinkRef {
588 pub index: usize,
589 pub left: NodeId,
590 pub right: NodeId,
591 pub evidence: String,
592 pub proposer: String,
593 pub priority: u32,
594 pub settled: bool,
595 #[serde(default)]
596 pub projection: ProjectionHint,
597}
598
599pub trait EngineView {
600 fn root(&self, side: TreeSide) -> NodeId;
601 fn visible(&self, id: NodeId) -> bool;
602 fn nodes(&self, side: TreeSide) -> Vec<NodeId>;
603 fn item(&self, id: NodeId) -> &ItemRef;
604 fn parent(&self, id: NodeId) -> Option<NodeId>;
605 fn children(&self, id: NodeId) -> Vec<NodeId>;
606 fn has_children(&self, id: NodeId) -> bool;
607 fn is_linked(&self, id: NodeId) -> bool;
608 fn links(&self) -> Vec<LinkRef>;
609 fn links_of(&self, id: NodeId) -> Vec<LinkRef>;
610 fn artifact_bytes(
611 &self,
612 id: NodeId,
613 format: &ArtifactFormat,
614 data: &dyn DataAccess,
615 ) -> BinocResult<Option<Vec<u8>>>;
616
617 fn identity_tokens(
627 &self,
628 _id: NodeId,
629 _data: &dyn DataAccess,
630 ) -> BinocResult<Option<Vec<IdentityToken>>> {
631 Ok(None)
632 }
633}
634
635#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
637#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
638pub struct Edit {
639 pub verb: String,
640 pub params: serde_json::Value,
641 #[serde(default)]
642 pub projection: EditProjection,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub provenance: Option<String>,
653}
654
655impl Edit {
656 pub fn new(verb: impl Into<String>, params: serde_json::Value) -> Self {
657 Self {
658 verb: verb.into(),
659 params,
660 projection: EditProjection::default(),
661 provenance: None,
662 }
663 }
664
665 pub fn with_provenance(mut self, provenance: impl Into<String>) -> Self {
668 self.provenance = Some(provenance.into());
669 self
670 }
671
672 pub fn hidden(mut self) -> Self {
673 self.projection.visible = false;
674 self
675 }
676
677 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
678 self.projection.hint.tags.push(tag.into());
679 self
680 }
681
682 pub fn with_item_type(mut self, item_type: impl Into<String>) -> Self {
683 self.projection.hint.item_type = Some(item_type.into());
684 self
685 }
686
687 pub fn with_summary(mut self, summary: impl Into<Summary>) -> Self {
688 self.projection.hint.summary = Some(summary.into());
689 self
690 }
691}
692
693#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
694#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
695pub struct EditProjection {
696 #[serde(default = "default_visible")]
697 pub visible: bool,
698 #[serde(default)]
699 pub hint: ProjectionHint,
700}
701
702impl Default for EditProjection {
703 fn default() -> Self {
704 Self {
705 visible: true,
706 hint: ProjectionHint::default(),
707 }
708 }
709}
710
711fn default_visible() -> bool {
712 true
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize)]
716#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
717pub struct WriterDescriptor {
718 pub name: String,
719 #[serde(default)]
720 pub formats: Vec<ArtifactFormat>,
721 pub input: NodeMatch,
722 #[serde(default)]
723 pub shape: ShapeFilter,
724 #[serde(default)]
728 pub fallback: bool,
729}
730
731pub struct LinkCtx<'a> {
732 pub view: &'a dyn EngineView,
733 pub link: LinkRef,
734 pub row_keys: &'a [String],
735 pub row_identity_policies: RowIdentityPolicies,
736}
737
738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
739pub struct RowIdentityPolicies {
740 pub on_null_key: IdentityFailurePolicy,
741 pub on_duplicate_key: IdentityFailurePolicy,
742}
743
744impl Default for RowIdentityPolicies {
745 fn default() -> Self {
746 Self {
747 on_null_key: IdentityFailurePolicy::Diagnostic,
748 on_duplicate_key: IdentityFailurePolicy::Diagnostic,
749 }
750 }
751}
752
753pub trait EditListWriter: Send + Sync {
754 fn descriptor(&self) -> WriterDescriptor;
755 fn write(&self, ctx: &LinkCtx<'_>, data: &dyn DataAccess) -> BinocResult<Option<WriteOutput>>;
756 fn extract(
757 &self,
758 _ctx: &LinkCtx<'_>,
759 _edits: &[Edit],
760 _aspect: &str,
761 _data: &dyn DataAccess,
762 ) -> BinocResult<Option<ExtractResult>> {
763 Ok(None)
764 }
765}
766
767#[derive(Debug, Clone, Default, Serialize, Deserialize)]
768#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
769pub struct WriteOutput {
770 pub edits: Vec<Edit>,
771 #[serde(default, skip_serializing_if = "Vec::is_empty")]
772 pub diagnostics: Vec<Diagnostic>,
773}
774
775impl From<Vec<Edit>> for WriteOutput {
776 fn from(edits: Vec<Edit>) -> Self {
777 Self {
778 edits,
779 diagnostics: Vec::new(),
780 }
781 }
782}
783
784pub trait CompactionRule: Send + Sync {
785 fn name(&self) -> &str;
786
787 fn format(&self) -> Option<ArtifactFormat> {
795 None
796 }
797
798 fn rewrite(
799 &self,
800 ctx: &LinkCtx<'_>,
801 edits: &[Edit],
802 data: &dyn DataAccess,
803 ) -> BinocResult<Option<Vec<Edit>>>;
804}
805
806pub fn edit_count_summary(edit_count: usize) -> Summary {
808 Summary(vec![
809 Segment::Uint(edit_count as u64),
810 Segment::Text(format!(" edit{}", if edit_count == 1 { "" } else { "s" })),
811 ])
812}
813
814#[cfg(test)]
815mod projection_hint_tests {
816 use super::*;
817
818 #[test]
819 fn overlay_retracts_a_superseded_tag() {
820 let mut acc = ProjectionHint::default()
824 .tag("binoc.move")
825 .tag("binoc.keep");
826 let reshape = ProjectionHint::default()
827 .tag("binoc.container-reshape")
828 .retract_tag("binoc.move");
829 acc.overlay_from(&reshape);
830 assert!(acc.tags.contains(&"binoc.container-reshape".to_string()));
831 assert!(acc.tags.contains(&"binoc.keep".to_string()));
832 assert!(!acc.tags.contains(&"binoc.move".to_string()));
833 }
834
835 #[test]
836 fn retraction_holds_regardless_of_union_order() {
837 let mut acc = ProjectionHint::default();
840 let hint = ProjectionHint::default()
841 .tag("binoc.move")
842 .retract_tag("binoc.move");
843 acc.merge_from(&hint);
844 assert!(!acc.tags.contains(&"binoc.move".to_string()));
845 }
846}