aimx/aim/read.rs
1//! Journaled workflow reading utilities
2//!
3//! This module provides functions for reading specific versions of AIM files
4//! based on their journal entries. It enables efficient access to historical
5//! versions without parsing the entire file.
6//!
7//! # Overview
8//!
9//! The reading utilities work in conjunction with the journal system to:
10//! - Locate specific versions of AIM files using byte offsets
11//! - Read the latest version of a workflow
12//! - Read any specific version by its epoch number
13//!
14//! These functions are designed to work with the [`Journal`] entries that map
15//! version numbers to byte positions in AIM files, enabling efficient random
16//! access to specific versions.
17//!
18//! # Examples
19//!
20//! ```no_run
21//! use std::path::Path;
22//! use aimx::aim::{Journal, journal_load, read_latest, read_version};
23//!
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! let mut journals = Vec::new();
26//! let path = Path::new("workflow.aim");
27//!
28//! // Load existing journal entries
29//! journal_load(&mut journals, path)?;
30//!
31//! // Read the latest version
32//! let latest_content = read_latest(&mut journals, path)?;
33//!
34//! // Read a specific version
35//! let version_content = read_version(&mut journals, path, 2)?;
36//! # Ok(())
37//! # }
38//! ```
39
40use anyhow::{anyhow, Result};
41use crate::aim::{Journal, journal_load};
42use std::{
43 fs::File,
44 io::{Read, Seek, SeekFrom},
45 path::Path,
46};
47
48/// Reads the contents of the latest version section of an AIM file based on its journal entries.
49///
50/// This function retrieves the content of the most recent complete version in an AIM file
51/// by using the journal entries to locate and read from the appropriate byte position.
52/// It reads from the start position of the latest journal entry to the end of the file.
53///
54/// # Arguments
55///
56/// * `journals` - A mutable reference to a vector that will be populated with journal entries.
57/// This vector is cleared and populated with entries from the journal file.
58/// * `aim_path` - The path to the AIM file from which to read the latest version content.
59///
60/// # Returns
61///
62/// * `Ok(String)` - The content of the latest version section as a String
63/// * `Err` - An error if the file cannot be read, if there are no journal entries,
64/// or if there are UTF-8 conversion issues
65///
66/// # Errors
67///
68/// This function will return an error in the following cases:
69/// * The AIM file cannot be opened or read
70/// * The journal file cannot be loaded or parsed
71/// * There are no journal entries in the file (empty journal)
72/// * UTF-8 conversion fails when reading the file content
73///
74/// # Examples
75///
76/// ```no_run
77/// use std::path::Path;
78/// use aimx::aim::{Journal, journal_load, read_latest};
79///
80/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
81/// let mut journals = Vec::new();
82/// let path = Path::new("workflow.aim");
83///
84/// // Read the latest version content
85/// let latest_content = read_latest(&mut journals, path)?;
86/// println!("Latest version content: {}", latest_content);
87/// # Ok(())
88/// # }
89/// ```
90pub fn read_latest(journals: &mut Vec<Journal>, aim_path: &Path) -> Result<String> {
91 // Load the journal
92 journal_load(journals, aim_path)?;
93
94 // If no entries, return None
95 if journals.is_empty() {
96 return Err(anyhow!("Version {} not found", 0));
97 }
98
99 // Get the latest (last) journal entry
100 let latest = &journals[journals.len() - 1];
101
102 // Open the AIM file
103 let mut file = File::open(aim_path)?;
104
105 // Seek to the position of the last entry
106 file.seek(SeekFrom::Start(latest.position()))?;
107
108 // Read the rest of the file
109 let mut contents = String::new();
110 file.read_to_string(&mut contents)?;
111
112 Ok(contents)
113}
114
115/// Reads the contents of a specific version of an AIM file based on its journal entries.
116///
117/// This function retrieves the content of a specific version in an AIM file by using
118/// the journal entries to locate and read from the appropriate byte positions. It reads
119/// from the start position of the requested version up to (but not including) the start
120/// position of the next version, or to the end of the file if it's the latest version.
121///
122/// # Arguments
123///
124/// * `journals` - A mutable reference to a vector that will be populated with journal entries.
125/// This vector is cleared and populated with entries from the journal file.
126/// * `aim_path` - The path to the AIM file from which to read the specific version content.
127/// * `version` - The epoch number of the version to read. This corresponds to the major
128/// version number in the AIM file's version headers (e.g., `[1]`, `[2]`, etc.)
129///
130/// # Returns
131///
132/// * `Ok(String)` - The content of the specified version as a String
133/// * `Err` - An error if the file cannot be read, if the requested version is not found,
134/// or if there are UTF-8 conversion issues
135///
136/// # Errors
137///
138/// This function will return an error in the following cases:
139/// * The AIM file cannot be opened or read
140/// * The journal file cannot be loaded or parsed
141/// * The requested version is not found in the journal entries
142/// * UTF-8 conversion fails when reading the file content
143///
144/// # Examples
145///
146/// ```no_run
147/// use std::path::Path;
148/// use aimx::aim::{Journal, journal_load, read_version};
149///
150/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
151/// let mut journals = Vec::new();
152/// let path = Path::new("workflow.aim");
153///
154/// // Load existing journal entries
155/// journal_load(&mut journals, path)?;
156///
157/// // Read a specific version (e.g., version 2)
158/// let version_content = read_version(&mut journals, path, 2)?;
159/// println!("Version 2 content: {}", version_content);
160/// # Ok(())
161/// # }
162/// ```
163pub fn read_version(journals: &mut Vec<Journal>, aim_path: &Path, version: u32) -> Result<String> {
164 // Load the journal
165 journal_load(journals, aim_path)?;
166
167 // If no entries, return None
168 if journals.is_empty() {
169 return Err(anyhow!("Version {} not found", version));
170 }
171
172 // Find the journal entry for the requested version
173 let mut start_pos: Option<u64> = None;
174 let mut end_pos: Option<u64> = None;
175
176 // Direct lookup optimization
177 if version > 0
178 && version < journals.len() as u32
179 && version == journals[version as usize - 1].version()
180 {
181 start_pos = Some(journals[version as usize - 1].position());
182 if version == journals.len() as u32 - 2 {
183 end_pos = None; // Read to end of file
184 } else {
185 // Otherwise, read until the next entry
186 end_pos = Some(journals[version as usize].position());
187 }
188 } else {
189 // Slower scan for the exact version match
190 for (i, journal) in journals.iter().enumerate() {
191 if journal.version() == version {
192 start_pos = Some(journal.position());
193 // If this is the last entry, read to the end of the file
194 if i == journals.len() - 1 {
195 end_pos = None; // Read to end of file
196 } else {
197 // Otherwise, read until the next entry
198 end_pos = Some(journals[i + 1].position());
199 }
200 break;
201 }
202 }
203 }
204
205 // If we didn't find the version, return None
206 if start_pos.is_none() {
207 return Err(anyhow!("Version {} not found", version));
208 }
209
210 // Open the AIM file
211 let mut file = File::open(aim_path)?;
212
213 // Seek to the start position
214 file.seek(SeekFrom::Start(start_pos.unwrap()))?;
215
216 // Read the section
217 let mut contents = String::new();
218
219 if let Some(end) = end_pos {
220 // Calculate the length to read
221 let length = end - start_pos.unwrap();
222 contents.reserve(length as usize);
223
224 // Read exactly the specified number of bytes
225 let mut buffer = vec![0; length as usize];
226 file.read_exact(&mut buffer)?;
227 contents = String::from_utf8(buffer)
228 .map_err(|e| anyhow!("UTF-8 error: {:?}", e))?;
229 } else {
230 // Read to the end of the file
231 file.read_to_string(&mut contents)?;
232 }
233
234 Ok(contents)
235}