pattern_core/lib.rs
1//! pattern-core - Core pattern data structures
2//!
3//! This crate provides the core pattern data structures for the pattern-rs library.
4//! It is a faithful port of the gram-hs reference implementation.
5//!
6//! # Overview
7//!
8//! The `pattern-core` crate defines these main types:
9//!
10//! - **[`StandardGraph`]**: An ergonomic, zero-configuration graph
11//! for building and querying graph structures. This is the recommended starting point
12//! for most users.
13//!
14//! - **[`Subject`]**: A self-descriptive value type with identity, labels,
15//! and properties. Use [`Subject::build`] for fluent construction.
16//!
17//! - **[`Pattern<V>`](pattern::Pattern)**: A recursive, nested structure (s-expression-like)
18//! that is generic over value type `V`. This is the foundational data structure for
19//! representing nested, hierarchical data that may be interpreted as graphs.
20//!
21//! # Quick Start
22//!
23//! Build a graph with [`StandardGraph`]:
24//!
25//! ```rust
26//! use pattern_core::graph::StandardGraph;
27//! use pattern_core::subject::Subject;
28//!
29//! let mut g = StandardGraph::new();
30//!
31//! // Add nodes using the fluent SubjectBuilder
32//! let alice = Subject::build("alice").label("Person").property("name", "Alice").done();
33//! let bob = Subject::build("bob").label("Person").property("name", "Bob").done();
34//! g.add_node(alice.clone());
35//! g.add_node(bob.clone());
36//!
37//! // Add a relationship — pass the Subject objects directly
38//! g.add_relationship(Subject::build("r1").label("KNOWS").done(), &alice, &bob);
39//!
40//! assert_eq!(g.node_count(), 2);
41//! assert_eq!(g.relationship_count(), 1);
42//!
43//! // Query the graph
44//! let source = g.source(&"r1".into()).unwrap();
45//! assert_eq!(source.value.identity.0, "alice");
46//! let neighbors = g.neighbors(&"bob".into());
47//! assert_eq!(neighbors.len(), 1);
48//! ```
49//!
50//! # Low-Level Pattern Construction
51//!
52//! For direct pattern manipulation without the graph layer:
53//!
54//! ```rust
55//! use pattern_core::{Pattern, Subject, Symbol, Value};
56//! use std::collections::{HashSet, HashMap};
57//!
58//! // Create an atomic pattern (special case)
59//! let atomic = Pattern::point("hello".to_string());
60//!
61//! // Create a pattern with elements (primary constructor)
62//! let pattern = Pattern::pattern("parent".to_string(), vec![
63//! Pattern::point("child1".to_string()),
64//! Pattern::point("child2".to_string()),
65//! ]);
66//!
67//! // Access pattern components
68//! assert_eq!(atomic.value(), "hello");
69//! assert_eq!(pattern.length(), 2);
70//! assert_eq!(pattern.depth(), 1);
71//!
72//! // Transform pattern values (Functor)
73//! let upper = pattern.clone().map(|s| s.to_uppercase());
74//! assert_eq!(upper.value(), "PARENT");
75//!
76//! // Validate pattern structure
77//! use pattern_core::ValidationRules;
78//! let rules = ValidationRules {
79//! max_depth: Some(10),
80//! ..Default::default()
81//! };
82//! assert!(pattern.validate(&rules).is_ok());
83//!
84//! // Analyze pattern structure
85//! let analysis = pattern.analyze_structure();
86//! println!("Structure: {}", analysis.summary);
87//!
88//! // Create a pattern with Subject value
89//! let subject = Subject {
90//! identity: Symbol("n".to_string()),
91//! labels: {
92//! let mut s = HashSet::new();
93//! s.insert("Person".to_string());
94//! s
95//! },
96//! properties: {
97//! let mut m = HashMap::new();
98//! m.insert("name".to_string(), Value::VString("Alice".to_string()));
99//! m
100//! },
101//! };
102//!
103//! let pattern_with_subject: Pattern<Subject> = Pattern::point(subject);
104//! ```
105//!
106//! # Pattern Combination
107//!
108//! Patterns can be combined associatively using the `combine()` method when the value type
109//! implements the `Combinable` trait. Combination merges two patterns by combining their values
110//! and concatenating their elements.
111//!
112//! ```rust
113//! use pattern_core::{Pattern, Combinable};
114//!
115//! // Combine atomic patterns (no elements)
116//! let p1 = Pattern::point("hello".to_string());
117//! let p2 = Pattern::point(" world".to_string());
118//! let combined = p1.combine(p2);
119//! assert_eq!(combined.value(), "hello world");
120//! assert_eq!(combined.length(), 0);
121//!
122//! // Combine patterns with elements
123//! let p3 = Pattern::pattern("a".to_string(), vec![
124//! Pattern::point("b".to_string()),
125//! Pattern::point("c".to_string()),
126//! ]);
127//! let p4 = Pattern::pattern("d".to_string(), vec![
128//! Pattern::point("e".to_string()),
129//! ]);
130//! let result = p3.combine(p4);
131//! assert_eq!(result.value(), "ad");
132//! assert_eq!(result.length(), 3); // [b, c, e]
133//!
134//! // Associativity: (a ⊕ b) ⊕ c = a ⊕ (b ⊕ c)
135//! let a = Pattern::point("a".to_string());
136//! let b = Pattern::point("b".to_string());
137//! let c = Pattern::point("c".to_string());
138//! let left = a.clone().combine(b.clone()).combine(c.clone());
139//! let right = a.combine(b.combine(c));
140//! assert_eq!(left, right);
141//! ```
142//!
143//! # Pattern Ordering
144//!
145//! Patterns implement `Ord` and `PartialOrd` for types that support ordering,
146//! enabling sorting, comparison, and use in ordered data structures.
147//!
148//! ```rust
149//! use pattern_core::Pattern;
150//! use std::collections::{BTreeSet, BTreeMap};
151//!
152//! // Compare patterns
153//! let p1 = Pattern::point(1);
154//! let p2 = Pattern::point(2);
155//! assert!(p1 < p2);
156//!
157//! // Value-first ordering: values compared before elements
158//! let p3 = Pattern::pattern(3, vec![Pattern::point(100)]);
159//! let p4 = Pattern::pattern(4, vec![Pattern::point(1)]);
160//! assert!(p3 < p4); // 3 < 4, elements not compared
161//!
162//! // Sort patterns
163//! let mut patterns = vec![
164//! Pattern::point(5),
165//! Pattern::point(2),
166//! Pattern::point(8),
167//! ];
168//! patterns.sort();
169//! assert_eq!(patterns[0], Pattern::point(2));
170//!
171//! // Find min/max
172//! let min = patterns.iter().min().unwrap();
173//! let max = patterns.iter().max().unwrap();
174//! assert_eq!(min, &Pattern::point(2));
175//! assert_eq!(max, &Pattern::point(8));
176//!
177//! // Use in BTreeSet (maintains sorted order)
178//! let mut set = BTreeSet::new();
179//! set.insert(Pattern::point(5));
180//! set.insert(Pattern::point(2));
181//! set.insert(Pattern::point(8));
182//! let sorted: Vec<_> = set.iter().map(|p| p.value).collect();
183//! assert_eq!(sorted, vec![2, 5, 8]);
184//!
185//! // Use as BTreeMap keys
186//! let mut map = BTreeMap::new();
187//! map.insert(Pattern::point(1), "first");
188//! map.insert(Pattern::point(2), "second");
189//! assert_eq!(map.get(&Pattern::point(1)), Some(&"first"));
190//! ```
191//!
192//! # WASM Compatibility
193//!
194//! All types in this crate are fully compatible with WebAssembly targets. Compile for WASM with:
195//!
196//! ```bash
197//! cargo build --package pattern-core --target wasm32-unknown-unknown
198//! ```
199//!
200//! # Reference Implementation
201//!
202//! This crate is ported from the gram-hs reference implementation:
203//! - Pattern: `../pattern-hs/libs/pattern/src/Pattern.hs`
204//! - Subject: `../pattern-hs/libs/subject/src/Subject/Core.hs`
205//! - Feature Spec: `../pattern-hs/specs/001-pattern-data-structure/`
206
207pub mod graph;
208pub mod pattern;
209pub mod pattern_graph;
210pub mod reconcile;
211pub mod subject;
212pub mod test_utils;
213
214#[cfg(feature = "python")]
215pub mod python;
216
217#[cfg(feature = "wasm")]
218pub mod wasm;
219
220pub use graph::{
221 all_paths, betweenness_centrality, bfs, canonical_classifier, classify_by_shape,
222 connected_components, degree_centrality, dfs, directed, directed_reverse, filter_graph,
223 fold_graph, frame_query, from_graph_lens, from_pattern_graph, from_test_node, has_cycle,
224 has_path, is_connected, is_neighbor, map_all_graph, map_graph, map_with_context, materialize,
225 memoize_incident_rels, minimum_spanning_tree, para_graph, para_graph_fixed,
226 query_annotations_of, query_co_members, query_walks_containing, shortest_path,
227 topological_sort, undirected, unfold_graph, CategoryMappers, GraphClass, GraphClassifier,
228 GraphQuery, GraphValue, GraphView, StandardGraph, Substitution, TraversalDirection,
229 TraversalWeight,
230};
231pub use pattern::{unfold, Pattern, StructureAnalysis, ValidationError, ValidationRules};
232pub use pattern_graph::{
233 from_pattern_graph as graph_query_from_pattern_graph, from_patterns, from_patterns_with_policy,
234 merge as pg_merge, merge_with_policy as pg_merge_with_policy, PatternGraph,
235};
236pub use reconcile::{
237 ElementMergeStrategy, HasIdentity, LabelMerge, Mergeable, PropertyMerge, ReconciliationPolicy,
238 Refinable, SubjectMergeStrategy,
239};
240pub use subject::{PropertyRecord, RangeValue, Subject, SubjectBuilder, Symbol, Value};
241
242// Re-export comonad operations for convenient access
243// These are defined in pattern::comonad and pattern::comonad_helpers modules
244// All operations are methods on Pattern<V>, so no additional re-exports needed beyond Pattern itself
245
246// ============================================================================
247// Combinable Trait
248// ============================================================================
249
250/// Types that support associative combination.
251///
252/// Implementors must ensure that combination is associative:
253/// `(a.combine(b)).combine(c)` must equal `a.combine(b.combine(c))` for all values.
254///
255/// This trait is used to enable pattern combination for `Pattern<V>` where `V: Combinable`.
256///
257/// # Laws
258///
259/// **Associativity**: For all values a, b, c of type Self:
260/// ```text
261/// (a.combine(b)).combine(c) == a.combine(b.combine(c))
262/// ```
263///
264/// # Examples
265///
266/// ```rust
267/// use pattern_core::Combinable;
268///
269/// let s1 = String::from("hello");
270/// let s2 = String::from(" world");
271/// let result = s1.combine(s2);
272/// assert_eq!(result, "hello world");
273/// ```
274pub trait Combinable {
275 /// Combines two values associatively.
276 ///
277 /// # Parameters
278 ///
279 /// * `self` - The first value (consumed)
280 /// * `other` - The second value to combine with (consumed)
281 ///
282 /// # Returns
283 ///
284 /// A new value representing the combination of `self` and `other`.
285 ///
286 /// # Laws
287 ///
288 /// Must be associative: `(a.combine(b)).combine(c) == a.combine(b.combine(c))`
289 fn combine(self, other: Self) -> Self;
290}
291
292// ============================================================================
293// Standard Implementations
294// ============================================================================
295
296/// Combines two strings by concatenation.
297///
298/// String concatenation is associative: `(a + b) + c = a + (b + c)`
299///
300/// # Examples
301///
302/// ```rust
303/// use pattern_core::Combinable;
304///
305/// let s1 = String::from("hello");
306/// let s2 = String::from(" world");
307/// let result = s1.combine(s2);
308/// assert_eq!(result, "hello world");
309/// ```
310impl Combinable for String {
311 fn combine(mut self, other: Self) -> Self {
312 self.push_str(&other);
313 self
314 }
315}
316
317/// Combines two vectors by concatenation.
318///
319/// Vector concatenation is associative: `(a ++ b) ++ c = a ++ (b ++ c)`
320///
321/// # Examples
322///
323/// ```rust
324/// use pattern_core::Combinable;
325///
326/// let v1 = vec![1, 2, 3];
327/// let v2 = vec![4, 5];
328/// let result = v1.combine(v2);
329/// assert_eq!(result, vec![1, 2, 3, 4, 5]);
330/// ```
331impl<T> Combinable for Vec<T> {
332 fn combine(mut self, other: Self) -> Self {
333 self.extend(other);
334 self
335 }
336}
337
338/// Combines two unit values (trivial).
339///
340/// Unit combination is trivially associative.
341///
342/// # Examples
343///
344/// ```rust
345/// use pattern_core::Combinable;
346///
347/// let u1 = ();
348/// let u2 = ();
349/// let result = u1.combine(u2);
350/// assert_eq!(result, ());
351/// ```
352impl Combinable for () {
353 fn combine(self, _other: Self) -> Self {}
354}
355
356// ============================================================================
357// Subject Combination Strategies
358// ============================================================================
359
360/// Combination strategy for Subject that merges labels and properties.
361///
362/// This strategy combines two subjects by:
363/// - Using the identity from the first subject
364/// - Taking the union of labels from both subjects
365/// - Merging properties (values from the second subject overwrite the first)
366///
367/// # Semigroup Laws
368///
369/// This implementation satisfies associativity:
370/// - Identity choice is associative (always picks leftmost)
371/// - Label union is associative (set union is associative)
372/// - Property merge is associative with right-bias (latter values win)
373///
374/// # Examples
375///
376/// ```rust
377/// use pattern_core::{Subject, Symbol, Combinable};
378/// use std::collections::{HashMap, HashSet};
379///
380/// let s1 = Subject {
381/// identity: Symbol("n1".to_string()),
382/// labels: {
383/// let mut s = HashSet::new();
384/// s.insert("Person".to_string());
385/// s
386/// },
387/// properties: HashMap::new(),
388/// };
389///
390/// let s2 = Subject {
391/// identity: Symbol("n2".to_string()),
392/// labels: {
393/// let mut s = HashSet::new();
394/// s.insert("Employee".to_string());
395/// s
396/// },
397/// properties: HashMap::new(),
398/// };
399///
400/// // Merge combines labels and uses first identity
401/// let merged = s1.combine(s2);
402/// assert_eq!(merged.identity.0, "n1");
403/// assert!(merged.labels.contains("Person"));
404/// assert!(merged.labels.contains("Employee"));
405/// ```
406impl Combinable for Subject {
407 fn combine(self, other: Self) -> Self {
408 // Keep first identity (leftmost in associative chain)
409 let identity = self.identity;
410
411 // Union of labels (set union is associative)
412 let labels = self.labels.union(&other.labels).cloned().collect();
413
414 // Merge properties (right overwrites left)
415 let mut properties = self.properties;
416 properties.extend(other.properties);
417
418 Subject {
419 identity,
420 labels,
421 properties,
422 }
423 }
424}
425
426/// Newtype wrapper for "first wins" combination strategy.
427///
428/// When combining two FirstSubject instances, the first subject is returned
429/// and the second is discarded. This is useful for scenarios where you want
430/// to keep the initial subject and ignore subsequent ones.
431///
432/// # Semigroup Laws
433///
434/// This satisfies associativity trivially: first(first(a, b), c) = first(a, first(b, c)) = a
435///
436/// # Examples
437///
438/// ```rust
439/// use pattern_core::{Subject, Symbol, Combinable};
440/// use std::collections::HashSet;
441///
442/// let s1 = Subject {
443/// identity: Symbol("alice".to_string()),
444/// labels: HashSet::new(),
445/// properties: Default::default(),
446/// };
447///
448/// let s2 = Subject {
449/// identity: Symbol("bob".to_string()),
450/// labels: HashSet::new(),
451/// properties: Default::default(),
452/// };
453///
454/// // First wins - s2 is discarded
455/// let result = s1.combine(s2);
456/// assert_eq!(result.identity.0, "alice");
457/// ```
458#[derive(Clone, PartialEq)]
459pub struct FirstSubject(pub Subject);
460
461impl Combinable for FirstSubject {
462 fn combine(self, _other: Self) -> Self {
463 self // Always return first, discard second
464 }
465}
466
467/// Newtype wrapper for "last wins" combination strategy.
468///
469/// When combining two LastSubject instances, the second subject is returned
470/// and the first is discarded. This is useful for scenarios where you want
471/// the most recent subject to take precedence.
472///
473/// # Semigroup Laws
474///
475/// This satisfies associativity trivially: last(last(a, b), c) = last(a, last(b, c)) = c
476///
477/// # Examples
478///
479/// ```rust
480/// use pattern_core::{Subject, Symbol, Combinable, LastSubject};
481/// use std::collections::HashSet;
482///
483/// let s1 = LastSubject(Subject {
484/// identity: Symbol("alice".to_string()),
485/// labels: HashSet::new(),
486/// properties: Default::default(),
487/// });
488///
489/// let s2 = LastSubject(Subject {
490/// identity: Symbol("bob".to_string()),
491/// labels: HashSet::new(),
492/// properties: Default::default(),
493/// });
494///
495/// // Last wins - s1 is the last argument, so it wins
496/// let result = s2.combine(s1);
497/// assert_eq!(result.0.identity.0, "alice");
498/// ```
499#[derive(Clone, PartialEq)]
500pub struct LastSubject(pub Subject);
501
502impl Combinable for LastSubject {
503 fn combine(self, other: Self) -> Self {
504 other // Always return second, discard first
505 }
506}
507
508/// Newtype wrapper for "empty" combination strategy that creates anonymous subjects.
509///
510/// When combining two EmptySubject instances, the result is always an anonymous
511/// subject with no labels or properties. This serves as the identity element for
512/// a Monoid-like structure.
513///
514/// # Semigroup Laws
515///
516/// This satisfies associativity trivially: empty(empty(a, b), c) = empty(a, empty(b, c)) = empty
517///
518/// # Monoid Laws
519///
520/// When used with Default, this provides monoid identity:
521/// - Left identity: empty.combine(s) = empty
522/// - Right identity: s.combine(empty) = empty
523///
524/// # Examples
525///
526/// ```rust
527/// use pattern_core::{Subject, Symbol, Combinable, EmptySubject};
528/// use std::collections::HashSet;
529///
530/// let s1 = EmptySubject(Subject {
531/// identity: Symbol("alice".to_string()),
532/// labels: {
533/// let mut s = HashSet::new();
534/// s.insert("Person".to_string());
535/// s
536/// },
537/// properties: Default::default(),
538/// });
539///
540/// let empty = EmptySubject(Subject {
541/// identity: Symbol("_".to_string()),
542/// labels: HashSet::new(),
543/// properties: Default::default(),
544/// });
545///
546/// // Always returns empty (anonymous)
547/// let result = s1.combine(empty);
548/// assert_eq!(result.0.identity.0, "_");
549/// assert!(result.0.labels.is_empty());
550/// ```
551#[derive(Clone, PartialEq)]
552pub struct EmptySubject(pub Subject);
553
554impl Combinable for EmptySubject {
555 fn combine(self, _other: Self) -> Self {
556 // Always return anonymous empty subject
557 EmptySubject(Subject {
558 identity: Symbol("_".to_string()),
559 labels: Default::default(),
560 properties: Default::default(),
561 })
562 }
563}
564
565impl Default for EmptySubject {
566 fn default() -> Self {
567 EmptySubject(Subject {
568 identity: Symbol("_".to_string()),
569 labels: Default::default(),
570 properties: Default::default(),
571 })
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use std::collections::{HashMap, HashSet};
579
580 #[test]
581 fn subject_merge_combines_labels_and_properties() {
582 let s1 = Subject {
583 identity: Symbol("n1".to_string()),
584 labels: {
585 let mut s = HashSet::new();
586 s.insert("Person".to_string());
587 s
588 },
589 properties: {
590 let mut m = HashMap::new();
591 m.insert("name".to_string(), Value::VString("Alice".to_string()));
592 m
593 },
594 };
595
596 let s2 = Subject {
597 identity: Symbol("n2".to_string()),
598 labels: {
599 let mut s = HashSet::new();
600 s.insert("Employee".to_string());
601 s
602 },
603 properties: {
604 let mut m = HashMap::new();
605 m.insert("role".to_string(), Value::VString("Engineer".to_string()));
606 m
607 },
608 };
609
610 let merged = s1.combine(s2);
611
612 assert_eq!(merged.identity.0, "n1");
613 assert_eq!(merged.labels.len(), 2);
614 assert!(merged.labels.contains("Person"));
615 assert!(merged.labels.contains("Employee"));
616 assert_eq!(merged.properties.len(), 2);
617 }
618
619 #[test]
620 fn subject_merge_is_associative() {
621 let s1 = Subject {
622 identity: Symbol("a".to_string()),
623 labels: {
624 let mut s = HashSet::new();
625 s.insert("L1".to_string());
626 s
627 },
628 properties: HashMap::new(),
629 };
630
631 let s2 = Subject {
632 identity: Symbol("b".to_string()),
633 labels: {
634 let mut s = HashSet::new();
635 s.insert("L2".to_string());
636 s
637 },
638 properties: HashMap::new(),
639 };
640
641 let s3 = Subject {
642 identity: Symbol("c".to_string()),
643 labels: {
644 let mut s = HashSet::new();
645 s.insert("L3".to_string());
646 s
647 },
648 properties: HashMap::new(),
649 };
650
651 // (s1 + s2) + s3
652 let left = s1.clone().combine(s2.clone()).combine(s3.clone());
653
654 // s1 + (s2 + s3)
655 let right = s1.combine(s2.combine(s3));
656
657 assert_eq!(left.identity, right.identity);
658 assert_eq!(left.labels, right.labels);
659 }
660
661 #[test]
662 fn first_subject_keeps_first() {
663 let s1 = FirstSubject(Subject {
664 identity: Symbol("alice".to_string()),
665 labels: HashSet::new(),
666 properties: HashMap::new(),
667 });
668
669 let s2 = FirstSubject(Subject {
670 identity: Symbol("bob".to_string()),
671 labels: HashSet::new(),
672 properties: HashMap::new(),
673 });
674
675 let result = s1.clone().combine(s2);
676 assert_eq!(result.0.identity.0, "alice");
677 }
678
679 #[test]
680 fn last_subject_keeps_last() {
681 let s1 = LastSubject(Subject {
682 identity: Symbol("alice".to_string()),
683 labels: HashSet::new(),
684 properties: HashMap::new(),
685 });
686
687 let s2 = LastSubject(Subject {
688 identity: Symbol("bob".to_string()),
689 labels: HashSet::new(),
690 properties: HashMap::new(),
691 });
692
693 let result = s1.combine(s2.clone());
694 assert_eq!(result.0.identity.0, "bob");
695 }
696
697 #[test]
698 fn empty_subject_returns_anonymous() {
699 let s1 = EmptySubject(Subject {
700 identity: Symbol("alice".to_string()),
701 labels: {
702 let mut s = HashSet::new();
703 s.insert("Person".to_string());
704 s
705 },
706 properties: HashMap::new(),
707 });
708
709 let s2 = EmptySubject(Subject {
710 identity: Symbol("bob".to_string()),
711 labels: HashSet::new(),
712 properties: HashMap::new(),
713 });
714
715 let result = s1.combine(s2);
716 assert_eq!(result.0.identity.0, "_");
717 assert!(result.0.labels.is_empty());
718 assert!(result.0.properties.is_empty());
719 }
720
721 #[test]
722 fn empty_subject_is_identity() {
723 let empty = EmptySubject::default();
724 let result = empty.clone().combine(empty);
725 assert_eq!(result.0.identity.0, "_");
726 }
727}