bin_efo/commands/
epoch.rs

1//! # `epoch` sub-command
2//!
3//! The `epoch` sub‑command is the single entry point for all epoch‑related
4//! operations in **efo**.  
5//! An *epoch* represents a point in the simulated world’s history.  
6//! The command lets you inspect, advance, modify and sanity‑check the epoch
7//! timeline.
8//!
9//! ### Usage
10//!
11//! ```bash
12//! # Show log of epochs (last 20 by default)
13//! efo epoch log --limit 20
14//!
15//! # Switch to an existing epoch
16//! efo epoch switch 5
17//!
18//! # Create the next epoch if none exist in the future
19//! efo epoch next
20//!
21//! # Delete all future epochs, returning to the present
22//! efo epoch reset
23//!
24//! # Add a unique tag to the current epoch
25//! efo epoch tag milestone1
26//!
27//! # Replace the current epoch’s summary
28//! efo epoch summary "Reached the new frontier"
29//!
30//! # (Future) Check consistency
31//! efo epoch fsck
32//! ```
33//!
34//! ### Summary
35//!
36//! | Sub‑command | Usage | What it changes |
37//! |-------------|-------|-----------------|
38//! | `log` | `efo epoch log [--limit N]` | Prints the chronological list of epochs up to the current one. |
39//! | `switch` | `efo epoch switch <id>` | Sets the current epoch to the specified `<id>` (must already exist). |
40//! | `next` | `efo epoch next` | Generates the next scheduled epoch and inserts it. |
41//! | `reset` | `efo epoch reset` | Deletes all epochs with an identifier greater than the current one. |
42//! | `diff` | `efo epoch diff <id1> <id2>` | *Unimplemented* – placeholder for future diffing logic. |
43//! | `tag` | `efo epoch tag <TAG>` | Adds a unique tag to the current epoch, updating the global tag map. |
44//! | `fsck` | `efo epoch fsck` | *Unimplemented* – intended to run a consistency check. |
45//! | `summary` | `efo epoch summary <text>` | Replaces the `summary` field of the current epoch with `<text>`. |
46//!
47
48#![warn(missing_docs, clippy::missing_docs_in_private_items)]
49
50use crate::{
51    error::AppError,
52    success::{SuccessMessages, display_success},
53};
54use lib_efo::{
55    epoch::{EpochConfig, EpochId, epoch_traits::ConfigWritable},
56    error::EpochError,
57};
58use log::{debug, info};
59
60pub mod cli_epoch;
61
62/// **Public entry point used by `efo::main`** - dispatches to the real
63/// handler that lives in `crate::world::EpochManager`.
64///
65/// # Arguments
66/// * `cmd` - The command to execute. This is an enum that represents
67///   one of the possible sub-commands for the `epoch` feature.
68///
69/// # Returns
70/// * `Ok(())` on success.
71/// * `Err(AppError)` if any step of the command handling fails.
72///
73/// # Examples
74/// ```no_run
75/// // Create a command to switch to epoch 3.
76/// let cmd = cli_epoch::EpochCmd::Switch(cli_epoch::Switch { target: 3 });
77/// // Execute the command.
78/// let result = handle_cmd(cmd);
79/// assert!(result.is_ok());
80/// ```
81///
82/// The function never panics under normal operation. All errors are
83/// reported via the returned `Result`.
84pub fn handle_cmd(cmd: &cli_epoch::EpochCmd) -> Result<(), AppError> {
85    let mut main_epoch = EpochConfig::new_from_path(None)?;
86    // TODO(2): Remove this debug
87    debug!("The main epoch befor any action is {:#?}", main_epoch);
88    match cmd {
89        cli_epoch::EpochCmd::Log(args) => handle_log(args, &mut main_epoch),
90        cli_epoch::EpochCmd::Switch(args) => handle_switch(args, &mut main_epoch),
91        cli_epoch::EpochCmd::Next(args) => handle_next(args, &mut main_epoch),
92        cli_epoch::EpochCmd::Reset(args) => handle_reset(args, &mut main_epoch),
93        cli_epoch::EpochCmd::Diff(args) => handle_diff(args),
94        cli_epoch::EpochCmd::Tag(args) => handle_taging(args, &mut main_epoch),
95        cli_epoch::EpochCmd::Fsck(args) => handle_fsck(args, &mut main_epoch),
96        cli_epoch::EpochCmd::Summary(args) => handle_summary(args, &mut main_epoch),
97    }?;
98
99    info!("The main epoch after all actions is {:#?}", main_epoch);
100    Ok(())
101}
102
103/// Edits the summary for the current epoch.
104///
105/// The function loads the epoch that is currently active in
106/// `main_epoch`, then replaces its `summary` field with the value
107/// supplied in `args.summary`. The epoch map in `main_epoch` is
108/// modified in place.
109///
110/// This helper is internal to the `epoch` sub-command and is not part
111/// of the public API.
112fn handle_summary(args: &cli_epoch::Summary, main_epoch: &mut EpochConfig) -> Result<(), AppError> {
113    let epoch_id = main_epoch.epoch_id;
114    main_epoch.try_load(&epoch_id)?;
115    #[allow(clippy::unwrap_used)] // The epoch_id must exists at this point
116    if let Some(epoch) = main_epoch
117        .list_existing_epochs
118        .get_mut(&epoch_id)
119        .ok_or(EpochError::EpochHashingImpossible(file!(), line!()))?
120    {
121        epoch.summary = args.summary.clone();
122        epoch.save_with_context("Saving new summary", &lib_efo::epoch::SaveMode::Erase)?;
123        display_success(&SuccessMessages::ChangedSummary(
124            epoch_id,
125            args.summary.clone(),
126        ));
127    }
128
129    Ok(())
130}
131
132/// Show a diff between two epochs.
133///
134/// Currently unimplemented. The function is a placeholder until a
135/// world-level diff algorithm is available.
136///
137/// This helper is internal to the `epoch` sub-command and is not part
138/// of the public API.
139fn handle_diff(args: &cli_epoch::Diff) -> Result<(), AppError> {
140    let _ = args;
141    unimplemented!("Blocker: There is actually no way to diff the world yet")
142}
143
144/// Show a log of all epochs up to the current one.
145///
146/// The function collects the identifiers of all epochs that are
147/// less than or equal to the currently active epoch (`main_epoch.epoch_id`),
148/// sorts them, and then asks the configuration to log the elements
149/// up to the supplied limit.  The log is written by the
150/// underlying `EpochConfig::log_elements` implementation.
151///
152/// This helper is internal to the `epoch` sub-command.
153fn handle_log(args: &cli_epoch::Log, main_epoch: &mut EpochConfig) -> Result<(), AppError> {
154    let limit = args.limit;
155    let start_id = &main_epoch.epoch_id;
156
157    let mut list_id: Vec<EpochId> = main_epoch
158        .list_existing_epochs
159        .iter()
160        .filter(|(id, _)| id <= &start_id)
161        .map(|(id, _)| *id)
162        .collect();
163    list_id.sort();
164    main_epoch.log_elements(&list_id[..std::cmp::min(list_id.len(), limit)])?;
165    Ok(())
166}
167
168/// Switches the current epoch to the one specified by the user.
169///
170/// The function loads the target epoch, updates the epoch identifier
171/// in the configuration, and persists the change.  It also prints a
172/// success message via `display_success`.
173///
174/// This helper is internal to the `epoch` sub-command.
175fn handle_switch(args: &cli_epoch::Switch, main_epoch: &mut EpochConfig) -> Result<(), AppError> {
176    let target = main_epoch.get_epoch_id(&args.target)?;
177    main_epoch.try_load(&target)?;
178    main_epoch.epoch_id = target;
179
180    main_epoch.save_with_context(
181        "Saving modified epoch config",
182        &lib_efo::epoch::SaveMode::Erase,
183    )?;
184    display_success(&SuccessMessages::EpochSwitchSuccess(target));
185    Ok(())
186}
187
188/// Handle the `next` sub-command.
189///
190/// Advances the epoch to the next scheduled event.  If the current
191/// configuration contains any epochs with an identifier larger than
192/// the current one, an error is returned - otherwise the next epoch
193/// is computed.
194///
195/// # Arguments
196/// * `main_epoch` - mutable reference to the current `EpochConfig`
197///   (the `cli_epoch::Next` argument is unused).
198///
199/// # Returns
200/// * `Ok(())` if the next epoch was successfully computed.
201/// * `Err(AppError::Epoch(EpochError::FutureEpochExist))` if there are
202///   no future epochs to advance to.
203fn handle_next(_args: &cli_epoch::Next, main_epoch: &mut EpochConfig) -> Result<(), AppError> {
204    let current_id = main_epoch.epoch_id;
205
206    // Check that there are no future epochs
207    if main_epoch
208        .list_existing_epochs
209        .keys()
210        .any(|id| id > &current_id)
211    {
212        Err(AppError::Epoch(EpochError::FutureEpochExist))
213    } else {
214        let new_epoch = main_epoch.compute_next()?;
215        let new_id = new_epoch.epoch_id;
216
217        // Insert new epoch
218        main_epoch.insert_and_save(new_epoch, &lib_efo::epoch::SaveMode::DoNotErase)?;
219        display_success(&SuccessMessages::EpochNextSuccess(current_id, new_id));
220        Ok(())
221    }
222}
223
224/// Handle the `reset` sub-command.
225///
226/// Deletes all epochs whose identifier is greater than the current one,
227/// effectively rolling back to the present epoch.
228///
229/// # Arguments
230/// * `main_epoch` - mutable reference to the current `EpochConfig`
231///   (the `cli_epoch::Reset` argument is unused).
232///
233/// # Returns
234/// * `Ok(())` if all future epochs were successfully removed.
235/// * `Err(AppError)` if any deletion fails.
236fn handle_reset(args: &cli_epoch::Reset, main_epoch: &mut EpochConfig) -> Result<(), AppError> {
237    let _ = args;
238    let current_id = main_epoch.epoch_id;
239    // for id in main_epoch
240    for id in main_epoch
241        .list_existing_epochs
242        .keys()
243        .filter(|&k| k > &current_id)
244        .copied()
245        // Middle step necessary for borrowing purposes
246        .collect::<Vec<_>>()
247    {
248        main_epoch.delete_id(id)?
249    }
250
251    display_success(&SuccessMessages::DeletedAllFutureEvents(current_id));
252    Ok(())
253}
254
255/// Handle the `tag` sub-command.
256///
257/// Adds a tag to the current epoch.  The tag must not already exist
258/// elsewhere in the configuration; otherwise an error is returned.
259///
260/// # Arguments
261/// * `args` - `cli_epoch::Tag` containing the tag to add.
262/// * `main_epoch` - mutable reference to the current `EpochConfig`.
263///
264/// # Returns
265/// * `Ok(())` if the tag was successfully added.
266/// * `Err(AppError::Epoch(EpochError::TagAlreadyExists))` if the tag
267///   already exists in another epoch.
268/// * `Err(AppError::Epoch(EpochError::Unknown))` if the epoch cannot
269///   be found or saved.
270fn handle_taging(args: &cli_epoch::Tag, main_epoch: &mut EpochConfig) -> Result<(), AppError> {
271    // Load into memory the current epoch
272    main_epoch.try_load(&main_epoch.epoch_id.clone())?;
273
274    // Check if the tag doens't exists
275    if main_epoch.tags.contains_key(&args.tag) {
276        Err(AppError::Epoch(EpochError::TagAlreadyExists(
277            main_epoch.epoch_id,
278            args.tag.clone(),
279        )))?;
280    }
281
282    // Modify the epoch to add the tag inside
283    #[allow(clippy::unwrap_used)]
284    if let Some(current_epoch_info) = main_epoch
285        .list_existing_epochs
286        .get_mut(&main_epoch.epoch_id)
287        .ok_or(EpochError::EpochHashingImpossible(file!(), line!()))?
288    {
289        current_epoch_info.tags.push(args.tag.clone());
290        current_epoch_info
291            .save_with_context("Saving modified epoch", &lib_efo::epoch::SaveMode::Erase)?;
292        main_epoch
293            .tags
294            .insert(args.tag.clone(), main_epoch.epoch_id);
295        main_epoch.save_with_context(
296            "Saving modified main config",
297            &lib_efo::epoch::SaveMode::Erase,
298        )?;
299        Ok(())
300    } else {
301        Err(AppError::Epoch(lib_efo::error::EpochError::Unknown(
302            format!("Couldn't add the tag {}", args.tag),
303        )))
304    }
305}
306
307/// Perform a filesystem consistency check on the epoch data.
308///
309/// The function invokes the internal consistency-check logic of
310/// `EpochConfig`.  It is intended for use from the `fsck` sub-command
311/// and does **not** modify the current epoch.  Any problems that
312/// are detected are reported through the returned `Result`; the
313/// command may also print a diagnostic message to the user.
314///
315/// This helper is internal to the `epoch` sub-command.
316fn handle_fsck(args: &cli_epoch::Fsck, main_epoch: &mut EpochConfig) -> Result<(), AppError> {
317    let _ = main_epoch;
318    let _ = args;
319    unimplemented!()
320}