aimx/aim/workflow.rs
1//! Workflow container and versioned persistence.
2//!
3//! Provides rule storage, lookup, and journaled version management for AIMX
4//! workflows. Used as the concrete implementation behind [`WorkflowLike`].
5
6use crate::{
7 Aim, aim::{BasePath, Journal, Row, Rule, Typedef, Version, Writer, WriterLike, parse_rule, parse_version, read_latest, read_version, save_journal, version::now}, expressions::{Expression, Reference}, literals::parse_unsigned, values::{Errata, Instance, Node, Value}
8};
9use anyhow::{Result, anyhow};
10use jiff::civil::DateTime;
11use std::{
12 any::Any,
13 collections::HashMap,
14 fmt::Debug, // Import Debug trait for the trait bound
15 fs::OpenOptions,
16 io::Write,
17 path::{Path, PathBuf},
18 sync::Arc,
19 fmt,
20};
21
22/// Thread-safe abstraction over workflow rule storage and metadata.
23///
24/// Implemented by concrete workflow containers such as [`Workflow`].
25pub trait WorkflowLike: Send + Sync + Debug + 'static {
26 // --- Core Metadata ---
27
28 /// Returns the global locator (Reference) for this workflow.
29 ///
30 /// The locator uniquely identifies the workflow within the workspace.
31 fn locator(&self) -> Arc<Reference>;
32
33 /// Returns the file system path to the workflow's AIM file.
34 ///
35 /// This path points to the `.aim` file that stores the workflow content.
36 fn path(&self) -> Arc<PathBuf>;
37
38 fn date(&self) -> DateTime;
39
40 /// Returns the major version (epoch) of the workflow.
41 ///
42 /// The epoch represents major structural changes to the workflow.
43 /// Increments when significant changes are made that require a new version.
44 fn version(&self) -> u32;
45
46 /// Returns the minor version (partial) of the workflow.
47 ///
48 /// The partial represents incremental updates within the same epoch.
49 /// Increments for minor changes that don't require a new major version.
50 fn minor_version(&self) -> u32;
51
52 // --- Rule Access ---
53
54 /// Gets the row index of a rule by identifier.
55 ///
56 /// # Parameters
57 ///
58 /// * `identifier` - The rule identifier
59 ///
60 /// # Returns
61 ///
62 /// `Some(usize)` if the rule exists, `None` otherwise.
63 fn get_index(&self, identifier: &str) -> Option<usize>;
64
65 /// Retrieves a rule by its row index.
66 ///
67 /// # Parameters
68 /// - `index`: The zero-based row index
69 ///
70 /// # Returns
71 /// `Some(Rule)` if the index is valid and contains a rule, `None` otherwise
72 fn get_row(&self, index: usize) -> Row;
73
74 /// Retrieves a rule by its identifier.
75 ///
76 /// # Parameters
77 /// - `identifier`: The rule's unique identifier
78 ///
79 /// # Returns
80 /// `Some(Rule)` if the identifier exists, `None` otherwise
81 fn get_rule(&self, identifier: &str) -> Option<Arc<Rule>>;
82
83 // --- Introspection & Bulk Access ---
84
85 /// Checks if a rule with the given identifier exists.
86 ///
87 /// # Parameters
88 /// - `identifier`: The identifier to check
89 ///
90 /// # Returns
91 /// `true` if the rule exists, `false` otherwise
92 fn contains(&self, identifier: &str) -> bool;
93
94 /// Checks if a specific row index contains a rule.
95 ///
96 /// # Parameters
97 /// - `index`: The row index to check
98 ///
99 /// # Returns
100 /// `true` if the row contains a rule, `false` if empty or out of bounds
101 fn has_rule(&self, index: usize) -> bool;
102
103 /// Returns the number of rules in the workflow.
104 ///
105 /// This counts only the actual rules, excluding empty rows.
106 fn rule_count(&self) -> usize;
107
108 /// Returns the total number of rows in the workflow.
109 ///
110 /// This includes both rules and empty rows.
111 fn row_count(&self) -> usize;
112
113 /// Returns an iterator over all rows.
114 ///
115 /// The iterator yields `&Option<Rule>` items, including empty rows.
116 /// Use this when you need to preserve the exact row structure.
117 fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Row> + 'a>;
118
119 /// Returns an iterator over all rules.
120 ///
121 /// The iterator yields `&Rule` items, skipping empty rows.
122 /// Use this when you only need the actual rules.
123 fn iter_rules(&self) -> Box<dyn Iterator<Item = Arc<Rule>> + '_>;
124
125 // --- Trait Object Helper ---
126
127 /// Returns a reference to the workflow as a trait object.
128 ///
129 /// This method enables downcasting to the concrete workflow type
130 /// when working with trait objects.
131 fn as_any(&self) -> &dyn Any;
132}
133
134/// Workflow execution result used by orchestrators to understand whether
135/// evaluation completed, suspended, or failed.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum WorkflowStatus {
138 /// Evaluation ran to completion.
139 Completed,
140 /// Evaluation suspended at a rule identifier; may be resumed.
141 Suspended { state_id: Arc<str> },
142 /// Evaluation failed at or near `state_id` with optional error detail.
143 Failed {
144 state_id: Option<Arc<str>>,
145 errata: Option<Arc<Errata>>,
146 },
147}
148
149impl WorkflowStatus {
150 /// Returns true if the workflow completed successfully.
151 pub fn is_completed(&self) -> bool {
152 matches!(self, WorkflowStatus::Completed)
153 }
154
155 /// Returns the failure Errata when status is Failed.
156 pub fn error(&self) -> Option<&Errata> {
157 match self {
158 WorkflowStatus::Failed {
159 errata: Some(e), ..
160 } => Some(e),
161 _ => None,
162 }
163 }
164
165 /// Returns the identifier of the rule where execution last stopped
166 /// for both Suspended and Failed states.
167 pub fn state_id(&self) -> Option<Arc<str>> {
168 match self {
169 WorkflowStatus::Suspended { state_id }
170 | WorkflowStatus::Failed {
171 state_id: Some(state_id),
172 ..
173 } => Some(state_id.clone()),
174 _ => None,
175 }
176 }
177}
178
179/// Change classification used to decide how a workflow is persisted.
180///
181/// `None` → no-op, `Partial` → append partial update, `Version` → write full version.
182#[derive(Debug, Clone, PartialEq)]
183pub enum Changes {
184 /// No changes have been made
185 None,
186 /// Changes that only require a partial save
187 Partial,
188 /// Changes that require a full version change
189 Version,
190}
191
192/// In-memory workflow with journaled versioning for AIMX rules.
193///
194/// Maintains ordered rule rows plus identifier lookup, tracks changes as
195/// [`Changes`], and persists via versioned AIM/journal files.
196#[derive(Debug, Clone)]
197pub struct Workflow {
198 /// A global reference to this workflow's node.
199 locator: Arc<Reference>,
200 /// Path to the workflow `.aim` file.
201 path: Arc<PathBuf>,
202 /// Ordered storage of rules; `None` represents an empty row.
203 rows: Vec<Row>,
204 /// Hash map for identifier-based lookups to row indices.
205 lookup: HashMap<Arc<str>, usize>,
206 /// Current version
207 version: Version,
208 /// Version journal
209 journals: Vec<Arc<Journal>>,
210 /// Type of changes that have been made
211 changes: Changes,
212
213 /// This flag allows workflows to track changes for partial saves.
214 /// This flag only affects `append_or_update`, `update_rule` and
215 /// `set_rule` which are used by `Workflow::parse()`. Normally this
216 /// flag should be set to `true` after parsing input, however it should
217 /// be set to `true` before loading a previous versions it ensures all
218 /// 'recovered' rules are saved.
219 track_changes: bool,
220}
221
222impl PartialEq<Workflow> for Reference {
223 fn eq(&self, other: &Workflow) -> bool {
224 self == &*other.locator
225 }
226}
227
228impl PartialEq<Reference> for Workflow {
229 fn eq(&self, other: &Reference) -> bool {
230 &*self.locator == other
231 }
232}
233
234impl PartialEq<Workflow> for Workflow {
235 fn eq(&self, other: &Workflow) -> bool {
236 &self.locator == &other.locator
237 }
238}
239
240
241impl WorkflowLike for Workflow {
242 // --- Core Metadata ---
243
244 fn locator(&self) -> Arc<Reference> {
245 self.locator.clone()
246 }
247
248 fn path(&self) -> Arc<PathBuf> {
249 self.path.clone()
250 }
251
252 fn date(&self) -> DateTime {
253 self.version.date()
254 }
255
256 fn version(&self) -> u32 {
257 self.version.epoch()
258 }
259
260 fn minor_version(&self) -> u32 {
261 self.version.partial()
262 }
263
264 // --- Rule Access ---
265
266 fn get_index(&self, identifier: &str) -> Option<usize> {
267 self.lookup.get(identifier).copied()
268 }
269
270 fn has_rule(&self, index: usize) -> bool {
271 if index < self.rows.len() {
272 self.rows[index].is_rule()
273 } else {
274 false
275 }
276 }
277
278 // --- Introspection & Bulk Access ---
279
280 fn get_row(&self, index: usize) -> Row {
281 if index < self.rows.len() {
282 self.rows[index].clone()
283 } else {
284 Row::Empty
285 }
286 }
287
288 fn contains(&self, identifier: &str) -> bool {
289 self.lookup.contains_key(identifier)
290 }
291
292 fn get_rule(&self, identifier: &str) -> Option<Arc<Rule>> {
293 self.lookup
294 .get(identifier)
295 .and_then(|&index| self.rows[index].rule())
296 }
297
298 fn rule_count(&self) -> usize {
299 self.lookup.len()
300 }
301
302 fn row_count(&self) -> usize {
303 self.rows.len()
304 }
305
306 fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Row> + 'a> {
307 Box::new(self.rows.iter())
308 }
309
310 fn iter_rules(&self) -> Box<dyn Iterator<Item = Arc<Rule>> + '_> {
311 Box::new(self.rows.iter().filter_map(|row| row.rule()))
312 }
313
314 // --- Trait Object Helper ---
315
316 fn as_any(&self) -> &dyn Any {
317 self
318 }
319}
320
321impl Workflow {
322 // -----------------------------------------------------------------------------
323 // Test Methods
324 // -----------------------------------------------------------------------------
325
326 /// Creates a new empty workflow.
327 ///
328 /// The workflow starts with:
329 /// - Empty rule storage
330 /// - Version 0.0
331 /// - Extract inference pattern
332 /// - Empty path and reference
333 /// - No changes pending
334 /// - Change tracking disabled
335 ///
336
337 pub fn for_testing(locator: Arc<Reference>) -> Result<Self> {
338 let path: Arc<PathBuf> = Arc::new(PathBuf::from("test.aim"));
339 Ok(Self::new_from(locator, path))
340 }
341
342 /// Creates a new workflow by parsing AIM content.
343 ///
344 /// This constructor parses the provided AIM content and creates a workflow
345 /// with the parsed rules. Change tracking is automatically enabled.
346 ///
347
348 pub fn parse_for_tests(input: &str) -> Result<Self> {
349 let mut workflow = Self::for_testing(Reference::workspace())?;
350 workflow.parse(input, None)?;
351 Ok(workflow)
352 }
353
354 // -----------------------------------------------------------------------------
355 // Api Methods
356 // -----------------------------------------------------------------------------
357
358 // Called exclusively by Workspace::create
359 pub fn new_from(locator: Arc<Reference>, path: Arc<PathBuf>) -> Self {
360 Workflow {
361 locator,
362 path,
363 rows: Vec::new(),
364 lookup: HashMap::new(),
365 version: Version::new(0, 0, now()),
366 journals: Vec::new(),
367 changes: Changes::None,
368 track_changes: true,
369 }
370 }
371
372 /// Creates a new workflow by loading from a global locator (Reference).
373 ///
374 /// This constructor loads a workflow from the file system based on the
375 /// provided locator. If the file doesn't exist, an empty workflow
376 /// is created with the locator set.
377 ///
378
379 pub fn open(locator: Arc<Reference>) -> Result<Self> {
380 let path = BasePath::convert(&locator)?;
381 let mut workflow = Self::new_from(locator, path);
382 if workflow.path.exists() {
383 workflow.load()?;
384 }
385 Ok(workflow)
386 }
387
388 /// Checks if the workflow has unsaved changes.
389 ///
390 /// # Returns
391 ///
392 /// `true` if the workflow has changes that need to be saved, `false` otherwise.
393 pub fn is_touched(&self) -> bool {
394 self.changes != Changes::None
395 }
396
397 /// Flags changes that only require a partial save.
398 ///
399 /// This method escalates the change status from `None` to `Partial`.
400 /// If changes are already at `Partial` or `Version`, they remain unchanged.
401 pub fn set_partial_change(&mut self) {
402 // We only escalate partial changes from none
403 if self.changes == Changes::None {
404 self.changes = Changes::Partial;
405 }
406 }
407
408 /// Flags changes that require a version change on save.
409 ///
410 /// This method sets the change status to `Version`, indicating that
411 /// structural changes have been made that require a new version.
412 pub fn set_version_change(&mut self) {
413 self.changes = Changes::Version;
414 }
415
416 /// Clears the changes flag.
417 ///
418 /// This method is typically called after a successful save operation
419 /// to reset the change tracking state.
420 pub fn clear_changes(&mut self) {
421 self.changes = Changes::None;
422 }
423
424 /// Gets the first version number from the journal.
425 ///
426 /// # Returns
427 ///
428 /// The epoch of the first version, or 0 if no versions exist.
429 pub fn first_version(&self) -> u32 {
430 if self.journals.is_empty() {
431 0
432 } else {
433 // first() guaranteed to exist
434 self.journals.first().unwrap().version()
435 }
436 }
437
438 /// Gets the latest version number from the journal.
439 ///
440 /// # Returns
441 ///
442 /// The epoch of the latest version, or 0 if no versions exist.
443 pub fn latest_version(&self) -> u32 {
444 if self.journals.is_empty() {
445 0
446 } else {
447 // last() guaranteed to exist
448 self.journals.last().unwrap().version()
449 }
450 }
451
452 /// Test-only: override the workflow file path.
453 pub fn set_path(&mut self, path: &Path) {
454 self.path = Arc::new(path.to_path_buf());
455 }
456
457 /// Loads a specific version of an AIM file.
458 ///
459 /// This method loads a particular version (and optionally a specific partial)
460 /// from the AIM file. After loading, the workflow switches back to the latest
461 /// version but flags that a version change is needed since the loaded version
462 /// differs from the current one.
463 ///
464 /// # Parameters
465 ///
466 /// * `path` - The path to the AIM file
467 /// * `version` - The epoch version to load
468 /// * `partial` - Optional partial version within the epoch
469 ///
470 /// # Returns
471 ///
472 /// `Ok(())` if successful, or an error if the version cannot be loaded.
473 pub fn load_version(&mut self, version: u32, partial: Option<u32>) -> Result<()> {
474 // Read the contents of specific version section
475 let contents = read_version(&mut self.journals, &self.path, version)?;
476 self.version.set_epoch(version);
477 // IMPORTANT! parser will throw an error if version is not found
478 self.parse(&contents, partial)?;
479 // Switch back to the latest version
480 self.version.set_epoch(self.latest_version());
481 // This is different to the latest version so flag a version change
482 self.set_version_change();
483 Ok(())
484 }
485
486 /// Loads the latest version of an AIM file.
487 ///
488 /// This method loads the most recent version from the AIM file and
489 /// clears any pending changes. Change tracking is enabled after loading.
490 ///
491 /// # Parameters
492 ///
493 /// * `path` - The path to the AIM file
494 ///
495 /// # Returns
496 ///
497 /// `Ok(())` if successful, or an error if the file cannot be loaded.
498 pub fn load(&mut self) -> Result<()> {
499 // Read the latest section
500 let contents = read_latest(&mut self.journals, &self.path)?;
501 self.version.set_epoch(self.latest_version());
502 self.track_changes = false;
503 self.parse(&contents, None)?;
504 self.clear_changes();
505 self.track_changes = true;
506 Ok(())
507 }
508
509 /// Saves the AIM structure to its associated file.
510 ///
511 /// This function saves changes to the AIM file based on the type of changes made:
512 /// - `Changes::Version`: Creates a new version with incremented epoch
513 /// - `Changes::Partial`: Creates a partial update with incremented partial counter
514 /// - `Changes::None`: No changes to save, returns Ok(())
515 ///
516 /// The function also updates the journal file with position information for fast lookup.
517 ///
518 /// # Returns
519 ///
520 /// `Ok(())` if successful, or an error if the save operation fails.
521 ///
522 pub fn save(&mut self) -> Result<()> {
523 let mut writer = Writer::formulizer();
524 let mut new_version = self.version.clone();
525 let mut update_journal = false;
526 // Guarantee a version header on the first save
527 if self.version.epoch() == 0 {
528 self.set_version_change();
529 }
530 match self.changes {
531 Changes::Version => {
532 new_version.increment_epoch();
533 new_version.print(&mut writer);
534 // Collect touched rows first to avoid borrowing conflicts
535 let mut touched_rows = Vec::new();
536 for (index, row) in self.iter_with_rows() {
537 let untouched = row.save(&mut writer);
538 if let Some(row) = untouched {
539 touched_rows.push((index, row));
540 }
541 }
542 // Now update the rows after the iteration is complete
543 for (index, row) in touched_rows {
544 self.rows[index] = row;
545 }
546 update_journal = true;
547 }
548 Changes::Partial => {
549 new_version.increment_partial();
550 new_version.print(&mut writer);
551 // Collect touched rows first to avoid borrowing conflicts
552 let mut touched_rows = Vec::new();
553 for (index, row) in self.iter_with_rows() {
554 let untouched = row.partial_save(index, &mut writer);
555 if let Some(row) = untouched {
556 touched_rows.push((index, row));
557 }
558 }
559 // Now update the rows after the iteration is complete
560 for (index, row) in touched_rows {
561 self.rows[index] = row;
562 }
563 }
564 Changes::None => return Ok(()),
565 }
566 // Open file for appending
567 let mut file = OpenOptions::new()
568 .create(true)
569 .append(true)
570 .open(&*self.path)?;
571
572 if update_journal {
573 self.journals
574 .push(Arc::new(Journal::new(new_version.epoch(), file.metadata()?.len(), now())));
575 save_journal(&self.path, &self.journals)?;
576 }
577 file.write(writer.finish().as_bytes())?;
578 self.version = new_version;
579 self.clear_changes();
580 Ok(())
581 }
582
583 pub fn save_all(&mut self) -> Result<()> {
584 // Recursively save child nodes and instances
585 for rule in self.iter_rules() {
586 if let Some(node) = rule.get_node() {
587 node.save_all()?;
588 // Also handle instances within this workflow
589 } else if let Some(instance) = rule.get_instance() {
590 instance.save_all()?;
591 }
592 }
593 self.save()
594 }
595
596 pub fn check_identifier_unique(&self, identifier: &str) -> Result<()> {
597 Aim::check_identifier(identifier)?;
598 if self.contains(&identifier) {
599 return Err(anyhow!("Rule {} exists in {}", identifier, self.locator));
600 }
601 Ok(())
602 }
603
604 pub fn add_node(&mut self, identifier: Arc<str>, node: Node) -> Result<Arc<Reference>> {
605 self.check_identifier_unique(&identifier)?;
606
607 // If this is the workspace make a new top level reference
608 let reference = if self.locator.contains_root() {
609 Reference::new(identifier.clone())
610 } else {
611 Arc::new(self.locator.add_child(identifier.clone())?)
612 };
613
614 // Initialize as Unloaded
615 node.init(reference.clone());
616
617 let rule = Rule::new(
618 identifier,
619 Typedef::Node,
620 Expression::Empty,
621 Arc::new(Value::Node(node))
622 );
623 self.append_or_update(Row::Rule(Arc::new(rule)));
624 Ok(reference)
625 }
626
627 pub fn copy_node(&mut self, from: &Node, to: Arc<str>) -> Result<()> {
628 self.check_identifier_unique(&to)?;
629 let from_workflow = from.get_workflow_like()?;
630 let from_path = from_workflow.path();
631
632 // Create and add new node reference
633 let to_reference = if &*self.locator == "_" {
634 Reference::new(to.clone())
635 } else {
636 Arc::new(self.locator.add_child(to.clone())?)
637 };
638
639 // Construct target path for the new node
640 let base_path = self.path.with_extension("");
641 if !base_path.exists() {
642 std::fs::create_dir_all(&base_path)?;
643 }
644 let to_path = base_path.join(format!("{}.aim", to));
645
646 // Copy AIM file if it exists
647 if from_path.exists() {
648 std::fs::copy(&*from_path, &*to_path)?;
649 }
650
651 // Copy JNL file if it exists
652 let from_jnl_path = from_path.with_extension("jnl");
653 let to_jnl_path = to_path.with_extension("jnl");
654 if from_jnl_path.exists() {
655 std::fs::copy(&from_jnl_path, &to_jnl_path)?;
656 }
657
658 // Copy directory contents recursively if it exists (for child nodes)
659 let from_dir_path = from_path.with_extension("");
660 let to_dir_path = to_path.with_extension("");
661 if from_dir_path.exists() && from_dir_path.is_dir() {
662 if to_dir_path.exists() {
663 std::fs::remove_dir_all(&to_dir_path)?;
664 }
665 Self::copy_dir_all(&from_dir_path, &to_dir_path)?;
666 }
667
668 let node = Node::init_new(to_reference, from.inference());
669 self.add_node(to.clone(), node)?;
670 Ok(())
671 }
672
673 /// Recursively copy directory contents
674 fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
675 std::fs::create_dir_all(dst)?;
676 for entry in std::fs::read_dir(src)? {
677 let entry = entry?;
678 let file_type = entry.file_type()?;
679 let dst_path = dst.join(entry.file_name());
680
681 if file_type.is_dir() {
682 Self::copy_dir_all(&entry.path(), &dst_path)?;
683 } else {
684 std::fs::copy(entry.path(), dst_path)?;
685 }
686 }
687 Ok(())
688 }
689
690 pub fn get_node(&self, identifier: &str) -> Result<Node> {
691 if let Some(rule) = self.get_rule(identifier) {
692 if let Some(node) = rule.get_node() {
693 return Ok(node);
694 }
695 }
696 return Err(anyhow!("Node {} not found", identifier));
697 }
698
699 pub fn rename_node(&mut self, from: &str, to: Arc<str>) -> Result<()> {
700 self.check_identifier_unique(&to)?;
701 if let Some(from_rule) = self.get_rule(from) {
702 // Save the 'from' workflow before renaming its node
703 if let Some(from_node) = from_rule.get_node() {
704 let from_workflow = from_node.get_workflow_like()?;
705 let from_path = from_workflow.path();
706
707 // Rename the workflow node
708 if let Some(index) = self.get_index(from) {
709 match self.get_row(index) {
710 Row::Rule(from_rule)
711 | Row::Touched(from_rule) => {
712 let to_rule = Arc::new(from_rule.clone().rename_node(to.clone())?);
713 // Clear the previous row
714 self.replace_row(index, Row::Touched(to_rule.clone()))?;
715
716 // Rename associated files if they exist
717 if from_path.exists() {
718 // Rename the .aim file
719 let to_path = from_path.with_file_name(format!("{}.aim", to));
720 std::fs::rename(&*from_path, &to_path)?;
721
722 // Rename the .jnl file
723 let from_jnl_path = from_path.with_extension("jnl");
724 if from_jnl_path.exists() {
725 let to_jnl_path = to_path.with_extension("jnl");
726 std::fs::rename(from_jnl_path, to_jnl_path)?;
727 }
728
729 // Rename the directory if it exists (for child nodes)
730 let from_dir_path = from_path.with_extension("");
731 if from_dir_path.exists() && from_dir_path.is_dir() {
732 let to_dir_path = to_path.with_extension("");
733 std::fs::rename(from_dir_path, to_dir_path)?;
734 }
735 }
736 return Ok(())
737 }
738 _ => unreachable!(),
739 }
740 }
741 }
742 }
743 Err(anyhow!("Node {} not found", from))
744 }
745
746 pub fn delete_node(&mut self, identifier: &str) -> Result<()> {
747 if let Some(rule) = self.get_rule(identifier) {
748 // Save the 'from' workflow before renaming its node
749 if let Some(node) = rule.get_node() {
750 let workflow = node.get_workflow_like()?;
751 let node_path = workflow.path();
752
753 self.delete_rule(identifier);
754 // Remove associated files
755
756 // Remove the .aim file
757 if node_path.exists() {
758 std::fs::remove_file(&*node_path)?;
759 }
760
761 // Remove the .jnl file
762 let jnl_path = node_path.with_extension("jnl");
763 if jnl_path.exists() {
764 std::fs::remove_file(jnl_path)?;
765 }
766
767 // Remove the directory if it exists (for child nodes)
768 let dir_path = node_path.with_extension("");
769 if dir_path.exists() && dir_path.is_dir() {
770 std::fs::remove_dir_all(dir_path)?;
771 }
772 return Ok(())
773 }
774 }
775 return Err(anyhow!("Node {} not found", identifier));
776 }
777
778 pub fn add_instance(&mut self, identifier: Arc<str>, source: Arc<Reference>, target: Arc<Reference>) -> Result<()> {
779 self.check_identifier_unique(&identifier)?;
780 let instance = Instance::new(source, target);
781 let rule = Rule::new(
782 identifier,
783 Typedef::Instance,
784 Expression::Empty,
785 Arc::new(Value::Instance(instance))
786 );
787 Ok(self.append_or_update(Row::Rule(Arc::new(rule))))
788 }
789
790 /// Appends a row to the workflow or updates an existing rule.
791 ///
792 /// If a rule with the same identifier already exists, it is updated.
793 /// Otherwise, the rule is appended to the end of the workflow.
794 ///
795 /// # Parameters
796 ///
797 /// * `row` - The rule to append or update
798 pub fn append_or_update(&mut self, row: Row) {
799 match row {
800 Row::Rule(rule) | Row::Touched(rule) => {
801 // Update if the rule already exists
802 if self.lookup.contains_key(&rule.identifier()) {
803 // Rule update guaranteed because identifier exists
804 self.update_rule(rule).unwrap();
805 return;
806 }
807 if Aim::check_identifier(&rule.identifier()).is_ok() {
808 let index = self.rows.len();
809 self.lookup.insert(rule.identifier().clone(), index);
810 if self.track_changes {
811 self.set_partial_change();
812 self.rows.push(Row::Touched(rule));
813 } else {
814 self.rows.push(Row::Rule(rule));
815 }
816 }
817 }
818 _ => {
819 if self.track_changes {
820 self.set_version_change();
821 }
822 self.rows.push(row);
823 }
824 }
825 }
826
827 /// Updates an existing rule.
828 ///
829 /// # Parameters
830 ///
831 /// * `rule` - The updated rule
832 ///
833 /// # Returns
834 ///
835 /// `Ok(())` if successful, or an error if the rule doesn't exist.
836 pub fn update_rule(&mut self, rule: Arc<Rule>) -> Result<()> {
837 match self.lookup.get(&rule.identifier()) {
838 Some(&index) => {
839 if self.track_changes {
840 self.set_partial_change();
841 self.rows[index] = Row::Touched(rule);
842 } else {
843 self.rows[index] = Row::Rule(rule);
844 }
845 Ok(())
846 }
847 None => Err(anyhow!("Rule {} not found in {}", rule.identifier(), self.locator())),
848 }
849 }
850
851 /// Updates an existing rule value.
852 ///
853 /// # Parameters
854 ///
855 /// * `identifier` = The rule identifier
856 /// * `value` - The new value
857 ///
858 /// # Returns
859 ///
860 /// `Ok(())` if successful, or an error if the rule doesn't exist.
861 pub fn update_value(&mut self, identifier: &str, value: Arc<Value>) -> Result<()> {
862 if let Some(&index) = self.lookup.get(identifier) {
863 let arc_rule = self.rows[index].rule().unwrap();
864 if arc_rule.value().as_ref() != &*value {
865 let mut new_rule = (*arc_rule).clone();
866 let _ = new_rule.set_value(value);
867 if self.track_changes {
868 self.set_partial_change();
869 self.rows[index] = Row::Touched(Arc::new(new_rule));
870 } else {
871 self.rows[index] = Row::Rule(Arc::new(new_rule));
872 }
873 }
874 Ok(())
875 } else {
876 Err(anyhow!("Rule {} not found in {}", identifier, self.locator()))
877 }
878 }
879
880 /// Deletes a rule by identifier.
881 ///
882 /// # Parameters
883 ///
884 /// * `identifier` - The rule identifier to delete
885 ///
886 /// # Returns
887 ///
888 /// `Some(Rule)` if the rule was deleted, `None` if it didn't exist.
889 pub fn delete_rule(&mut self, identifier: &str) -> Row {
890 match self.lookup.remove(identifier) {
891 Some(index) => {
892 self.set_version_change();
893 let row = self.rows.remove(index);
894 self.reindex();
895 row
896 }
897 None => Row::Empty,
898 }
899 }
900
901 // --- Indexed Rule Access --
902
903 /// Sets a rule at a specific row index.
904 ///
905 /// If a rule already exists at the specified index with a different identifier,
906 /// this method will return an error. If the index is beyond the current size
907 /// of the workflow, the rows vector will be expanded to accommodate it.
908 ///
909 /// # Parameters
910 ///
911 /// * `index` - The zero-based row index where to place the rule
912 /// * `row` - The rule to set, or None to create an empty row
913 ///
914 /// # Returns
915 ///
916 /// `Ok(())` if successful, or an error if there's a conflict with an existing rule
917 pub fn set_row(&mut self, index: usize, row: Row) -> Result<()> {
918 if let Row::Rule(rule) | Row::Touched(rule) = row {
919 Aim::check_identifier(&rule.identifier())?;
920 // Check if the rule identifier already exists
921 if self.lookup.contains_key(&rule.identifier()) {
922 if index < self.rows.len() {
923 // Get the rule at the specified index
924 match &self.rows[index] {
925 Row::Rule(old_rule) | Row::Touched(old_rule) => {
926 // Ensure the new rule and old rule identifiers match
927 if rule.identifier() != old_rule.identifier() {
928 return Err(anyhow!("Rule mismatch {} at {}[{}], found {}", rule.identifier(), self.locator(), index, old_rule.identifier()));
929 }
930 // Manage this special case
931 if self.track_changes {
932 self.set_partial_change();
933 self.rows[index] = Row::Touched(rule);
934 } else {
935 self.rows[index] = Row::Rule(rule);
936 }
937 return Ok(());
938 }
939 _ => {
940 return Err(anyhow!("No Rule {} at {}[{}]", rule.identifier(), self.locator(), index));
941 }
942 }
943 } else {
944 return Err(anyhow!("Rule {} is missing at {}[{}]", rule.identifier(), self.locator(), index));
945 }
946 }
947 // Expand vector if needed
948 if index >= self.rows.len() {
949 self.rows.resize_with(index + 1, || Row::Empty);
950 }
951 // Insert at specified position
952 self.lookup.insert(rule.identifier().clone(), index);
953 if self.track_changes {
954 self.set_partial_change();
955 self.rows[index] = Row::Touched(rule);
956 } else {
957 self.rows[index] = Row::Rule(rule);
958 }
959 } else {
960 // Expand vector if needed
961 if index >= self.rows.len() {
962 self.rows.resize_with(index + 1, || Row::Empty);
963 }
964 if self.track_changes {
965 self.set_partial_change();
966 }
967 self.rows[index] = row;
968 }
969 Ok(())
970 }
971
972 /// Inserts a rule at a specific row index.
973 ///
974 /// This method inserts a rule at the specified index, shifting existing rules
975 /// to higher indices. If the index is beyond the current size of the workflow,
976 /// empty rows will be created as needed. If a rule with the same identifier
977 /// already exists in the workflow, this method returns an error.
978 ///
979 /// # Parameters
980 ///
981 /// * `index` - The zero-based row index where to insert the rule
982 /// * `row` - The rule to insert, or None to create an empty row
983 ///
984 /// # Returns
985 ///
986 /// `Ok(())` if successful, or an error if a rule with the same identifier already exists
987 pub fn insert_row(&mut self, index: usize, row: Row) -> Result<()> {
988 if let Row::Rule(rule) | Row::Touched(rule) = &row {
989 Aim::check_identifier(&rule.identifier())?;
990 if self.lookup.contains_key(&rule.identifier()) {
991 return Err(anyhow!("Rule {} exists in {}", rule.identifier(), self.locator));
992 }
993 if index >= self.rows.len() {
994 // Expand vector if needed
995 if index > self.rows.len() {
996 self.rows.resize_with(index, || Row::Empty);
997 }
998 // and append
999 self.lookup.insert(rule.identifier().clone(), index);
1000 self.rows.push(row);
1001 } else {
1002 // Insert at specified position
1003 self.lookup.insert(rule.identifier().clone(), index);
1004 self.rows.insert(index, row);
1005 }
1006 } else if index >= self.rows.len() {
1007 // Expand vector if needed
1008 if index > self.rows.len() {
1009 self.rows.resize_with(index, || Row::Empty);
1010 }
1011 // and append
1012 self.rows.push(row);
1013 } else {
1014 self.rows.insert(index, row);
1015 }
1016
1017 // Update indices of all rows
1018 self.reindex();
1019 self.set_version_change();
1020 Ok(())
1021 }
1022
1023 /// Repositions a rule from one row index to another.
1024 ///
1025 /// This method moves a rule from the `from` index to the `to` index,
1026 /// shifting other rules as needed. If either index is beyond the current
1027 /// size of the workflow, the rows vector will be expanded to accommodate them.
1028 ///
1029 /// # Parameters
1030 ///
1031 /// * `from` - The current zero-based row index of the rule to move
1032 /// * `to` - The target zero-based row index where to place the rule
1033 ///
1034 /// # Returns
1035 ///
1036 /// `Ok(())` if successful, or an error if the operation fails
1037 pub fn reposition_row(&mut self, from: usize, to: usize) -> Result<()> {
1038 if from != to {
1039 // Expand vectors if needed
1040 let max_row = from.max(to);
1041 if max_row >= self.rows.len() {
1042 self.rows.resize_with(max_row + 1, || Row::Empty);
1043 }
1044 self.set_partial_change();
1045
1046 // Check if there's actually a rules to move
1047 if self.rows[from].is_empty() && self.rows[to].is_empty() {
1048 return Ok(());
1049 }
1050
1051 let row = self.rows.remove(from);
1052 self.rows.insert(to, row);
1053
1054 // Rebuild index mappings due to reordering
1055 self.reindex();
1056 }
1057 Ok(())
1058 }
1059
1060 pub fn replace_row(&mut self, index: usize, row: Row) -> Result<()> {
1061 if let Row::Rule(new_rule) | Row::Touched(new_rule) = row {
1062 Aim::check_identifier(&new_rule.identifier())?;
1063 // Check if the rule identifier already exists
1064 if self.lookup.contains_key(&new_rule.identifier()) {
1065 if index < self.rows.len() {
1066 // Get the rule at the specified index
1067 match &self.rows[index] {
1068 // Check if the old row is a rule
1069 Row::Rule(old_rule) | Row::Touched(old_rule) => {
1070 // Ensure the new rule and old rule identifiers match
1071 if new_rule.identifier() != old_rule.identifier() {
1072 return Err(anyhow!("Rule {} already exists in {}", new_rule.identifier(), self.locator()));
1073 }
1074 // Manage changes
1075 if self.track_changes {
1076 self.set_partial_change();
1077 self.rows[index] = Row::Touched(new_rule);
1078 } else {
1079 self.rows[index] = Row::Rule(new_rule);
1080 }
1081 return Ok(());
1082 }
1083 _ => {}
1084 }
1085 } else {
1086 return Err(anyhow!("Rule {} is missing at {}[{}]", new_rule.identifier(), self.locator(), index));
1087 }
1088 }
1089 // Expand vector if needed
1090 if index >= self.rows.len() {
1091 self.rows.resize_with(index + 1, || Row::Empty);
1092 }
1093 // Insert at specified position
1094 self.lookup.insert(new_rule.identifier().clone(), index);
1095 if self.track_changes {
1096 self.set_partial_change();
1097 self.rows[index] = Row::Touched(new_rule);
1098 } else {
1099 self.rows[index] = Row::Rule(new_rule);
1100 }
1101 } else {
1102 // Expand vector if needed
1103 if index >= self.rows.len() {
1104 self.rows.resize_with(index + 1, || Row::Empty);
1105 }
1106 if self.track_changes {
1107 self.set_partial_change();
1108 }
1109 self.rows[index] = row;
1110 }
1111 self.reindex();
1112 Ok(())
1113 }
1114
1115
1116 /// Clears a row at a specific index, removing the rule if present.
1117 ///
1118 /// This method removes the rule at the specified index but keeps the row
1119 /// structure intact. The row will become empty but still exist in the workflow.
1120 /// If the index is out of bounds, this method returns None.
1121 ///
1122 /// # Parameters
1123 ///
1124 /// * `index` - The zero-based row index to clear
1125 ///
1126 /// # Returns
1127 ///
1128 /// `Some(Rule)` if a rule was present and removed, `None` if the row was already empty or index was out of bounds
1129 pub fn clear_row(&mut self, index: usize) -> Row {
1130 // Check bounds
1131 if index >= self.rows.len() {
1132 return Row::Empty;
1133 }
1134 self.set_version_change();
1135 let row = self.rows[index].clone();
1136 self.rows[index] = Row::Empty;
1137 if let Row::Rule(rule) | Row::Touched(rule) = &row {
1138 self.lookup.remove(&rule.identifier());
1139 }
1140 row
1141 }
1142
1143 /// Removes a row at a specific index, removing the rule and shrinking the workflow.
1144 ///
1145 /// This method removes the rule at the specified index and removes the row
1146 /// entirely from the workflow, shifting subsequent rows to lower indices.
1147 /// If the index is out of bounds, this method returns None.
1148 ///
1149 /// # Parameters
1150 ///
1151 /// * `index` - The zero-based row index to remove
1152 ///
1153 /// # Returns
1154 ///
1155 /// `Some(Rule)` if a rule was present and removed, `None` if the index was out of bounds
1156 pub fn remove_row(&mut self, index: usize) -> Row {
1157 if index >= self.rows.len() {
1158 return Row::Empty;
1159 }
1160 self.set_version_change();
1161 let row = self.rows.remove(index);
1162 self.reindex();
1163 row
1164 }
1165
1166 /// Create an iterator over the rules with their indices.
1167 ///
1168 /// This iterator yields tuples of `(index, &Option<Rule>)`, providing
1169 /// both the position and content of each row including empty ones.
1170 pub fn iter_with_rows(&self) -> impl Iterator<Item = (usize, &Row)> {
1171 self.rows
1172 .iter()
1173 .enumerate()
1174 .map(|(index, row)| (index, row))
1175 }
1176
1177 /// Rebuilds the lookup mapping index.
1178 ///
1179 /// This helper function rebuilds the internal hash map that maps rule identifiers
1180 /// to their row indices. It's called automatically after operations that change
1181 /// the position of rules within the workflow.
1182 pub fn reindex(&mut self) {
1183 self.lookup.clear();
1184 for (index, row) in self.rows.iter().enumerate() {
1185 if let Row::Rule(rule) | Row::Touched(rule) = row {
1186 self.lookup.insert(rule.identifier().clone(), index);
1187 }
1188 }
1189 }
1190
1191 /// Parses AIM content and populates the workflow.
1192 ///
1193 /// This method parses AIM format content and creates rules based on the parsed data.
1194 /// It can optionally parse only up to a specific partial version. The method clears
1195 /// the current workflow contents before parsing.
1196 ///
1197 /// # Special tokens and their meanings
1198 ///
1199 /// - `[` .. `]` HEADER: Used to signify a version header.
1200 /// - `:` PARTIAL: Used inside a workflow header to signify a minor version number.
1201 /// - `@` DATETIME: Used inside a workflow header to signify the snapshot date and time.
1202 /// - `#` COMMENT: Used to signify a comment.
1203 /// - `$` CONSTANT: Used inside rule a definitions to signify a constant variable.
1204 /// - `(` .. `)` Delimits expression parenthesis
1205 /// - `{` .. `}` Delimits and expression collection
1206 /// - `"` | `'` Delimits strings
1207 /// - `^`, `!`, `&`, `%`, `*` Potentially available
1208 ///
1209 /// # Parameters
1210 ///
1211 /// * `input` - The AIM content to parse
1212 /// * `partial` - Optional partial version to parse up to
1213 ///
1214 /// # Returns
1215 ///
1216 /// `Ok(())` if parsing was successful, or an error if parsing failed
1217 pub fn parse(&mut self, input: &str, partial: Option<u32>) -> Result<()> {
1218 // Clear contents
1219 self.rows.clear();
1220 self.lookup.clear();
1221 self.clear_changes();
1222
1223 let lines: Vec<&str> = input.lines().collect();
1224 let mut is_version = false;
1225 let mut is_partial = false;
1226
1227 for line in lines {
1228 let mut opt_index: Option<u32> = None;
1229 let mut line = line.trim();
1230 // Append empty lines to maintain row spacing in first (non-partial) section
1231 if line.is_empty() {
1232 if is_version && self.version.partial() == 0 {
1233 self.append_or_update(Row::Empty);
1234 }
1235 continue;
1236 }
1237
1238 // Try to parse comment (lines starting with #)
1239 if line.starts_with('#') {
1240 if is_version && self.version.partial() == 0 {
1241 self.append_or_update(Row::Comment(Arc::from(line)));
1242 }
1243 continue;
1244 }
1245
1246 // Try to parse version headers (lines starting with [)
1247 if line.starts_with('[') {
1248 match parse_version(line) {
1249 Ok((_, current)) => {
1250 // When version/epoch is 0, assign the first version header encountered
1251 if self.version.epoch() == 0 {
1252 is_version = true;
1253 self.version = current;
1254 }
1255 // When epoch is known, merge partials
1256 else if self.version.epoch() == current.epoch() {
1257 is_version = true;
1258 // We've moved past the target partial, stop parsing
1259 if is_partial {
1260 break;
1261 }
1262 self.version.set_partial(current.partial());
1263 self.version.set_date(current.date());
1264 if let Some(opt_partial) = partial {
1265 if opt_partial == current.partial() {
1266 is_partial = true;
1267 }
1268 }
1269 } else if is_version {
1270 // We've moved past the target version, stop parsing
1271 break;
1272 }
1273 }
1274 // Skip lines with syntax errors
1275 Err(_e) => {}
1276 }
1277 continue;
1278 }
1279
1280 // If we haven't found the target version yet, skip this line
1281 if !is_version {
1282 continue;
1283 }
1284
1285 // Try to parse leading number (rule index used by partial saves)
1286 if line.chars().next().map_or(false, |c| c.is_ascii_digit()) {
1287 match parse_unsigned(line) {
1288 Ok((remainder, index)) => {
1289 opt_index = Some(index);
1290 line = remainder;
1291 }
1292 Err(_) => {}
1293 }
1294 }
1295
1296 // Try to parse rule
1297 match parse_rule(line) {
1298 Ok(rule) => {
1299 // If this is a node-typed rule, synthesize a lazy Node value now that
1300 // we know the workflow locator. This keeps parsing simple while
1301 // ensuring node rules always carry a concrete Node reference.
1302 if rule.typedef().is_node() {
1303 if let Some(node) = rule.get_node() {
1304 // Derive a child reference from the workflow locator + rule identifier.
1305 // Reference::new uses the identifier as a segment; append composes
1306 // a hierarchical locator like `parent.child`.
1307 let reference = self.locator.add_child(rule.identifier())?;
1308 // Initialize the node as Unloaded
1309 node.init(Arc::new(reference));
1310 }
1311 }
1312
1313 match opt_index {
1314 Some(index) => {
1315 // Set rule at index
1316 if self.contains(&rule.identifier()) {
1317 let _ = self.update_rule(Arc::new(rule));
1318 } else {
1319 let _ = self.set_row(index as usize, Row::Rule(Arc::new(rule)));
1320 }
1321 }
1322 None => {
1323 // Append or overwrite existing rule
1324 self.append_or_update(Row::Rule(Arc::new(rule)));
1325 }
1326 }
1327 }
1328 Err(_) => {
1329 // Ignore syntax errors, but maintain row spacing in (non-partial) sections
1330 if self.version.partial() == 0 {
1331 self.append_or_update(Row::Empty);
1332 }
1333 }
1334 }
1335 }
1336
1337 if !is_version && self.version.epoch() > 0 {
1338 return Err(anyhow!("Version {} not found", self.version.epoch()));
1339 }
1340
1341 Ok(())
1342 }
1343
1344 pub fn print(&self, writer: &mut Writer) {
1345 if writer.is_formulizer() {
1346 self.version.print(writer);
1347 }
1348 for row in self.iter_rows() {
1349 row.print(writer);
1350 }
1351 }
1352
1353 /// Return the formula-string representation (round-trippable by the parser).
1354 pub fn to_formula(&self) -> String {
1355 let mut writer = Writer::formulizer();
1356 self.print(&mut writer);
1357 writer.finish()
1358 }
1359}
1360
1361impl WriterLike for Workflow {
1362 fn write(&self, writer: &mut Writer) {
1363 self.print(writer);
1364 }
1365}
1366
1367impl fmt::Display for Workflow {
1368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1369 write!(f, "{}", self.to_stringized())
1370 }
1371}