Skip to content

Commit c3a71e7

Browse files
konardclaude
andcommitted
feat: refactor Rust implementation to separate files per struct/trait
- Split lib.rs into separate modules for better code organization - Added new modules: - link.rs: Link data structure - error.rs: LinkError enum - lino_link.rs: LinoLink for parsed LiNo notation - parser.rs: LiNo parser (corresponds to Platform.Protocols.Lino.Parser) - link_storage.rs: Persistent storage with naming support - changes_simplifier.rs: Changes simplification (corresponds to ChangesSimplifier.cs) - query_processor.rs: Advanced query processing - Added support for variable resolution ($var syntax) - Added named links functionality - Added LiNo query parsing with nested patterns - File structure now mirrors C# implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 74b973d commit c3a71e7

File tree

8 files changed

+1640
-472
lines changed

8 files changed

+1640
-472
lines changed

rust/src/changes_simplifier.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! ChangesSimplifier - Simplifies a list of changes
2+
//!
3+
//! This module provides functionality to simplify a list of changes by
4+
//! identifying chains of transformations.
5+
//! Corresponds to ChangesSimplifier.cs in C#
6+
7+
use crate::link::Link;
8+
use std::collections::{HashMap, HashSet};
9+
10+
/// Simplifies a list of changes by identifying chains of transformations.
11+
///
12+
/// If multiple final states are reachable from the same initial state, returns multiple simplified changes.
13+
/// If a scenario arises where no initial or final states can be identified (no-ops), returns the original transitions as-is.
14+
pub fn simplify_changes(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
15+
if changes.is_empty() {
16+
return vec![];
17+
}
18+
19+
// First, handle unchanged states directly
20+
let mut unchanged_states = Vec::new();
21+
let mut changed_states = Vec::new();
22+
23+
for (before, after) in changes.iter() {
24+
if before == after {
25+
unchanged_states.push((*before, *after));
26+
} else {
27+
changed_states.push((*before, *after));
28+
}
29+
}
30+
31+
// Gather all 'Before' links and all 'After' links from changed states
32+
let before_links: HashSet<Link> = changed_states.iter().map(|(b, _)| *b).collect();
33+
let after_links: HashSet<Link> = changed_states.iter().map(|(_, a)| *a).collect();
34+
35+
// Identify initial states: appear as Before but never as After
36+
let initial_states: Vec<Link> = before_links
37+
.iter()
38+
.filter(|b| !after_links.contains(b))
39+
.copied()
40+
.collect();
41+
42+
// Identify final states: appear as After but never as Before
43+
let final_states: HashSet<Link> = after_links
44+
.iter()
45+
.filter(|a| !before_links.contains(a))
46+
.copied()
47+
.collect();
48+
49+
// Build adjacency (Before -> possible list of After links)
50+
let mut adjacency: HashMap<Link, Vec<Link>> = HashMap::new();
51+
for (before, after) in changed_states.iter() {
52+
adjacency.entry(*before).or_default().push(*after);
53+
}
54+
55+
// If we have no identified initial states, treat it as a no-op scenario:
56+
// just return original transitions.
57+
if initial_states.is_empty() {
58+
return changes;
59+
}
60+
61+
let mut results = Vec::new();
62+
63+
// Add unchanged states first
64+
results.extend(unchanged_states);
65+
66+
// Traverse each initial state with DFS
67+
for initial in initial_states.iter() {
68+
let mut stack = vec![*initial];
69+
let mut visited: HashSet<Link> = HashSet::new();
70+
71+
while let Some(current) = stack.pop() {
72+
// Skip if already visited
73+
if !visited.insert(current) {
74+
continue;
75+
}
76+
77+
let has_next = adjacency.contains_key(&current);
78+
let next_links = adjacency.get(&current);
79+
let is_final_or_dead_end = final_states.contains(&current)
80+
|| !has_next
81+
|| next_links.is_none_or(|v| v.is_empty());
82+
83+
// If final or no further transitions, record (initial -> current)
84+
if is_final_or_dead_end {
85+
results.push((*initial, current));
86+
}
87+
88+
// Otherwise push neighbors
89+
if let Some(next_links) = next_links {
90+
for next in next_links {
91+
stack.push(*next);
92+
}
93+
}
94+
}
95+
}
96+
97+
// Sort the final results so that items appear in ascending order by their After link.
98+
// This ensures tests that expect a specific order pass reliably.
99+
results.sort_by(|a, b| {
100+
a.1.index
101+
.cmp(&b.1.index)
102+
.then_with(|| a.1.source.cmp(&b.1.source))
103+
.then_with(|| a.1.target.cmp(&b.1.target))
104+
});
105+
106+
results
107+
}
108+
109+
#[cfg(test)]
110+
mod tests {
111+
use super::*;
112+
113+
#[test]
114+
fn test_simplify_empty() {
115+
let changes: Vec<(Link, Link)> = vec![];
116+
let result = simplify_changes(changes);
117+
assert!(result.is_empty());
118+
}
119+
120+
#[test]
121+
fn test_simplify_no_op() {
122+
let link = Link::new(1, 2, 3);
123+
let changes = vec![(link, link)];
124+
let result = simplify_changes(changes);
125+
assert_eq!(result.len(), 1);
126+
assert_eq!(result[0], (link, link));
127+
}
128+
129+
#[test]
130+
fn test_simplify_chain() {
131+
let link1 = Link::new(1, 1, 1);
132+
let link2 = Link::new(1, 2, 2);
133+
let link3 = Link::new(1, 3, 3);
134+
135+
let changes = vec![(link1, link2), (link2, link3)];
136+
let result = simplify_changes(changes);
137+
138+
assert_eq!(result.len(), 1);
139+
assert_eq!(result[0], (link1, link3));
140+
}
141+
142+
#[test]
143+
fn test_simplify_with_unchanged() {
144+
let unchanged = Link::new(2, 2, 2);
145+
let link1 = Link::new(1, 1, 1);
146+
let link2 = Link::new(1, 2, 2);
147+
148+
let changes = vec![(unchanged, unchanged), (link1, link2)];
149+
let result = simplify_changes(changes);
150+
151+
assert_eq!(result.len(), 2);
152+
}
153+
}

rust/src/error.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! Error types for link operations
2+
//!
3+
//! This module defines all error types used throughout the link-cli.
4+
5+
use thiserror::Error;
6+
7+
/// Error types for link operations
8+
#[derive(Error, Debug)]
9+
pub enum LinkError {
10+
#[error("Link not found: {0}")]
11+
NotFound(u32),
12+
13+
#[error("Invalid link format: {0}")]
14+
InvalidFormat(String),
15+
16+
#[error("Storage error: {0}")]
17+
StorageError(String),
18+
19+
#[error("Query error: {0}")]
20+
QueryError(String),
21+
22+
#[error("Parse error: {0}")]
23+
ParseError(String),
24+
}

0 commit comments

Comments
 (0)