Adapt Files to new file representation
This commit is contained in:
parent
d27812b836
commit
d8617ede24
2 changed files with 111 additions and 72 deletions
154
src/files.rs
154
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::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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue