From d8617ede24ee9ea58172cf61f25070fb7bdd97d7 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 2 Jan 2022 00:43:06 +0100 Subject: [PATCH] Adapt Files to new file representation --- src/files.rs | 154 ++++++++++++++++++++++++++++----------------- src/files/error.rs | 29 +++++---- 2 files changed, 111 insertions(+), 72 deletions(-) diff --git a/src/files.rs b/src/files.rs index 0e5aded..3022ef1 100644 --- a/src/files.rs +++ b/src/files.rs @@ -1,12 +1,11 @@ -use std::collections::HashMap; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDate, Utc}; use tzfile::Tz; -use crate::eval::SourceInfo; - use self::commands::{Command, Done, File}; pub use self::error::{Error, Result}; @@ -46,6 +45,10 @@ pub struct Source { } impl Source { + pub fn new(file: usize, command: usize) -> Self { + Self { file, command } + } + pub fn file(&self) -> usize { self.file } @@ -61,19 +64,30 @@ pub struct SourcedCommand<'a> { pub struct Files { files: Vec, timezone: Tz, + logs: HashMap, } impl Files { + /* Loading */ + pub fn load(path: &Path) -> Result { - let mut paths = HashMap::new(); + // Track already loaded files by their normalized paths + let mut loaded = HashSet::new(); + let mut files = vec![]; - Self::load_file(&mut paths, &mut files, path)?; + Self::load_file(&mut loaded, &mut files, path)?; + let timezone = Self::determine_timezone(&files)?; - Ok(Self { files, timezone }) + let logs = Self::collect_logs(&files)?; + Ok(Self { + files, + timezone, + logs, + }) } fn load_file( - paths: &mut HashMap, + loaded: &mut HashSet, files: &mut Vec, name: &Path, ) -> Result<()> { @@ -81,7 +95,7 @@ impl Files { path: name.to_path_buf(), error: e, })?; - if paths.contains_key(&path) { + if loaded.contains(&path) { // We've already loaded this exact file. return Ok(()); } @@ -93,51 +107,72 @@ impl Files { // Using `name` instead of `path` for the unwrap below. let file = parse::parse(name, &content)?; - let includes = file.includes.clone(); - paths.insert(path.clone(), files.len()); + let includes = file + .commands + .iter() + .filter_map(|c| match c { + Command::Include(path) => Some(path.clone()), + _ => None, + }) + .collect::>(); + + loaded.insert(path.clone()); files.push(LoadedFile::new(path, name.to_owned(), file)); for include in includes { // Since we've successfully opened the file, its name can't be the - // root directory or empty string and must thus have a parent. + // root directory or empty string and it must thus have a parent. let include_path = name.parent().unwrap().join(include); - Self::load_file(paths, files, &include_path)?; + Self::load_file(loaded, files, &include_path)?; } Ok(()) } fn determine_timezone(files: &[LoadedFile]) -> Result { - let mut found: Option<(PathBuf, String)> = None; + let mut found: Option = None; - for file in files { - if let Some(file_tz) = &file.file.timezone { - if let Some((found_name, found_tz)) = &found { - if found_tz != file_tz { + for command in Self::commands_of_files(files) { + if let Command::Timezone(tz) = command.command { + if let Some(found_tz) = &found { + if tz != found_tz { return Err(Error::TzConflict { - file1: found_name.clone(), tz1: found_tz.clone(), - file2: file.name.clone(), - tz2: file_tz.clone(), + tz2: tz.clone(), }); } } else { - found = Some((file.name.clone(), file_tz.clone())); + found = Some(tz.clone()); } } } - Ok(if let Some((_, tz)) = found { - Tz::named(&tz).map_err(|e| Error::ResolveTz { - timezone: tz, - error: e, - })? + Ok(if let Some(timezone) = found { + Tz::named(&timezone).map_err(|error| Error::ResolveTz { timezone, error })? } else { - Tz::local().map_err(|e| Error::LocalTz { error: e })? + Tz::local().map_err(|error| Error::LocalTz { error })? }) } + fn collect_logs(files: &[LoadedFile]) -> Result> { + let mut logs = HashMap::new(); + + for command in Self::commands_of_files(files) { + if let Command::Log(log) = command.command { + if let Entry::Vacant(e) = logs.entry(log.date) { + e.insert(command.source); + } else { + return Err(Error::LogConflict(log.date)); + } + } + } + + Ok(logs) + } + + /* Saving */ + pub fn save(&self) -> Result<()> { for file in &self.files { if file.dirty { @@ -148,6 +183,7 @@ impl Files { } fn save_file(path: &Path, file: &File) -> Result<()> { + // TODO Sort commands within file let formatted = format!("{}", file); if file.contents == formatted { println!("Unchanged file {:?}", path); @@ -161,26 +197,47 @@ impl Files { Ok(()) } - pub fn mark_all_dirty(&mut self) { - for file in self.files.iter_mut() { - file.dirty = true; + /* Querying */ + + pub fn files(&self) -> Vec<(&Path, &File)> { + self.files + .iter() + .map(|f| (&f.name as &Path, &f.file)) + .collect() + } + + fn commands_of_files(files: &[LoadedFile]) -> Vec> { + let mut result = vec![]; + for (file_index, file) in files.iter().enumerate() { + for (command_index, command) in file.file.commands.iter().enumerate() { + let source = Source::new(file_index, command_index); + result.push(SourcedCommand { source, command }); + } } + result + } + + pub fn commands(&self) -> Vec> { + Self::commands_of_files(&self.files) } pub fn command(&self, source: Source) -> &Command { &self.files[source.file].file.commands[source.command] } - pub fn sources(&self) -> Vec> { - self.files - .iter() - .map(|f| SourceInfo { - name: Some(f.name.to_string_lossy().to_string()), - content: &f.file.contents, - }) - .collect() + pub fn now(&self) -> DateTime<&Tz> { + Utc::now().with_timezone(&&self.timezone) } + /* Updating */ + + pub fn mark_all_dirty(&mut self) { + for file in self.files.iter_mut() { + file.dirty = true; + } + } + + /* /// Add a [`Done`] statement to the task identified by `source`. /// /// Returns whether the addition was successful. It can fail if the entry @@ -195,22 +252,5 @@ impl Files { file.dirty = true; true } - - pub fn commands(&self) -> Vec> { - let mut result = vec![]; - for (file_index, file) in self.files.iter().enumerate() { - for (command_index, command) in file.file.commands.iter().enumerate() { - let source = Source { - file: file_index, - command: command_index, - }; - result.push(SourcedCommand { source, command }); - } - } - result - } - - pub fn now(&self) -> DateTime<&Tz> { - Utc::now().with_timezone(&&self.timezone) - } + */ } diff --git a/src/files/error.rs b/src/files/error.rs index 27b766b..aace490 100644 --- a/src/files/error.rs +++ b/src/files/error.rs @@ -1,8 +1,12 @@ use std::path::PathBuf; use std::{io, result}; +use chrono::NaiveDate; + use super::parse; +// TODO Format TzConflict and LogConflict errors better + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Could not resolve {path}: {error}")] @@ -17,13 +21,10 @@ pub enum Error { LocalTz { error: io::Error }, #[error("{0}")] Parse(#[from] parse::Error), - #[error("{file1} has time zone {tz1} but {file2} has time zone {tz2}")] - TzConflict { - file1: PathBuf, - tz1: String, - file2: PathBuf, - tz2: String, - }, + #[error("Conflicting time zones {tz1} and {tz2}")] + TzConflict { tz1: String, tz2: String }, + #[error("Duplicate logs for {0}")] + LogConflict(NaiveDate), } impl Error { @@ -50,15 +51,13 @@ impl Error { eprintln!(" {}", error); } Error::Parse(error) => eprintln!("{}", error), - Error::TzConflict { - file1, - tz1, - file2, - tz2, - } => { + Error::TzConflict { tz1, tz2 } => { eprintln!("Time zone conflict:"); - eprintln!(" {:?} has time zone {}", file1, tz1); - eprintln!(" {:?} has time zone {}", file2, tz2); + eprintln!(" Both {} and {} are specified", tz1, tz2); + } + Error::LogConflict(date) => { + eprintln!("Log conflict:"); + eprintln!(" More than one entry exists for {}", date); } } }