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}