1use std::any::{Any, TypeId};
2use std::borrow::Cow;
3use std::collections::{BTreeMap, HashMap};
4use std::sync::{Arc, Mutex};
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 Annotation, ArtifactFormat, BinocError, BinocResult, DataAccess, Diagnostic, ExtractResult,
10 GlobalClaim, IdentityExtractor, IdentityFailurePolicy, IdentityToken, ItemRef, NodeIdentity,
11 RowIdentity, Segment, Summary,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17#[serde(rename_all = "snake_case")]
18pub enum TreeSide {
19 Left,
20 Right,
21}
22
23impl TreeSide {
24 pub fn label(self) -> &'static str {
25 match self {
26 TreeSide::Left => "left",
27 TreeSide::Right => "right",
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35pub struct NodeId {
36 pub side: TreeSide,
37 pub index: u32,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41struct ArtifactDecodeCacheKey {
42 id: NodeId,
43 format: ArtifactFormat,
44 type_id: TypeId,
45}
46
47#[derive(Clone)]
48enum CachedDecode {
49 Missing,
50 Present(Arc<dyn Any + Send + Sync>),
51}
52
53#[derive(Default)]
58pub struct ArtifactDecodeCache {
59 entries: Mutex<HashMap<ArtifactDecodeCacheKey, CachedDecode>>,
60}
61
62impl ArtifactDecodeCache {
63 pub fn get_or_try_insert_with<T>(
64 &self,
65 id: NodeId,
66 format: &ArtifactFormat,
67 load: impl FnOnce() -> BinocResult<Option<T>>,
68 ) -> BinocResult<Option<Arc<T>>>
69 where
70 T: Any + Send + Sync,
71 {
72 let key = ArtifactDecodeCacheKey {
73 id,
74 format: format.clone(),
75 type_id: TypeId::of::<T>(),
76 };
77 if let Some(cached) = self.lookup::<T>(&key)? {
78 return Ok(cached);
79 }
80
81 let loaded = match load()? {
82 Some(value) => CachedDecode::Present(Arc::new(value)),
83 None => CachedDecode::Missing,
84 };
85
86 let cached = {
87 let mut entries = self.entries.lock().map_err(cache_poisoned)?;
88 entries.entry(key).or_insert(loaded).clone()
89 };
90 decode_cached::<T>(cached)
91 }
92
93 fn lookup<T>(&self, key: &ArtifactDecodeCacheKey) -> BinocResult<Option<Option<Arc<T>>>>
94 where
95 T: Any + Send + Sync,
96 {
97 let cached = self
98 .entries
99 .lock()
100 .map_err(cache_poisoned)?
101 .get(key)
102 .cloned();
103 cached.map(decode_cached::<T>).transpose()
104 }
105}
106
107fn decode_cached<T>(cached: CachedDecode) -> BinocResult<Option<Arc<T>>>
108where
109 T: Any + Send + Sync,
110{
111 match cached {
112 CachedDecode::Missing => Ok(None),
113 CachedDecode::Present(value) => value
114 .downcast::<T>()
115 .map(Some)
116 .map_err(|_| BinocError::Other("artifact decode cache type mismatch".into())),
117 }
118}
119
120fn cache_poisoned<T>(_err: std::sync::PoisonError<T>) -> BinocError {
121 BinocError::Other("artifact decode cache lock poisoned".into())
122}
123
124#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
126#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
127pub struct ProjectionHint {
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub action: Option<String>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub item_type: Option<String>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub tags: Vec<String>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub retract_tags: Vec<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub summary: Option<Summary>,
145 #[serde(default, skip_serializing_if = "Vec::is_empty")]
146 pub annotations: Vec<Annotation>,
147}
148
149pub fn projection_hint_is_default(hint: &ProjectionHint) -> bool {
150 hint == &ProjectionHint::default()
151}
152
153impl ProjectionHint {
154 pub fn action(mut self, action: impl Into<String>) -> Self {
155 self.action = Some(action.into());
156 self
157 }
158
159 pub fn item_type(mut self, item_type: impl Into<String>) -> Self {
160 self.item_type = Some(item_type.into());
161 self
162 }
163
164 pub fn tag(mut self, tag: impl Into<String>) -> Self {
165 self.tags.push(tag.into());
166 self
167 }
168
169 pub fn retract_tag(mut self, tag: impl Into<String>) -> Self {
173 self.retract_tags.push(tag.into());
174 self
175 }
176
177 pub fn summary(mut self, summary: impl Into<Summary>) -> Self {
178 self.summary = Some(summary.into());
179 self
180 }
181
182 pub fn annotate(
183 mut self,
184 package: impl Into<String>,
185 key: impl Into<String>,
186 value: serde_json::Value,
187 ) -> Self {
188 upsert_annotation(&mut self.annotations, package.into(), key.into(), value);
189 self
190 }
191
192 pub fn merge_from(&mut self, other: &ProjectionHint) {
193 if self.action.is_none() {
194 self.action = other.action.clone();
195 }
196 if self.item_type.is_none() {
197 self.item_type = other.item_type.clone();
198 }
199 if self.summary.is_none() {
200 self.summary = other.summary.clone();
201 }
202 self.merge_tags(other);
203 merge_annotations_if_missing(&mut self.annotations, &other.annotations);
204 }
205
206 fn merge_tags(&mut self, other: &ProjectionHint) {
210 self.tags.extend(other.tags.iter().cloned());
211 self.tags.sort();
212 self.tags.dedup();
213 self.retract_tags.extend(other.retract_tags.iter().cloned());
214 self.retract_tags.sort();
215 self.retract_tags.dedup();
216 if !self.retract_tags.is_empty() {
217 self.tags.retain(|tag| !self.retract_tags.contains(tag));
218 }
219 }
220
221 pub fn overlay_from(&mut self, other: &ProjectionHint) {
224 if other.action.is_some() {
225 self.action = other.action.clone();
226 }
227 if other.item_type.is_some() {
228 self.item_type = other.item_type.clone();
229 }
230 if other.summary.is_some() {
231 self.summary = other.summary.clone();
232 }
233 self.merge_tags(other);
234 overlay_annotations(&mut self.annotations, &other.annotations);
235 }
236}
237
238fn merge_annotations_if_missing(target: &mut Vec<Annotation>, source: &[Annotation]) {
239 for annotation in source {
240 if !target.iter().any(|existing| {
241 existing.package == annotation.package && existing.key == annotation.key
242 }) {
243 target.push(annotation.clone());
244 }
245 }
246}
247
248fn overlay_annotations(target: &mut Vec<Annotation>, source: &[Annotation]) {
249 for annotation in source {
250 upsert_annotation(
251 target,
252 annotation.package.clone(),
253 annotation.key.clone(),
254 annotation.value.clone(),
255 );
256 }
257}
258
259fn upsert_annotation(
260 annotations: &mut Vec<Annotation>,
261 package: String,
262 key: String,
263 value: serde_json::Value,
264) {
265 if let Some(existing) = annotations
266 .iter_mut()
267 .find(|annotation| annotation.package == package && annotation.key == key)
268 {
269 existing.value = value;
270 } else {
271 annotations.push(Annotation::new(package, key, value));
272 }
273}
274
275pub struct ProjectionAnnotationContext<'a> {
276 pub action: &'a str,
277 pub item_type: &'a str,
278 pub path: &'a str,
279 pub source_path: Option<&'a str>,
280 pub source_item_type: Option<&'a str>,
287 pub evidence: Option<&'a str>,
288 pub edits: &'a [Edit],
289 pub container: bool,
290 pub unlinked_side: Option<TreeSide>,
291}
292
293pub trait ProjectionAnnotator: Send + Sync {
294 fn name(&self) -> &str;
295 fn annotate(&self, ctx: &ProjectionAnnotationContext<'_>) -> ProjectionHint;
296}
297
298#[derive(Clone)]
300pub enum CoreRule {
301 Expand(Arc<dyn ExpandRule>),
302 Parse(Arc<dyn ParseRule>),
303 Pair(Arc<dyn PairRule>),
304}
305
306impl CoreRule {
307 pub fn name(&self) -> String {
308 match self {
309 CoreRule::Expand(rule) => rule.descriptor().name,
310 CoreRule::Parse(rule) => rule.descriptor().name,
311 CoreRule::Pair(rule) => rule.descriptor().name,
312 }
313 }
314}
315
316#[derive(Default, Clone)]
322pub struct CorrespondenceEngineConfig {
323 pub rules: Vec<CoreRule>,
324 pub writers: Vec<Arc<dyn EditListWriter>>,
325 pub compaction: Vec<Arc<dyn CompactionRule>>,
326 pub annotators: Vec<Arc<dyn ProjectionAnnotator>>,
327 pub identity_extractors: Vec<Arc<dyn IdentityExtractor>>,
333 pub row_keys: BTreeMap<String, Vec<String>>,
334 pub row_identity_policies: BTreeMap<String, RowIdentityPolicies>,
335 pub node_identities: BTreeMap<String, NodeIdentity>,
336 pub root_projection: ProjectionHint,
337 pub dataset_configurator: Option<Arc<dyn CorrespondenceDatasetConfigurator>>,
338 pub dispatch_resolver: Option<Arc<dyn DispatchResolver>>,
343}
344
345pub trait CorrespondenceDatasetConfigurator: Send + Sync {
346 fn configure(
347 &self,
348 config: &mut CorrespondenceEngineConfig,
349 dataset: &serde_json::Value,
350 left_root: &ItemRef,
351 right_root: &ItemRef,
352 data: &dyn DataAccess,
353 ) -> BinocResult<Vec<Diagnostic>>;
354}
355
356pub trait DispatchResolver: Send + Sync {
357 fn configure_item(&self, item: &mut ItemRef) -> BinocResult<Vec<Diagnostic>>;
358
359 fn forced_rule_for(&self, _item: &ItemRef) -> Option<String> {
360 None
361 }
362
363 fn row_identity_for(&self, _path: &str) -> Option<RowIdentity> {
364 None
365 }
366
367 fn node_identity_for(&self, _path: &str) -> Option<NodeIdentity> {
368 None
369 }
370}
371
372#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
374#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
375pub struct NodeMatch {
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub is_dir: Option<bool>,
378 #[serde(default, skip_serializing_if = "Vec::is_empty")]
379 pub extensions: Vec<String>,
380 #[serde(default, skip_serializing_if = "Vec::is_empty")]
381 pub media_types: Vec<String>,
382}
383
384impl NodeMatch {
385 pub fn matches(&self, item: &ItemRef) -> bool {
386 if let Some(expected) = self.is_dir {
387 if item.is_dir != expected {
388 return false;
389 }
390 }
391 if !self.extensions.is_empty() {
392 let ext = item.extension();
393 if !ext
394 .as_ref()
395 .is_some_and(|ext| self.extensions.iter().any(|candidate| candidate == ext))
396 {
397 return false;
398 }
399 }
400 if !self.media_types.is_empty() {
401 let media_type = item.media_type.as_deref().unwrap_or("");
402 if !self
403 .media_types
404 .iter()
405 .any(|candidate| candidate == media_type)
406 {
407 return false;
408 }
409 }
410 true
411 }
412}
413
414#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
416#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
417#[serde(rename_all = "snake_case")]
418pub enum ShapeFilter {
419 #[default]
420 Any,
421 Container,
422 Leaf,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
426#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
427pub struct ExpandDescriptor {
428 pub name: String,
429 pub input: NodeMatch,
430 #[serde(default)]
431 pub fires_beneath_settled: bool,
432}
433
434pub trait ExpandRule: Send + Sync {
435 fn descriptor(&self) -> ExpandDescriptor;
436 fn expand(&self, item: &ItemRef, data: &dyn DataAccess) -> BinocResult<ExpandOutput>;
437}
438
439#[derive(Debug, Clone, Default, Serialize, Deserialize)]
440#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
441pub struct ExpandOutput {
442 pub children: Vec<ItemRef>,
443 #[serde(default, skip_serializing_if = "Vec::is_empty")]
444 pub diagnostics: Vec<Diagnostic>,
445}
446
447impl From<Vec<ItemRef>> for ExpandOutput {
448 fn from(children: Vec<ItemRef>) -> Self {
449 Self {
450 children,
451 diagnostics: Vec::new(),
452 }
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
466pub struct MemberMatch {
467 #[serde(rename = "match")]
468 pub matcher: NodeMatch,
469 #[serde(default)]
470 pub required: bool,
471}
472
473impl MemberMatch {
474 pub fn required(matcher: NodeMatch) -> Self {
476 Self {
477 matcher,
478 required: true,
479 }
480 }
481
482 pub fn optional(matcher: NodeMatch) -> Self {
484 Self {
485 matcher,
486 required: false,
487 }
488 }
489}
490
491impl From<NodeMatch> for MemberMatch {
494 fn from(matcher: NodeMatch) -> Self {
495 MemberMatch::required(matcher)
496 }
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
511#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
512#[serde(rename_all = "snake_case")]
513pub enum Correlation {
514 #[default]
516 SharedStem,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
520#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
521pub struct ParseDescriptor {
522 pub name: String,
523 pub input: NodeMatch,
531 pub output: ArtifactFormat,
532 #[serde(default)]
533 pub fires_beneath_settled: bool,
534}
535
536#[derive(Debug, Clone)]
543pub struct ParseGroup {
544 pub anchor: ItemRef,
545 pub members: Vec<Option<ItemRef>>,
546}
547
548impl ParseGroup {
549 pub fn single(anchor: ItemRef) -> Self {
551 Self {
552 members: vec![Some(anchor.clone())],
553 anchor,
554 }
555 }
556
557 pub fn member(&self, index: usize) -> Option<&ItemRef> {
559 self.members.get(index).and_then(Option::as_ref)
560 }
561
562 pub fn present(&self) -> impl Iterator<Item = &ItemRef> {
564 self.members.iter().filter_map(Option::as_ref)
565 }
566}
567
568pub trait ParseRule: Send + Sync {
569 fn descriptor(&self) -> ParseDescriptor;
570
571 fn parse(&self, item: &ItemRef, data: &dyn DataAccess) -> BinocResult<ParseOutput>;
575
576 fn extra_members(&self) -> Vec<MemberMatch> {
582 Vec::new()
583 }
584
585 fn correlation(&self) -> Correlation {
588 Correlation::SharedStem
589 }
590
591 fn parse_group(&self, group: &ParseGroup, data: &dyn DataAccess) -> BinocResult<ParseOutput> {
598 self.parse(&group.anchor, data)
599 }
600}
601
602pub fn member_set(rule: &dyn ParseRule) -> Vec<MemberMatch> {
607 let mut members = vec![MemberMatch::required(rule.descriptor().input)];
608 members.extend(rule.extra_members());
609 members
610}
611
612pub fn parse_arity(rule: &dyn ParseRule) -> usize {
615 1 + rule.extra_members().len()
616}
617
618#[derive(Debug, Clone, Default, Serialize, Deserialize)]
619#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
620pub struct ParseOutput {
621 pub bytes: Vec<u8>,
622 #[serde(default, skip_serializing_if = "Vec::is_empty")]
623 pub diagnostics: Vec<Diagnostic>,
624 #[serde(default, skip_serializing_if = "Vec::is_empty")]
625 pub children: Vec<ParsedChild>,
626 #[serde(default, skip_serializing_if = "Vec::is_empty")]
633 pub artifacts: Vec<ParsedArtifact>,
634 #[serde(default, skip_serializing_if = "projection_hint_is_default")]
641 pub projection: ProjectionHint,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
645#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
646pub struct ParsedChild {
647 pub item: ItemRef,
648 #[serde(default, skip_serializing_if = "Vec::is_empty")]
649 pub artifacts: Vec<ParsedArtifact>,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize)]
653#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
654pub struct ParsedArtifact {
655 pub format: ArtifactFormat,
656 pub bytes: Vec<u8>,
657}
658
659impl From<Vec<u8>> for ParseOutput {
660 fn from(bytes: Vec<u8>) -> Self {
661 Self {
662 bytes,
663 diagnostics: Vec::new(),
664 children: Vec::new(),
665 artifacts: Vec::new(),
666 projection: ProjectionHint::default(),
667 }
668 }
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize)]
672#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
673pub struct PairDescriptor {
674 pub name: String,
675 #[serde(default)]
676 pub emits: Vec<String>,
677 #[serde(default)]
685 pub reads: Vec<ArtifactFormat>,
686 #[serde(default)]
687 pub sees_beneath_settled: bool,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize)]
691#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
692pub struct LinkProposal {
693 pub left: u32,
694 pub right: u32,
695 pub evidence: String,
696 #[serde(default)]
697 pub settled: bool,
698 #[serde(default)]
699 pub projection: ProjectionHint,
700}
701
702pub trait PairRule: Send + Sync {
703 fn descriptor(&self) -> PairDescriptor;
704 fn propose(&self, view: &dyn EngineView, data: &dyn DataAccess) -> BinocResult<PairOutput>;
705 fn final_diagnostics(
706 &self,
707 _view: &dyn EngineView,
708 _data: &dyn DataAccess,
709 ) -> BinocResult<Vec<Diagnostic>> {
710 Ok(Vec::new())
711 }
712
713 fn final_claims(
720 &self,
721 _view: &dyn EngineView,
722 _data: &dyn DataAccess,
723 ) -> BinocResult<Vec<GlobalClaim>> {
724 Ok(Vec::new())
725 }
726}
727
728#[derive(Debug, Clone, Default, Serialize, Deserialize)]
729#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
730pub struct PairOutput {
731 pub proposals: Vec<LinkProposal>,
732 #[serde(default, skip_serializing_if = "Vec::is_empty")]
733 pub diagnostics: Vec<Diagnostic>,
734}
735
736impl From<Vec<LinkProposal>> for PairOutput {
737 fn from(proposals: Vec<LinkProposal>) -> Self {
738 Self {
739 proposals,
740 diagnostics: Vec::new(),
741 }
742 }
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize)]
746#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
747pub struct LinkRef {
748 pub index: usize,
749 pub left: NodeId,
750 pub right: NodeId,
751 pub evidence: String,
752 pub proposer: String,
753 pub priority: u32,
754 pub settled: bool,
755 #[serde(default)]
756 pub projection: ProjectionHint,
757}
758
759pub trait EngineView {
760 fn root(&self, side: TreeSide) -> NodeId;
761 fn visible(&self, id: NodeId) -> bool;
762 fn nodes(&self, side: TreeSide) -> Vec<NodeId>;
763 fn item(&self, id: NodeId) -> &ItemRef;
764 fn parent(&self, id: NodeId) -> Option<NodeId>;
765 fn children(&self, id: NodeId) -> Vec<NodeId>;
766 fn has_children(&self, id: NodeId) -> bool;
767 fn is_linked(&self, id: NodeId) -> bool;
768 fn links(&self) -> Vec<LinkRef>;
769 fn links_of(&self, id: NodeId) -> Vec<LinkRef>;
770 fn artifact_bytes(
771 &self,
772 id: NodeId,
773 format: &ArtifactFormat,
774 data: &dyn DataAccess,
775 ) -> BinocResult<Option<Vec<u8>>>;
776
777 fn identity_tokens(
787 &self,
788 _id: NodeId,
789 _data: &dyn DataAccess,
790 ) -> BinocResult<Option<Vec<IdentityToken>>> {
791 Ok(None)
792 }
793}
794
795#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
797#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
798pub struct Edit {
799 pub verb: String,
800 pub params: serde_json::Value,
801 #[serde(default)]
802 pub projection: EditProjection,
803 #[serde(default, skip_serializing_if = "Option::is_none")]
812 pub provenance: Option<String>,
813}
814
815impl Edit {
816 pub fn new(verb: impl Into<String>, params: serde_json::Value) -> Self {
817 Self {
818 verb: verb.into(),
819 params,
820 projection: EditProjection::default(),
821 provenance: None,
822 }
823 }
824
825 pub fn with_provenance(mut self, provenance: impl Into<String>) -> Self {
828 self.provenance = Some(provenance.into());
829 self
830 }
831
832 pub fn hidden(mut self) -> Self {
833 self.projection.visible = false;
834 self
835 }
836
837 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
838 self.projection.hint.tags.push(tag.into());
839 self
840 }
841
842 pub fn with_item_type(mut self, item_type: impl Into<String>) -> Self {
843 self.projection.hint.item_type = Some(item_type.into());
844 self
845 }
846
847 pub fn with_summary(mut self, summary: impl Into<Summary>) -> Self {
848 self.projection.hint.summary = Some(summary.into());
849 self
850 }
851}
852
853#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
854#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
855pub struct EditProjection {
856 #[serde(default = "default_visible")]
857 pub visible: bool,
858 #[serde(default)]
859 pub hint: ProjectionHint,
860}
861
862impl Default for EditProjection {
863 fn default() -> Self {
864 Self {
865 visible: true,
866 hint: ProjectionHint::default(),
867 }
868 }
869}
870
871fn default_visible() -> bool {
872 true
873}
874
875#[derive(Debug, Clone, Serialize, Deserialize)]
876#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
877pub struct WriterDescriptor {
878 pub name: String,
879 #[serde(default)]
880 pub formats: Vec<ArtifactFormat>,
881 pub input: NodeMatch,
882 #[serde(default)]
883 pub shape: ShapeFilter,
884 #[serde(default)]
888 pub fallback: bool,
889}
890
891pub struct LinkCtx<'a> {
892 pub view: &'a dyn EngineView,
893 pub link: LinkRef,
894 pub row_keys: Cow<'a, [String]>,
895 pub row_identity_policies: RowIdentityPolicies,
896 pub node_identity: Option<Cow<'a, NodeIdentity>>,
897 pub artifact_cache: &'a ArtifactDecodeCache,
898}
899
900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub struct RowIdentityPolicies {
902 pub on_null_key: IdentityFailurePolicy,
903 pub on_duplicate_key: IdentityFailurePolicy,
904}
905
906impl Default for RowIdentityPolicies {
907 fn default() -> Self {
908 Self {
909 on_null_key: IdentityFailurePolicy::Diagnostic,
910 on_duplicate_key: IdentityFailurePolicy::Diagnostic,
911 }
912 }
913}
914
915pub trait EditListWriter: Send + Sync {
916 fn descriptor(&self) -> WriterDescriptor;
917 fn write(&self, ctx: &LinkCtx<'_>, data: &dyn DataAccess) -> BinocResult<Option<WriteOutput>>;
918 fn extract(
919 &self,
920 _ctx: &LinkCtx<'_>,
921 _edits: &[Edit],
922 _aspect: &str,
923 _data: &dyn DataAccess,
924 ) -> BinocResult<Option<ExtractResult>> {
925 Ok(None)
926 }
927}
928
929#[derive(Debug, Clone, Default, Serialize, Deserialize)]
930#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
931pub struct WriteOutput {
932 pub edits: Vec<Edit>,
933 #[serde(default, skip_serializing_if = "Vec::is_empty")]
934 pub diagnostics: Vec<Diagnostic>,
935}
936
937impl From<Vec<Edit>> for WriteOutput {
938 fn from(edits: Vec<Edit>) -> Self {
939 Self {
940 edits,
941 diagnostics: Vec::new(),
942 }
943 }
944}
945
946pub trait CompactionRule: Send + Sync {
947 fn name(&self) -> &str;
948
949 fn format(&self) -> Option<ArtifactFormat> {
957 None
958 }
959
960 fn rewrite(
961 &self,
962 ctx: &LinkCtx<'_>,
963 edits: &[Edit],
964 data: &dyn DataAccess,
965 ) -> BinocResult<Option<Vec<Edit>>>;
966}
967
968pub fn edit_count_summary(edit_count: usize) -> Summary {
970 Summary(vec![
971 Segment::Uint(edit_count as u64),
972 Segment::Text(format!(" edit{}", if edit_count == 1 { "" } else { "s" })),
973 ])
974}
975
976#[cfg(test)]
977mod projection_hint_tests {
978 use super::*;
979
980 #[test]
981 fn overlay_retracts_a_superseded_tag() {
982 let mut acc = ProjectionHint::default()
986 .tag("binoc.move")
987 .tag("binoc.keep");
988 let reshape = ProjectionHint::default()
989 .tag("binoc.container-reshape")
990 .retract_tag("binoc.move");
991 acc.overlay_from(&reshape);
992 assert!(acc.tags.contains(&"binoc.container-reshape".to_string()));
993 assert!(acc.tags.contains(&"binoc.keep".to_string()));
994 assert!(!acc.tags.contains(&"binoc.move".to_string()));
995 }
996
997 #[test]
998 fn retraction_holds_regardless_of_union_order() {
999 let mut acc = ProjectionHint::default();
1002 let hint = ProjectionHint::default()
1003 .tag("binoc.move")
1004 .retract_tag("binoc.move");
1005 acc.merge_from(&hint);
1006 assert!(!acc.tags.contains(&"binoc.move".to_string()));
1007 }
1008
1009 #[test]
1010 fn overlay_replaces_annotation_value_by_key() {
1011 let mut acc = ProjectionHint::default().annotate(
1012 "binoc",
1013 "content_type_inference",
1014 serde_json::json!("left"),
1015 );
1016 let overlay = ProjectionHint::default().annotate(
1017 "binoc",
1018 "content_type_inference",
1019 serde_json::json!("right"),
1020 );
1021 acc.overlay_from(&overlay);
1022 assert_eq!(
1023 acc.annotations,
1024 vec![Annotation::new(
1025 "binoc",
1026 "content_type_inference",
1027 serde_json::json!("right")
1028 )]
1029 );
1030 }
1031}