Adapt Files to new file representation

This commit is contained in:
Joscha 2022-01-02 00:43:06 +01:00
parent d27812b836
commit d8617ede24
2 changed files with 111 additions and 72 deletions

View file

@ -1,12 +1,11 @@
use std::collections::HashMap; use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc}; use chrono::{DateTime, NaiveDate, Utc};
use tzfile::Tz; use tzfile::Tz;
use crate::eval::SourceInfo;
use self::commands::{Command, Done, File}; use self::commands::{Command, Done, File};
pub use self::error::{Error, Result}; pub use self::error::{Error, Result};
@ -46,6 +45,10 @@ pub struct Source {
} }
impl Source { impl Source {
pub fn new(file: usize, command: usize) -> Self {
Self { file, command }
}
pub fn file(&self) -> usize { pub fn file(&self) -> usize {
self.file self.file
} }
@ -61,19 +64,30 @@ pub struct SourcedCommand<'a> {
pub struct Files { pub struct Files {
files: Vec<LoadedFile>, files: Vec<LoadedFile>,
timezone: Tz, timezone: Tz,
logs: HashMap<NaiveDate, Source>,
} }
impl Files { impl Files {
/* Loading */
pub fn load(path: &Path) -> Result<Self> { pub fn load(path: &Path) -> Result<Self> {
let mut paths = HashMap::new(); // Track already loaded files by their normalized paths
let mut loaded = HashSet::new();
let mut files = vec![]; 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)?; let timezone = Self::determine_timezone(&files)?;
Ok(Self { files, timezone }) let logs = Self::collect_logs(&files)?;
Ok(Self {
files,
timezone,
logs,
})
} }
fn load_file( fn load_file(
paths: &mut HashMap<PathBuf, usize>, loaded: &mut HashSet<PathBuf>,
files: &mut Vec<LoadedFile>, files: &mut Vec<LoadedFile>,
name: &Path, name: &Path,
) -> Result<()> { ) -> Result<()> {
@ -81,7 +95,7 @@ impl Files {
path: name.to_path_buf(), path: name.to_path_buf(),
error: e, error: e,
})?; })?;
if paths.contains_key(&path) { if loaded.contains(&path) {
// We've already loaded this exact file. // We've already loaded this exact file.
return Ok(()); return Ok(());
} }
@ -93,51 +107,72 @@ impl Files {
// Using `name` instead of `path` for the unwrap below. // Using `name` instead of `path` for the unwrap below.
let file = parse::parse(name, &content)?; 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::<Vec<_>>();
loaded.insert(path.clone());
files.push(LoadedFile::new(path, name.to_owned(), file)); files.push(LoadedFile::new(path, name.to_owned(), file));
for include in includes { for include in includes {
// Since we've successfully opened the file, its name can't be the // 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); let include_path = name.parent().unwrap().join(include);
Self::load_file(paths, files, &include_path)?; Self::load_file(loaded, files, &include_path)?;
} }
Ok(()) Ok(())
} }
fn determine_timezone(files: &[LoadedFile]) -> Result<Tz> { fn determine_timezone(files: &[LoadedFile]) -> Result<Tz> {
let mut found: Option<(PathBuf, String)> = None; let mut found: Option<String> = None;
for file in files { for command in Self::commands_of_files(files) {
if let Some(file_tz) = &file.file.timezone { if let Command::Timezone(tz) = command.command {
if let Some((found_name, found_tz)) = &found { if let Some(found_tz) = &found {
if found_tz != file_tz { if tz != found_tz {
return Err(Error::TzConflict { return Err(Error::TzConflict {
file1: found_name.clone(),
tz1: found_tz.clone(), tz1: found_tz.clone(),
file2: file.name.clone(), tz2: tz.clone(),
tz2: file_tz.clone(),
}); });
} }
} else { } else {
found = Some((file.name.clone(), file_tz.clone())); found = Some(tz.clone());
} }
} }
} }
Ok(if let Some((_, tz)) = found { Ok(if let Some(timezone) = found {
Tz::named(&tz).map_err(|e| Error::ResolveTz { Tz::named(&timezone).map_err(|error| Error::ResolveTz { timezone, error })?
timezone: tz,
error: e,
})?
} else { } else {
Tz::local().map_err(|e| Error::LocalTz { error: e })? Tz::local().map_err(|error| Error::LocalTz { error })?
}) })
} }
fn collect_logs(files: &[LoadedFile]) -> Result<HashMap<NaiveDate, Source>> {
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<()> { pub fn save(&self) -> Result<()> {
for file in &self.files { for file in &self.files {
if file.dirty { if file.dirty {
@ -148,6 +183,7 @@ impl Files {
} }
fn save_file(path: &Path, file: &File) -> Result<()> { fn save_file(path: &Path, file: &File) -> Result<()> {
// TODO Sort commands within file
let formatted = format!("{}", file); let formatted = format!("{}", file);
if file.contents == formatted { if file.contents == formatted {
println!("Unchanged file {:?}", path); println!("Unchanged file {:?}", path);
@ -161,26 +197,47 @@ impl Files {
Ok(()) Ok(())
} }
pub fn mark_all_dirty(&mut self) { /* Querying */
for file in self.files.iter_mut() {
file.dirty = true; 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<SourcedCommand<'_>> {
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<SourcedCommand<'_>> {
Self::commands_of_files(&self.files)
} }
pub fn command(&self, source: Source) -> &Command { pub fn command(&self, source: Source) -> &Command {
&self.files[source.file].file.commands[source.command] &self.files[source.file].file.commands[source.command]
} }
pub fn sources(&self) -> Vec<SourceInfo<'_>> { pub fn now(&self) -> DateTime<&Tz> {
self.files Utc::now().with_timezone(&&self.timezone)
.iter()
.map(|f| SourceInfo {
name: Some(f.name.to_string_lossy().to_string()),
content: &f.file.contents,
})
.collect()
} }
/* 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`. /// Add a [`Done`] statement to the task identified by `source`.
/// ///
/// Returns whether the addition was successful. It can fail if the entry /// Returns whether the addition was successful. It can fail if the entry
@ -195,22 +252,5 @@ impl Files {
file.dirty = true; file.dirty = true;
true true
} }
*/
pub fn commands(&self) -> Vec<SourcedCommand<'_>> {
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)
}
} }

View file

@ -1,8 +1,12 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::{io, result}; use std::{io, result};
use chrono::NaiveDate;
use super::parse; use super::parse;
// TODO Format TzConflict and LogConflict errors better
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("Could not resolve {path}: {error}")] #[error("Could not resolve {path}: {error}")]
@ -17,13 +21,10 @@ pub enum Error {
LocalTz { error: io::Error }, LocalTz { error: io::Error },
#[error("{0}")] #[error("{0}")]
Parse(#[from] parse::Error), Parse(#[from] parse::Error),
#[error("{file1} has time zone {tz1} but {file2} has time zone {tz2}")] #[error("Conflicting time zones {tz1} and {tz2}")]
TzConflict { TzConflict { tz1: String, tz2: String },
file1: PathBuf, #[error("Duplicate logs for {0}")]
tz1: String, LogConflict(NaiveDate),
file2: PathBuf,
tz2: String,
},
} }
impl Error { impl Error {
@ -50,15 +51,13 @@ impl Error {
eprintln!(" {}", error); eprintln!(" {}", error);
} }
Error::Parse(error) => eprintln!("{}", error), Error::Parse(error) => eprintln!("{}", error),
Error::TzConflict { Error::TzConflict { tz1, tz2 } => {
file1,
tz1,
file2,
tz2,
} => {
eprintln!("Time zone conflict:"); eprintln!("Time zone conflict:");
eprintln!(" {:?} has time zone {}", file1, tz1); eprintln!(" Both {} and {} are specified", tz1, tz2);
eprintln!(" {:?} has time zone {}", file2, tz2); }
Error::LogConflict(date) => {
eprintln!("Log conflict:");
eprintln!(" More than one entry exists for {}", date);
} }
} }
} }