aimx/workflow.rs
1
2//! # Workflow Management
3//!
4//! This module provides the core workflow management functionality for AIMX.
5//! A workflow represents a collection of rules organized in a hierarchical structure,
6//! with support for versioning, concurrent access, and efficient rule management.
7//!
8//! ## Key Concepts
9//!
10//! - **Workflow**: A container for rules that represents a logical unit of work
11//! - **Rule**: Individual definitions with identifiers, types, expressions, and values
12//! - **Versioning**: Support for major (epoch) and minor (partial) version tracking
13//! - **Concurrent Access**: Thread-safe design supporting multiple readers and atomic writes
14//! - **Change Tracking**: Automatic detection of changes requiring partial or full saves
15//!
16//! ## Workflow Structure
17//!
18//! Workflows maintain rules in two complementary data structures:
19//! - `rows`: A vector maintaining rule order and empty slots
20//! - `lookup`: A hash map for O(1) identifier-based rule access
21//!
22//! This dual-structure approach provides both ordered access (for serialization)
23//! and fast lookup (for evaluation and rule management).
24//!
25//! ## Version Control
26//!
27//! Workflows support a sophisticated version control system:
28//! - **Epoch**: Major version numbers that increment on structural changes
29//! - **Partial**: Minor version numbers for incremental updates within an epoch
30//! - **Journaling**: External journal files track version offsets for efficient loading
31//! - **Change Tracking**: Automatic detection of change types (None, Partial, Version)
32//!
33//! ## Concurrency Model
34//!
35//! The workflow system uses a multi-version concurrency control (MVCC) model:
36//! - **Non-blocking Reads**: Unlimited concurrent readers access consistent snapshots
37//! - **Atomic Writes**: Writers operate on isolated copies before publishing changes
38//! - **Fine-grained Locking**: Per-workflow locking instead of global workspace locking
39//!
40//! This design ensures high performance for agentic workflows where many tasks
41//! may need to evaluate expressions simultaneously without blocking.
42//!
43//! ## Usage Examples
44//!
45//! ```rust
46//! use aimx::{Workflow, WorkflowLike, Rule, Typedef, Expression, Literal, Value};
47//!
48//! // Create a new workflow
49//! let mut workflow = Workflow::new();
50//!
51//! // Add rules to the workflow
52//! let rule = Rule::new("temperature".to_string(), Typedef::Number,
53//! Expression::Empty, Value::Literal(Literal::Number(72.0)));
54//! workflow.append_or_update(Some(rule));
55//!
56//! // Access rules by identifier
57//! let temp_rule = workflow.get_rule("temperature").unwrap();
58//! let temp = temp_rule.value().to_literal();
59//! assert_eq!(temp, &Literal::Number(72.0));
60//!
61//! // Check that we have rules
62//! assert!(workflow.rule_count() > 0);
63//! ```
64
65use std::{
66 any::Any,
67 fmt::Debug, // Import Debug trait for the trait bound
68 fs::OpenOptions,
69 io::Write,
70 path::{Path, PathBuf},
71 collections::HashMap,
72};
73use anyhow::{Result, anyhow};
74use crate::{
75 aim::{journal_save, parse_version, read_latest, read_version, Journal, Version}, create_path, literals::parse_unsigned, parse_rule, ContextLike, Reference, Rule, Writer
76};
77
78/// The central trait for interacting with workflow content.
79///
80/// This trait defines the interface for workflow management operations.
81/// It is designed to be thread-safe (`Send + Sync`) to support concurrent access
82/// in agentic workflow applications.
83///
84/// ## Concurrency Guarantees
85///
86/// - `Send`: The type can be safely sent (moved) to another thread
87/// - `Sync`: The type can be safely shared (`&T`) between threads
88/// - `Arc<T>` requires its contained type `T` to be `Send + Sync`
89///
90/// ## Implementation Notes
91///
92/// The `WorkflowLike` trait provides both read-only and mutable operations,
93/// with the mutable operations typically implemented through interior mutability
94/// patterns to maintain thread safety.
95///
96/// # Examples
97///
98/// ```rust
99/// use aimx::{WorkflowLike, Workflow};
100/// use std::sync::Arc;
101///
102/// // Create a workflow and wrap it in an Arc for thread-safe sharing
103/// let workflow = Arc::new(Workflow::new());
104///
105/// // Multiple threads can safely read from the workflow
106/// let workflow_clone = Arc::clone(&workflow);
107/// std::thread::spawn(move || {
108/// println!("Workflow version: {}", workflow_clone.version());
109/// });
110/// ```
111pub trait WorkflowLike: Send + Sync + Debug + 'static {
112 // --- Core Metadata ---
113
114 /// Returns the major version (epoch) of the workflow.
115 ///
116 /// The epoch represents major structural changes to the workflow.
117 /// Increments when significant changes are made that require a new version.
118 fn version(&self) -> u32;
119
120 /// Returns the minor version (partial) of the workflow.
121 ///
122 /// The partial represents incremental updates within the same epoch.
123 /// Increments for minor changes that don't require a new major version.
124 fn minor_version(&self) -> u32;
125
126 /// Returns the reference identifier for this workflow.
127 ///
128 /// The reference uniquely identifies the workflow within the workspace.
129 fn reference(&self) -> &Reference;
130
131 /// Returns the file system path to the workflow's AIM file.
132 ///
133 /// This path points to the `.aim` file that stores the workflow content.
134 fn path(&self) -> &Path;
135
136 // --- Rule Access ---
137
138 /// Retrieves a rule by its row index.
139 ///
140 /// # Parameters
141 /// - `index`: The zero-based row index
142 ///
143 /// # Returns
144 /// `Some(Rule)` if the index is valid and contains a rule, `None` otherwise
145 fn get_row(&self, index: usize) -> Option<Rule>;
146
147 /// Retrieves a rule by its identifier.
148 ///
149 /// # Parameters
150 /// - `identifier`: The rule's unique identifier
151 ///
152 /// # Returns
153 /// `Some(Rule)` if the identifier exists, `None` otherwise
154 fn get_rule(&self, identifier: &str) -> Option<Rule>;
155
156 // --- Introspection & Bulk Access ---
157
158 /// Checks if a rule with the given identifier exists.
159 ///
160 /// # Parameters
161 /// - `identifier`: The identifier to check
162 ///
163 /// # Returns
164 /// `true` if the rule exists, `false` otherwise
165 fn contains(&self, identifier: &str) -> bool;
166
167 /// Checks if a specific row index contains a rule.
168 ///
169 /// # Parameters
170 /// - `index`: The row index to check
171 ///
172 /// # Returns
173 /// `true` if the row contains a rule, `false` if empty or out of bounds
174 fn has_rule(&self, index: usize) -> bool;
175
176 /// Returns the number of rules in the workflow.
177 ///
178 /// This counts only the actual rules, excluding empty rows.
179 fn rule_count(&self) -> usize;
180
181 /// Returns the total number of rows in the workflow.
182 ///
183 /// This includes both rules and empty rows.
184 fn rule_rows(&self) -> usize;
185
186 /// Returns an iterator over all rows.
187 ///
188 /// The iterator yields `&Option<Rule>` items, including empty rows.
189 /// Use this when you need to preserve the exact row structure.
190 fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Option<Rule>> + 'a>;
191
192 /// Returns an iterator over all rules.
193 ///
194 /// The iterator yields `&Rule` items, skipping empty rows.
195 /// Use this when you only need the actual rules.
196 fn iter_rules<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Rule> + 'a>;
197
198 // --- Trait Object Helper ---
199
200 /// Returns a reference to the workflow as a trait object.
201 ///
202 /// This method enables downcasting to the concrete workflow type
203 /// when working with trait objects.
204 fn as_any(&self) -> &dyn Any;
205}
206
207/// Represents the types of changes that can be made to an AIM structure.
208///
209/// This enum is used to determine what kind of save operation is needed.
210/// The change type affects how the workflow is persisted to disk.
211///
212/// ## Change Types
213///
214/// - `None`: No changes have been made, no save needed
215/// - `Partial`: Incremental changes that can be saved as a partial update
216/// - `Version`: Structural changes that require a new version
217///
218/// # Examples
219///
220/// ```rust
221/// use aimx::workflow::Changes;
222///
223/// let no_changes = Changes::None;
224/// let partial_changes = Changes::Partial;
225/// let version_changes = Changes::Version;
226///
227/// assert_ne!(no_changes, partial_changes);
228/// assert_ne!(partial_changes, version_changes);
229/// ```
230#[derive(Debug, Clone, PartialEq)]
231pub enum Changes {
232 /// No changes have been made
233 None,
234 /// Changes that only require a partial save
235 Partial,
236 /// Changes that require a full version change
237 Version,
238}
239
240/// Represents a workflow containing rules with version control and change tracking.
241///
242/// A workflow is the central container for rules in the AIMX system. It manages:
243/// - Rule storage and indexing for efficient access
244/// - Version control with epoch and partial tracking
245/// - Change detection for optimal persistence
246/// - File system integration for loading and saving
247///
248/// ## Internal Structure
249///
250/// The workflow maintains rules using two complementary data structures:
251/// - `rows`: A vector preserving rule order and empty slots
252/// - `lookup`: A hash map enabling O(1) identifier-based access
253///
254/// This dual-structure approach provides both ordered serialization and fast lookup.
255///
256/// ## Version Control
257///
258/// Workflows support sophisticated version control:
259/// - **Epoch**: Major version numbers for structural changes
260/// - **Partial**: Minor version numbers for incremental updates
261/// - **Journaling**: External files track version offsets for efficient loading
262///
263/// ## Change Tracking
264///
265/// The workflow automatically tracks changes to optimize persistence:
266/// - `None`: No changes, skip saving
267/// - `Partial`: Incremental changes, append partial update
268/// - `Version`: Structural changes, create new version
269///
270/// # Examples
271///
272/// ```rust
273/// use aimx::{Workflow, WorkflowLike, Rule, Typedef, Expression, Literal, Value};
274///
275/// // Create a new workflow
276/// let mut workflow = Workflow::new();
277///
278/// // Add a rule
279/// let rule = Rule::new("temperature".to_string(), Typedef::Number,
280/// Expression::Empty, Value::Literal(Literal::Number(72.0)));
281/// workflow.append_or_update(Some(rule));
282///
283/// // Access rules by identifier
284/// let temp_rule = workflow.get_rule("temperature").unwrap();
285/// let temp = temp_rule.value().to_literal();
286/// assert_eq!(temp, &Literal::Number(72.0));
287/// ```
288#[derive(Debug, Clone)]
289pub struct Workflow {
290 /// Ordered storage of rules, with None representing empty slots
291 rows: Vec<Option<Rule>>,
292 /// Hash map for O(1) identifier-based lookups to row indices
293 lookup: HashMap<String, usize>,
294 /// Current version
295 version: Version,
296 /// Path to the workflow Aim file
297 path: PathBuf,
298 /// Reference to the workflow Node
299 reference: Reference,
300 /// Version journal
301 journals: Vec<Journal>,
302 /// Type of changes that have been made
303 changes: Changes,
304
305 /// This flag allows workflows to track changes for partial saves.
306 /// This flag only affects `append_or_update`, `update_rule` and
307 /// `set_rule` which are used by `Workflow::parse()`. Normally this
308 /// flag should be set to `true` after parsing input, however it should
309 /// be set to `true` before loading a previous versions it ensures all
310 /// 'recovered' rules are saved.
311 track_changes: bool,
312}
313
314impl PartialEq<Workflow> for Reference {
315 fn eq(&self, other: &Workflow) -> bool {
316 self == &other.reference
317 }
318}
319
320impl PartialEq<Reference> for Workflow {
321 fn eq(&self, other: &Reference) -> bool {
322 &self.reference == other
323 }
324}
325
326impl PartialEq<Workflow> for Workflow {
327 fn eq(&self, other: &Workflow) -> bool {
328 &self.reference == &other.reference
329 }
330}
331
332impl Workflow {
333 /// Creates a new empty workflow.
334 ///
335 /// The workflow starts with:
336 /// - Empty rule storage
337 /// - Version 0.0
338 /// - Empty path and reference
339 /// - No changes pending
340 /// - Change tracking disabled
341 ///
342 /// # Returns
343 ///
344 /// A new `Workflow` instance ready for use.
345 ///
346 /// # Examples
347 ///
348 /// ```rust
349 /// use aimx::{Workflow, WorkflowLike};
350 ///
351 /// let workflow = Workflow::new();
352 /// assert_eq!(workflow.version(), 0);
353 /// assert_eq!(workflow.rule_count(), 0);
354 /// ```
355 pub fn new() -> Self {
356 Workflow {
357 rows: Vec::new(),
358 lookup: HashMap::new(),
359 version: Version::new(0, 0),
360 path: PathBuf::new(),
361 reference: Reference::new("_"),
362 journals: Vec::new(),
363 changes: Changes::None,
364 track_changes: false,
365 }
366 }
367
368 /// Creates a new workflow by parsing AIM content.
369 ///
370 /// This constructor parses the provided AIM content and creates a workflow
371 /// with the parsed rules. Change tracking is automatically enabled.
372 ///
373 /// # Parameters
374 ///
375 /// * `input` - The AIM content to parse
376 ///
377 /// # Returns
378 ///
379 /// A new `Workflow` instance containing the parsed rules.
380 ///
381 /// # Examples
382 ///
383 /// ```rust
384 /// use aimx::{Workflow, WorkflowLike};
385 ///
386 /// let workflow = Workflow::parse_new("[1]\ntemperature: Number = 72");
387 /// assert!(workflow.rule_count() > 0);
388 /// ```
389 pub fn parse_new(input: &str) -> Self {
390 let mut workflow = Self::new();
391 let _ = workflow.parse(input, None);
392 workflow.track_changes();
393 workflow
394 }
395
396 /// Creates a new workflow by loading from a reference.
397 ///
398 /// This constructor loads a workflow from the file system based on the
399 /// provided reference. If the file doesn't exist, an empty workflow
400 /// is created with the reference set.
401 ///
402 /// # Parameters
403 ///
404 /// * `reference` - The workflow reference to load
405 ///
406 /// # Returns
407 ///
408 /// A new `Workflow` instance loaded from the file system.
409 pub fn load_new(reference: &Reference) -> Self {
410 let mut workflow = Self::new();
411 if let Ok(path) = create_path(reference) {
412 workflow.reference = reference.clone();
413 // This error should really be reported somewhere
414 workflow.load(&path).map_err(|_| ()).unwrap();
415 }
416 workflow
417 }
418
419 /// Creates a new workflow by loading from a workspace path.
420 ///
421 /// This constructor loads a workflow from the specified file system path.
422 ///
423 /// # Parameters
424 ///
425 /// * `path` - The path to the AIM file to load
426 ///
427 /// # Returns
428 ///
429 /// A new `Workflow` instance loaded from the file system.
430 pub fn new_workspace(path: &Path) -> Self {
431 let mut workflow = Self::new();
432 // This error should really be reported somewhere
433 workflow.load(path).map_err(|_| ()).unwrap();
434 workflow
435 }
436
437 /// Checks if the workflow has unsaved changes.
438 ///
439 /// # Returns
440 ///
441 /// `true` if the workflow has changes that need to be saved, `false` otherwise.
442 pub fn is_touched(&self) -> bool {
443 self.changes != Changes::None
444 }
445
446 /// Flags changes that only require a partial save.
447 ///
448 /// This method escalates the change status from `None` to `Partial`.
449 /// If changes are already at `Partial` or `Version`, they remain unchanged.
450 pub fn set_partial_change(&mut self) {
451 // We only escalate partial changes from none
452 if self.changes == Changes::None {
453 self.changes = Changes::Partial;
454 }
455 }
456
457 /// Flags changes that require a version change on save.
458 ///
459 /// This method sets the change status to `Version`, indicating that
460 /// structural changes have been made that require a new version.
461 pub fn set_version_change(&mut self) {
462 self.changes = Changes::Version;
463 }
464
465 /// Clears the changes flag.
466 ///
467 /// This method is typically called after a successful save operation
468 /// to reset the change tracking state.
469 pub fn clear_changes(&mut self) {
470 self.changes = Changes::None;
471 }
472
473 /// Enables change tracking for partial saves.
474 ///
475 /// When enabled, the workflow will track changes made through specific
476 /// operations (`append_or_update`, `update_rule`, `set_rule`) and
477 /// automatically flag them for partial saving.
478 ///
479 /// This should be enabled before or after using `workflow.parse()` directly.
480 pub fn track_changes(&mut self) {
481 self.track_changes = true;
482 }
483
484 /// Gets the first version number from the journal.
485 ///
486 /// # Returns
487 ///
488 /// The epoch of the first version, or 0 if no versions exist.
489 pub fn first_version(&self) -> u32 {
490 if self.journals.len() == 0 {
491 0
492 } else {
493 self.journals[0].version()
494 }
495 }
496
497 /// Gets the latest version number from the journal.
498 ///
499 /// # Returns
500 ///
501 /// The epoch of the latest version, or 0 if no versions exist.
502 pub fn latest_version(&self) -> u32 {
503 if self.journals.len() > 0 {
504 self.journals[self.journals.len() - 1].version()
505 }
506 else {
507 0
508 }
509 }
510
511 /// Loads a specific version of an AIM file.
512 ///
513 /// This method loads a particular version (and optionally a specific partial)
514 /// from the AIM file. After loading, the workflow switches back to the latest
515 /// version but flags that a version change is needed since the loaded version
516 /// differs from the current one.
517 ///
518 /// # Parameters
519 ///
520 /// * `path` - The path to the AIM file
521 /// * `version` - The epoch version to load
522 /// * `partial` - Optional partial version within the epoch
523 ///
524 /// # Returns
525 ///
526 /// `Ok(())` if successful, or an error if the version cannot be loaded.
527 pub fn load_version(
528 &mut self,
529 path: &Path,
530 version: u32,
531 partial: Option<u32>,
532 ) -> Result<()> {
533 if self.path != *path {
534 self.path = path.to_path_buf();
535 }
536 // Read the contents of specific version section
537 let contents = read_version(&mut self.journals, &path, version)?;
538 self.version.set_epoc(version);
539 self.track_changes();
540 // IMPORTANT! parser will throw an error if version is not found
541 self.parse(&contents, partial)?;
542 // Switch back to the latest version
543 self.version.set_epoc(self.latest_version());
544 // This is different to the latest version so flag a version change
545 self.set_version_change();
546 Ok(())
547 }
548
549 /// Loads the latest version of an AIM file.
550 ///
551 /// This method loads the most recent version from the AIM file and
552 /// clears any pending changes. Change tracking is enabled after loading.
553 ///
554 /// # Parameters
555 ///
556 /// * `path` - The path to the AIM file
557 ///
558 /// # Returns
559 ///
560 /// `Ok(())` if successful, or an error if the file cannot be loaded.
561 pub fn load(&mut self, path: &Path) -> Result<()> {
562 if self.path != *path {
563 self.path = path.to_path_buf();
564 }
565 // Read the latest section
566 let contents = read_latest(&mut self.journals, &path)?;
567 self.version.set_epoc(self.latest_version());
568 self.parse(&contents, None)?;
569 self.clear_changes();
570 self.track_changes();
571 Ok(())
572 }
573
574 /// Saves the AIM structure to its associated file.
575 ///
576 /// This function saves changes to the AIM file based on the type of changes made:
577 /// - `Changes::Version`: Creates a new version with incremented epoch
578 /// - `Changes::Partial`: Creates a partial update with incremented partial counter
579 /// - `Changes::None`: No changes to save, returns Ok(())
580 ///
581 /// The function also updates the journal file with position information for fast lookup.
582 ///
583 /// # Returns
584 ///
585 /// `Ok(())` if successful, or an error if the save operation fails.
586 ///
587 /// # Examples
588 ///
589 /// ```rust
590 /// use aimx::{Workflow, Rule, Typedef, Expression, Literal, Value};
591 ///
592 /// let mut workflow = Workflow::new();
593 /// let rule = Rule::new("test".to_string(), Typedef::Number,
594 /// Expression::Empty, Value::Literal(Literal::Number(42.0)));
595 /// workflow.track_changes();
596 /// workflow.append_or_update(Some(rule));
597 ///
598 /// // Workflow now has changes that could be saved
599 /// assert!(workflow.is_touched());
600 /// ```
601 pub fn save(&mut self) -> Result<()> {
602 let mut writer = Writer::formulizer();
603 let mut new_version = self.version.clone();
604 let mut update_journal = false;
605 // Guarantee a version header on the first save
606 if self.version.epoc() == 0 {
607 self.set_version_change();
608 }
609 match self.changes {
610 Changes::Version => {
611 new_version.increment_epoc();
612 writer.write_version(new_version.epoc(), new_version.partial());
613 self.iter_rows_mut().for_each(|opt_rule| match opt_rule {
614 Some(rule) => {
615 rule.save(&mut writer);
616 }
617 None => {
618 writer.write_eol();
619 }
620 });
621 update_journal = true;
622 }
623 Changes::Partial => {
624 new_version.increment_partial();
625 writer.write_version(new_version.epoc(), new_version.partial());
626 self.iter_mut_with_rows()
627 .for_each(|(index, opt_rule)| match opt_rule {
628 Some(rule) => {
629 rule.partial_save(index, &mut writer);
630 }
631 None => {}
632 });
633 }
634 Changes::None => return Ok(()),
635 }
636 // Open file for appending
637 let mut file = OpenOptions::new()
638 .create(true)
639 .append(true)
640 .open(&self.path)?;
641
642 if update_journal {
643 self.journals
644 .push(Journal::new(new_version.epoc(), file.metadata()?.len()));
645 journal_save(&self.path, &self.journals)?;
646 }
647 file.write(writer.finish().as_bytes())?;
648 self.version = new_version;
649 self.clear_changes();
650 Ok(())
651 }
652
653 /// Gets a mutable reference to a rule by identifier.
654 ///
655 /// # Parameters
656 ///
657 /// * `identifier` - The rule identifier
658 ///
659 /// # Returns
660 ///
661 /// `Some(&mut Rule)` if the rule exists, `None` otherwise.
662 pub fn get_rule_mut(&mut self, identifier: &str) -> Option<&mut Rule> {
663 self.lookup
664 .get(identifier)
665 .and_then(|&index| self.rows[index].as_mut())
666 }
667
668 /// Appends a rule to the workflow or updates an existing rule.
669 ///
670 /// If a rule with the same identifier already exists, it is updated.
671 /// Otherwise, the rule is appended to the end of the workflow.
672 ///
673 /// # Parameters
674 ///
675 /// * `row` - The rule to append or update
676 pub fn append_or_update(&mut self, row: Option<Rule>) {
677 if let Some(mut rule) = row {
678 // Update if the rule already exists
679 if self.lookup.contains_key(rule.identifier()) {
680 // Rule update guaranteed because identifier exists
681 self.update_rule(rule).unwrap();
682 return;
683 }
684 let index = self.rows.len();
685 self.lookup.insert(rule.identifier().to_string(), index);
686 if self.track_changes {
687 rule.touch();
688 self.set_partial_change();
689 }
690 self.rows.push(Some(rule));
691 } else {
692 if self.track_changes {
693 self.set_version_change();
694 }
695 self.rows.push(None);
696 }
697 }
698
699 /// Updates an existing rule.
700 ///
701 /// # Parameters
702 ///
703 /// * `rule` - The updated rule
704 ///
705 /// # Returns
706 ///
707 /// `Ok(())` if successful, or an error if the rule doesn't exist.
708 pub fn update_rule(&mut self, mut rule: Rule) -> Result<()> {
709 match self.lookup.get(rule.identifier()) {
710 Some(&index) => {
711 if self.track_changes {
712 rule.touch();
713 self.set_partial_change();
714 }
715 self.rows[index] = Some(rule);
716 Ok(())
717 }
718 None => Err(anyhow!("Rule {} not found", rule.identifier())),
719 }
720 }
721
722 /// Deletes a rule by identifier.
723 ///
724 /// # Parameters
725 ///
726 /// * `identifier` - The rule identifier to delete
727 ///
728 /// # Returns
729 ///
730 /// `Some(Rule)` if the rule was deleted, `None` if it didn't exist.
731 pub fn delete_rule(&mut self, identifier: &str) -> Option<Rule> {
732 match self.lookup.remove(identifier) {
733 Some(index) => {
734 self.set_version_change();
735 self.rows[index].take()
736 }
737 None => None,
738 }
739 }
740
741 // --- Indexed Rule Access --
742
743 /// Gets the row index of a rule by identifier.
744 ///
745 /// # Parameters
746 ///
747 /// * `identifier` - The rule identifier
748 ///
749 /// # Returns
750 ///
751 /// `Some(usize)` if the rule exists, `None` otherwise.
752 pub fn get_index(&self, identifier: &str) -> Option<usize> {
753 self.lookup.get(identifier).copied()
754 }
755
756 /// Sets a rule at a specific row index.
757 ///
758 /// If a rule already exists at the specified index with a different identifier,
759 /// this method will return an error. If the index is beyond the current size
760 /// of the workflow, the rows vector will be expanded to accommodate it.
761 ///
762 /// # Parameters
763 ///
764 /// * `index` - The zero-based row index where to place the rule
765 /// * `row` - The rule to set, or None to create an empty row
766 ///
767 /// # Returns
768 ///
769 /// `Ok(())` if successful, or an error if there's a conflict with an existing rule
770 pub fn set_row(&mut self, index: usize, row: Option<Rule>) -> Result<()> {
771 if let Some(mut rule) = row {
772 // Check if the rule identifier already exists
773 if self.lookup.contains_key(rule.identifier()) {
774 if index < self.rows.len() {
775 // Get the rule at the specified index
776 match self.rows[index].as_ref() {
777 Some(old_rule) => {
778 // Ensure the new rule and old rule identifiers match
779 if rule.identifier() != old_rule.identifier() {
780 return Err(anyhow!("Rule already exists"));
781 }
782 // Manage this special case
783 if self.track_changes {
784 rule.touch();
785 self.set_partial_change();
786 }
787 self.rows[index] = Some(rule);
788 return Ok(());
789 }
790 None => return Err(anyhow!("Rule already exists")),
791 }
792 } else {
793 return Err(anyhow!("Rule already exists"));
794 }
795 }
796 // Expand vector if needed
797 if index >= self.rows.len() {
798 self.rows.resize_with(index + 1, || None);
799 }
800 // Insert at specified position
801 self.lookup.insert(rule.identifier().to_string(), index);
802 if self.track_changes {
803 rule.touch();
804 self.set_partial_change();
805 }
806 self.rows[index] = Some(rule);
807 } else {
808 // Expand vector if needed
809 if index >= self.rows.len() {
810 self.rows.resize_with(index + 1, || None);
811 }
812 if self.track_changes {
813 self.set_partial_change();
814 }
815 self.rows[index] = None;
816 }
817 Ok(())
818 }
819
820 /// Inserts a rule at a specific row index.
821 ///
822 /// This method inserts a rule at the specified index, shifting existing rules
823 /// to higher indices. If the index is beyond the current size of the workflow,
824 /// empty rows will be created as needed. If a rule with the same identifier
825 /// already exists in the workflow, this method returns an error.
826 ///
827 /// # Parameters
828 ///
829 /// * `index` - The zero-based row index where to insert the rule
830 /// * `row` - The rule to insert, or None to create an empty row
831 ///
832 /// # Returns
833 ///
834 /// `Ok(())` if successful, or an error if a rule with the same identifier already exists
835 pub fn insert_row(&mut self, index: usize, row: Option<Rule>) -> Result<()> {
836 if let Some(rule) = row {
837 if self.lookup.contains_key(rule.identifier()) {
838 return Err(anyhow!("Rule already exists"));
839 }
840 if index >= self.rows.len() {
841 // Expand vector if needed
842 if index > self.rows.len() {
843 self.rows.resize_with(index, || None);
844 }
845 // and append
846 self.lookup.insert(rule.identifier().to_string(), index);
847 self.rows.push(Some(rule));
848 } else {
849 // Insert at specified position
850 self.lookup.insert(rule.identifier().to_string(), index);
851 self.rows.insert(index, Some(rule));
852 }
853 } else if index >= self.rows.len() {
854 // Expand vector if needed
855 if index > self.rows.len() {
856 self.rows.resize_with(index, || None);
857 }
858 // and append
859 self.rows.push(None);
860 } else {
861 self.rows.insert(index, None);
862 }
863
864 // Update indices of all rows
865 self.reindex();
866 self.set_version_change();
867 Ok(())
868 }
869
870 /// Repositions a rule from one row index to another.
871 ///
872 /// This method moves a rule from the `from` index to the `to` index,
873 /// shifting other rules as needed. If either index is beyond the current
874 /// size of the workflow, the rows vector will be expanded to accommodate them.
875 ///
876 /// # Parameters
877 ///
878 /// * `from` - The current zero-based row index of the rule to move
879 /// * `to` - The target zero-based row index where to place the rule
880 ///
881 /// # Returns
882 ///
883 /// `Ok(())` if successful, or an error if the operation fails
884 pub fn reposition_row(&mut self, from: usize, to: usize) -> Result<()> {
885 if from != to {
886 // Expand vectors if needed
887 let max_row = from.max(to);
888 if max_row >= self.rows.len() {
889 self.rows.resize_with(max_row + 1, || None);
890 }
891 self.set_partial_change();
892
893 // Check if there's actually a rules to move
894 if self.rows[from].is_none() && self.rows[to].is_none() {
895 return Ok(());
896 }
897
898 let opt_rule = self.rows.remove(from);
899 self.rows.insert(to, opt_rule);
900
901 // Rebuild index mappings due to reordering
902 self.reindex();
903 }
904 Ok(())
905 }
906
907 /// Clears a row at a specific index, removing the rule if present.
908 ///
909 /// This method removes the rule at the specified index but keeps the row
910 /// structure intact. The row will become empty but still exist in the workflow.
911 /// If the index is out of bounds, this method returns None.
912 ///
913 /// # Parameters
914 ///
915 /// * `index` - The zero-based row index to clear
916 ///
917 /// # Returns
918 ///
919 /// `Some(Rule)` if a rule was present and removed, `None` if the row was already empty or index was out of bounds
920 pub fn clear_row(&mut self, index: usize) -> Option<Rule> {
921 // Check bounds
922 if index >= self.rows.len() {
923 return None;
924 }
925
926 match self.rows[index].take() {
927 Some(rule) => {
928 // Remove from index map
929 self.lookup.remove(rule.identifier());
930 self.set_version_change();
931 Some(rule)
932 }
933 None => None,
934 }
935 }
936
937 /// Removes a row at a specific index, removing the rule and shrinking the workflow.
938 ///
939 /// This method removes the rule at the specified index and removes the row
940 /// entirely from the workflow, shifting subsequent rows to lower indices.
941 /// If the index is out of bounds, this method returns None.
942 ///
943 /// # Parameters
944 ///
945 /// * `index` - The zero-based row index to remove
946 ///
947 /// # Returns
948 ///
949 /// `Some(Rule)` if a rule was present and removed, `None` if the index was out of bounds
950 pub fn remove_row(&mut self, index: usize) -> Option<Rule> {
951 if index >= self.rows.len() {
952 return None;
953 }
954
955 match self.rows.remove(index) {
956 Some(rule) => {
957 // Remove from index map
958 self.lookup.remove(rule.identifier());
959
960 // Rebuild indices since we have empty spaces now
961 self.reindex();
962 self.set_version_change();
963 Some(rule)
964 }
965 None => None,
966 }
967 }
968
969 /// Create a mutable iterator over the rows.
970 ///
971 /// This iterator yields mutable references to `Option<Rule>`, allowing
972 /// modification of both rules and empty rows.
973 pub fn iter_rows_mut<'a>(&'a mut self) -> Box<dyn Iterator<Item = &'a mut Option<Rule>> + 'a> {
974 Box::new(self.rows.iter_mut())
975 }
976 /// Create a mutable iterator over the rules.
977 ///
978 /// This iterator yields mutable references to `Rule`, skipping empty rows.
979 /// It's useful when you need to modify only the actual rules in the workflow.
980 pub fn iter_rules_mut<'a>(&'a mut self) -> Box<dyn Iterator<Item = &'a mut Rule> + 'a> {
981 Box::new(self.rows.iter_mut().filter_map(|opt_rule| opt_rule.as_mut()))
982 }
983
984 /// Create an iterator over the rules with their indices.
985 ///
986 /// This iterator yields tuples of `(index, &Option<Rule>)`, providing
987 /// both the position and content of each row including empty ones.
988 pub fn iter_with_rows(&self) -> impl Iterator<Item = (usize, &Option<Rule>)> {
989 self.rows
990 .iter()
991 .enumerate()
992 .map(|(index, opt_rule)| (index, opt_rule))
993 }
994
995 /// Create a mutable iterator over the rules with their indices.
996 ///
997 /// This iterator yields tuples of `(index, &mut Option<Rule>)`, providing
998 /// both the position and mutable content of each row including empty ones.
999 pub fn iter_mut_with_rows(&mut self) -> impl Iterator<Item = (usize, &mut Option<Rule>)> {
1000 self.rows
1001 .iter_mut()
1002 .enumerate()
1003 .map(|(index, opt_rule)| (index, opt_rule))
1004 }
1005
1006 /// Rebuilds the lookup mapping index.
1007 ///
1008 /// This helper function rebuilds the internal hash map that maps rule identifiers
1009 /// to their row indices. It's called automatically after operations that change
1010 /// the position of rules within the workflow.
1011 pub fn reindex(&mut self) {
1012 self.lookup.clear();
1013 for (index, opt_rule) in self.rows.iter().enumerate() {
1014 if let Some(rule) = opt_rule {
1015 self.lookup.insert(rule.identifier().to_string(), index);
1016 }
1017 }
1018 }
1019
1020 /// Evaluates all rules in the workflow.
1021 ///
1022 /// This method evaluates each rule's expression in the context and stores
1023 /// the result back in the context. It's used to compute the current values
1024 /// of all rules in the workflow.
1025 ///
1026 /// # Parameters
1027 ///
1028 /// * `context` - The evaluation context where results will be stored
1029 pub fn evaluate(&self, context: &mut dyn ContextLike) {
1030 for rule in self.rows.iter().filter_map(|opt_rule| opt_rule.as_ref()) {
1031 let expression = rule.expression();
1032 let reference = Reference::new(rule.identifier());
1033 let value = expression.invoke(context);
1034 let _ = context.set_referenced(&reference, value);
1035 }
1036 }
1037
1038 /// Parses AIM content and populates the workflow.
1039 ///
1040 /// This method parses AIM format content and creates rules based on the parsed data.
1041 /// It can optionally parse only up to a specific partial version. The method clears
1042 /// the current workflow contents before parsing.
1043 ///
1044 /// # Parameters
1045 ///
1046 /// * `input` - The AIM content to parse
1047 /// * `partial` - Optional partial version to parse up to
1048 ///
1049 /// # Returns
1050 ///
1051 /// `Ok(())` if parsing was successful, or an error if parsing failed
1052 pub fn parse(&mut self, input: &str, partial: Option<u32>) -> Result<()> {
1053 // Clear contents
1054 self.rows.clear();
1055 self.lookup.clear();
1056 self.clear_changes();
1057
1058 let lines: Vec<&str> = input.lines().collect();
1059 let mut is_version = false;
1060 let mut is_partial = false;
1061
1062 for line in lines {
1063 let mut opt_index: Option<u32> = None;
1064 let mut line = line.trim();
1065 // Append empty lines to maintain row spacing in first (non-partial) section
1066 if line.is_empty() {
1067 if is_version && self.version.partial() == 0 {
1068 self.append_or_update(None);
1069 }
1070 continue;
1071 }
1072
1073 // Try to parse comment (lines starting with #)
1074 if line.starts_with('#') {
1075 continue;
1076 }
1077
1078 // Try to parse version headers (lines starting with [)
1079 if line.starts_with('[') {
1080 match parse_version(line) {
1081 Ok(current) => {
1082 // When version/epoc is 0, assign the first version header encountered
1083 if self.version.epoc() == 0 {
1084 is_version = true;
1085 self.version = current;
1086 }
1087 // When epoc is known, merge partials
1088 else if self.version.epoc() == current.epoc() {
1089 is_version = true;
1090 // We've move past the target partial, stop parsing
1091 if is_partial {
1092 break;
1093 }
1094 self.version.set_partial(current.partial());
1095 if let Some(opt_partial) = partial {
1096 if opt_partial == current.partial() {
1097 is_partial = true;
1098 }
1099 }
1100 } else if is_version {
1101 // We've moved past the target version, stop parsing
1102 break;
1103 }
1104 }
1105 // Skip lines with syntax errors
1106 Err(_e) => {}
1107 }
1108 continue;
1109 }
1110
1111 // If we haven't found the target version yet, skip this line
1112 if !is_version {
1113 continue;
1114 }
1115
1116 /* if line.starts_with(?) {
1117 // The grammar can support additional extension here if needed in the future
1118 // with signifiers like: '"'|'!'|'@'|'$'|'%'|'^'|'&'|'*'|'('
1119 } */
1120
1121 // Try to parse leading number (rule index used by partial saves)
1122 if line.chars().next().map_or(false, |c| c.is_ascii_digit()) {
1123 match parse_unsigned(line) {
1124 Ok((remainder, index)) => {
1125 opt_index = Some(index);
1126 line = remainder;
1127 }
1128 Err(_) => {}
1129 }
1130 }
1131
1132 // Try to parse rule
1133 match parse_rule(line) {
1134 Ok(rule) => {
1135 if rule.is_node() {
1136 todo!();
1137 }
1138 match opt_index {
1139 Some(index) => {
1140 // Set rule at index
1141 if self.contains(&rule.identifier()) {
1142 let _ = self.update_rule(rule);
1143 } else {
1144 let _ = self.set_row(index as usize, Some(rule));
1145 }
1146 }
1147 None => {
1148 // Append or overwrite existing rule
1149 self.append_or_update(Some(rule));
1150 }
1151 }
1152 }
1153 Err(_) => {
1154 // Ignore syntax errors, but maintain row spacing in (non-partial) sections
1155 if self.version.partial() == 0 {
1156 self.append_or_update(None);
1157 }
1158 }
1159 }
1160 }
1161
1162 if !is_version && self.version.epoc() > 0 {
1163 return Err(anyhow!("Version {} not found", self.version.epoc()));
1164 }
1165
1166 Ok(())
1167 }
1168}
1169
1170impl WorkflowLike for Workflow {
1171
1172 // --- Core Metadata ---
1173
1174 fn version(&self) -> u32 {
1175 self.version.epoc()
1176 }
1177
1178 fn minor_version(&self) -> u32 {
1179 self.version.partial()
1180 }
1181
1182 fn reference(&self) -> &Reference {
1183 &self.reference
1184 }
1185
1186 fn path(&self) -> &Path {
1187 self.path.as_path()
1188 }
1189
1190 // --- Rule Access ---
1191
1192 fn get_row(&self, index: usize) -> Option<Rule> {
1193 if index < self.rows.len() {
1194 self.rows[index].clone()
1195 } else {
1196 None
1197 }
1198 }
1199
1200 fn get_rule(&self, identifier: &str) -> Option<Rule> {
1201 self.lookup
1202 .get(identifier)
1203 .and_then(|&index| self.rows[index].clone())
1204 }
1205
1206 // --- Introspection & Bulk Access ---
1207
1208 fn contains(&self, identifier: &str) -> bool {
1209 self.lookup.contains_key(identifier)
1210 }
1211
1212 fn has_rule(&self, index: usize) -> bool {
1213 if index < self.rows.len() {
1214 self.rows[index].is_some()
1215 } else {
1216 false
1217 }
1218 }
1219
1220 fn rule_count(&self) -> usize {
1221 self.lookup.len()
1222 }
1223
1224 fn rule_rows(&self) -> usize {
1225 self.rows.len()
1226 }
1227
1228 fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Option<Rule>> + 'a> {
1229 Box::new(self.rows.iter())
1230 }
1231
1232 fn iter_rules<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Rule> + 'a> {
1233 Box::new(self.rows.iter().filter_map(|opt_rule| opt_rule.as_ref()))
1234 }
1235
1236 // --- Trait Object Helper ---
1237
1238 fn as_any(&self) -> &dyn Any {
1239 self
1240 }
1241}