Simplify the state file

This commit is contained in:
mo8it 2024-04-14 05:13:27 +02:00
parent 9831cbb139
commit 9dcc4b7df5
6 changed files with 86 additions and 169 deletions

2
.gitignore vendored
View file

@ -4,7 +4,7 @@ target/
/dev/Cargo.lock /dev/Cargo.lock
# State file # State file
.rustlings-state.json .rustlings-state.txt
# oranda # oranda
public/ public/

12
Cargo.lock generated
View file

@ -690,7 +690,6 @@ dependencies = [
"ratatui", "ratatui",
"rustlings-macros", "rustlings-macros",
"serde", "serde",
"serde_json",
"toml_edit", "toml_edit",
"which", "which",
] ]
@ -749,17 +748,6 @@ dependencies = [
"syn 2.0.58", "syn 2.0.58",
] ]
[[package]]
name = "serde_json"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.5" version = "0.6.5"

View file

@ -41,7 +41,6 @@ hashbrown = "0.14.3"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"
ratatui = "0.26.1" ratatui = "0.26.1"
rustlings-macros = { path = "rustlings-macros" } rustlings-macros = { path = "rustlings-macros" }
serde_json = "1.0.115"
serde.workspace = true serde.workspace = true
toml_edit.workspace = true toml_edit.workspace = true
which = "6.0.1" which = "6.0.1"

View file

@ -4,15 +4,14 @@ use crossterm::{
terminal::{Clear, ClearType}, terminal::{Clear, ClearType},
ExecutableCommand, ExecutableCommand,
}; };
use std::io::{StdoutLock, Write}; use std::{
fs::{self, File},
mod state_file; io::{Read, StdoutLock, Write},
};
use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE}; use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE};
use self::state_file::{write, StateFileDeser}; const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const STATE_FILE_NAME: &str = ".rustlings-state.json";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
#[must_use] #[must_use]
@ -27,11 +26,51 @@ pub struct AppState {
n_done: u16, n_done: u16,
welcome_message: String, welcome_message: String,
final_message: String, final_message: String,
file_buf: Vec<u8>,
} }
impl AppState { impl AppState {
fn update_from_file(&mut self) {
self.file_buf.clear();
self.n_done = 0;
if File::open(STATE_FILE_NAME)
.and_then(|mut file| file.read_to_end(&mut self.file_buf))
.is_ok()
{
let mut lines = self.file_buf.split(|c| *c == b'\n');
let Some(current_exercise_name) = lines.next() else {
return;
};
if lines.next().is_none() {
return;
}
let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len());
for done_exerise_name in lines {
if done_exerise_name.is_empty() {
break;
}
done_exercises.insert(done_exerise_name);
}
for (ind, exercise) in self.exercises.iter_mut().enumerate() {
if done_exercises.contains(exercise.name.as_bytes()) {
exercise.done = true;
self.n_done += 1;
}
if exercise.name.as_bytes() == current_exercise_name {
self.current_exercise_ind = ind;
}
}
}
}
pub fn new(info_file: InfoFile) -> Self { pub fn new(info_file: InfoFile) -> Self {
let mut exercises = info_file let exercises = info_file
.exercises .exercises
.into_iter() .into_iter()
.map(|mut exercise_info| { .map(|mut exercise_info| {
@ -55,42 +94,18 @@ impl AppState {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let (current_exercise_ind, n_done) = StateFileDeser::read().map_or((0, 0), |state_file| { let mut slf = Self {
let mut state_file_exercises = current_exercise_ind: 0,
hashbrown::HashMap::with_capacity(state_file.exercises.len());
for (ind, exercise_state) in state_file.exercises.into_iter().enumerate() {
state_file_exercises.insert(
exercise_state.name,
(ind == state_file.current_exercise_ind, exercise_state.done),
);
}
let mut current_exercise_ind = 0;
let mut n_done = 0;
for (ind, exercise) in exercises.iter_mut().enumerate() {
if let Some((current, done)) = state_file_exercises.get(exercise.name) {
if *done {
exercise.done = true;
n_done += 1;
}
if *current {
current_exercise_ind = ind;
}
}
}
(current_exercise_ind, n_done)
});
Self {
current_exercise_ind,
exercises, exercises,
n_done, n_done: 0,
welcome_message: info_file.welcome_message.unwrap_or_default(), welcome_message: info_file.welcome_message.unwrap_or_default(),
final_message: info_file.final_message.unwrap_or_default(), final_message: info_file.final_message.unwrap_or_default(),
} file_buf: Vec::with_capacity(2048),
};
slf.update_from_file();
slf
} }
#[inline] #[inline]
@ -120,7 +135,7 @@ impl AppState {
self.current_exercise_ind = ind; self.current_exercise_ind = ind;
write(self) self.write()
} }
pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> {
@ -132,7 +147,7 @@ impl AppState {
.position(|exercise| exercise.name == name) .position(|exercise| exercise.name == name)
.with_context(|| format!("No exercise found for '{name}'!"))?; .with_context(|| format!("No exercise found for '{name}'!"))?;
write(self) self.write()
} }
pub fn set_pending(&mut self, ind: usize) -> Result<()> { pub fn set_pending(&mut self, ind: usize) -> Result<()> {
@ -141,7 +156,7 @@ impl AppState {
if exercise.done { if exercise.done {
exercise.done = false; exercise.done = false;
self.n_done -= 1; self.n_done -= 1;
write(self)?; self.write()?;
} }
Ok(()) Ok(())
@ -193,7 +208,7 @@ impl AppState {
self.exercises[exercise_ind].done = false; self.exercises[exercise_ind].done = false;
self.n_done -= 1; self.n_done -= 1;
write(self)?; self.write()?;
return Ok(ExercisesProgress::Pending); return Ok(ExercisesProgress::Pending);
} }
@ -213,6 +228,31 @@ impl AppState {
Ok(ExercisesProgress::Pending) Ok(ExercisesProgress::Pending)
} }
// Write the state file.
// The file's format is very simple:
// - The first line is the name of the current exercise.
// - The second line is an empty line.
// - All remaining lines are the names of done exercises.
fn write(&mut self) -> Result<()> {
self.file_buf.clear();
self.file_buf
.extend_from_slice(self.current_exercise().name.as_bytes());
self.file_buf.extend_from_slice(b"\n\n");
for exercise in &self.exercises {
if exercise.done {
self.file_buf.extend_from_slice(exercise.name.as_bytes());
self.file_buf.extend_from_slice(b"\n");
}
}
fs::write(STATE_FILE_NAME, &self.file_buf)
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
Ok(())
}
} }
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"

View file

@ -1,110 +0,0 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use crate::exercise::Exercise;
use super::{AppState, STATE_FILE_NAME};
#[derive(Deserialize)]
pub struct ExerciseStateDeser {
pub name: String,
pub done: bool,
}
#[derive(Serialize)]
struct ExerciseStateSer<'a> {
name: &'a str,
done: bool,
}
struct ExercisesStateSerializer<'a>(&'a [Exercise]);
impl<'a> Serialize for ExercisesStateSerializer<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let iter = self.0.iter().map(|exercise| ExerciseStateSer {
name: exercise.name,
done: exercise.done,
});
serializer.collect_seq(iter)
}
}
#[derive(Deserialize)]
pub struct StateFileDeser {
pub current_exercise_ind: usize,
pub exercises: Vec<ExerciseStateDeser>,
}
#[derive(Serialize)]
struct StateFileSer<'a> {
current_exercise_ind: usize,
exercises: ExercisesStateSerializer<'a>,
}
impl StateFileDeser {
pub fn read() -> Option<Self> {
let file_content = fs::read(STATE_FILE_NAME).ok()?;
serde_json::de::from_slice(&file_content).ok()
}
}
pub fn write(app_state: &AppState) -> Result<()> {
let content = StateFileSer {
current_exercise_ind: app_state.current_exercise_ind,
exercises: ExercisesStateSerializer(&app_state.exercises),
};
let mut buf = Vec::with_capacity(4096);
serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?;
fs::write(STATE_FILE_NAME, buf)
.with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::info_file::Mode;
use super::*;
#[test]
fn ser_deser_sync() {
let current_exercise_ind = 1;
let exercises = [
Exercise {
name: "1",
path: "exercises/1.rs",
mode: Mode::Run,
hint: String::new(),
done: true,
},
Exercise {
name: "2",
path: "exercises/2.rs",
mode: Mode::Test,
hint: String::new(),
done: false,
},
];
let ser = StateFileSer {
current_exercise_ind,
exercises: ExercisesStateSerializer(&exercises),
};
let deser: StateFileDeser =
serde_json::de::from_slice(&serde_json::ser::to_vec(&ser).unwrap()).unwrap();
assert_eq!(deser.current_exercise_ind, current_exercise_ind);
assert!(deser
.exercises
.iter()
.zip(exercises)
.all(|(deser, ser)| deser.name == ser.name && deser.done == ser.done));
}
}

View file

@ -89,7 +89,7 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> {
} }
const GITIGNORE: &[u8] = b"/target const GITIGNORE: &[u8] = b"/target
/.rustlings-state.json /.rustlings-state.txt
"; ";
const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;