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 > ¤t_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 > ¤t_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}