moved the enforcing of some invariants to be done on save

This commit is contained in:
aaron-jack-manning 2022-09-10 16:14:05 +10:00
parent 5bb20e6166
commit 5361ea4630
4 changed files with 37 additions and 40 deletions

View File

@ -79,10 +79,3 @@ Then you can run `toru new` to create your first task.
## Backup and Syncing ## Backup and Syncing
Toru stores tasks and other metadata locally in the folder of the vault in the interest of making that data easily accessible for backups, syncing across devices, and to be easy to export from. However, because Toru uses sequential IDs, sharing a vault across computers asynchronously, such as with Git, can cause different notes to be in conflict with each other. If all vault metadata is synced across devices completely before use, such conflicts can be avoided. Toru stores tasks and other metadata locally in the folder of the vault in the interest of making that data easily accessible for backups, syncing across devices, and to be easy to export from. However, because Toru uses sequential IDs, sharing a vault across computers asynchronously, such as with Git, can cause different notes to be in conflict with each other. If all vault metadata is synced across devices completely before use, such conflicts can be avoided.
---
## Planned Changes
- Validate invariants at the point of saving, to create consistency across creating and editing notes.
- Convenient options with the edit command so that editing the raw file isn't the only option

View File

@ -85,10 +85,6 @@ pub fn edit_raw(id : Id, vault_folder : path::PathBuf, editor : &str, state : &m
if edited_task.data.id != task.data.id { if edited_task.data.id != task.data.id {
Err(error::Error::Generic(String::from("You cannot change the ID of a task in a direct edit"))) Err(error::Error::Generic(String::from("You cannot change the ID of a task in a direct edit")))
} }
// Enforce non numeric name invariant.
else if edited_task.data.name.chars().all(|c| c.is_numeric()) {
Err(error::Error::Generic(String::from("Name must not be purely numeric")))
}
else { else {
// Dependencies were edited so the graph needs to be updated. // Dependencies were edited so the graph needs to be updated.
if edited_task.data.dependencies != task.data.dependencies { if edited_task.data.dependencies != task.data.dependencies {

View File

@ -119,8 +119,8 @@ fn program() -> Result<(), error::Error> {
match command { match command {
Command::New { name, info, tag, dependency, priority, due } => { Command::New { name, info, tag, dependency, priority, due } => {
let task = tasks::Task::new(name, info, tag, dependency, priority, due, vault_folder, &mut state)?; let id = tasks::Task::new(name.clone(), info, tag, dependency, priority, due, vault_folder, &mut state)?;
println!("Created task {} (ID: {})", format::task(&task.data.name), format::id(task.data.id)); println!("Created task {} (ID: {})", format::task(&name), format::id(id));
}, },
Command::Delete { id_or_name } => { Command::Delete { id_or_name } => {
let id = state.data.index.lookup(&id_or_name)?; let id = state.data.index.lookup(&id_or_name)?;

View File

@ -16,7 +16,8 @@ pub type Id = u64;
pub struct Task { pub struct Task {
pub path : path::PathBuf, pub path : path::PathBuf,
file : fs::File, // This should only be None for a new task, in which case it should be written from the path.
file : Option<fs::File>,
pub data : InternalTask, pub data : InternalTask,
} }
@ -223,12 +224,7 @@ impl TimeEntry {
impl Task { impl Task {
/// Creates a new task from the input data. /// Creates a new task from the input data.
pub fn new(name : String, info : Option<String>, tags : Vec<String>, dependencies : Vec<Id>, priority : Option<Priority>, due : Option<chrono::NaiveDateTime>, vault_folder : &path::Path, state : &mut state::State) -> Result<Self, error::Error> { pub fn new(name : String, info : Option<String>, tags : Vec<String>, dependencies : Vec<Id>, priority : Option<Priority>, due : Option<chrono::NaiveDateTime>, vault_folder : &path::Path, state : &mut state::State) -> Result<Id, error::Error> {
// Exclude numeric names in the interest of allowing commands that take in ID or name.
if name.chars().all(|c| c.is_numeric()) {
return Err(error::Error::Generic(String::from("Name must not be purely numeric")));
};
// Update the state with the new next Id. // Update the state with the new next Id.
let id = state.data.next_id; let id = state.data.next_id;
@ -236,11 +232,6 @@ impl Task {
let path = vault_folder.join("tasks").join(&format!("{}.toml", id)); let path = vault_folder.join("tasks").join(&format!("{}.toml", id));
let mut file = fs::File::options()
.write(true)
.create(true)
.open(&path)?;
// Adding to dependency graph appropriately. // Adding to dependency graph appropriately.
state.data.deps.insert_node(id); state.data.deps.insert_node(id);
if !dependencies.is_empty() { if !dependencies.is_empty() {
@ -267,19 +258,17 @@ impl Task {
completed : None, completed : None,
}; };
let file_contents = toml::to_string(&data)?;
file.set_len(0)?;
file.seek(io::SeekFrom::Start(0))?;
file.write_all(file_contents.as_bytes())?;
state.data.index.insert(data.name.clone(), id); state.data.index.insert(data.name.clone(), id);
Ok(Task { let task = Task {
path, path,
file, file : None,
data, data,
}) };
task.save()?;
Ok(id)
} }
/// Loads a task directly from its path, for use with the temporary edit file. /// Loads a task directly from its path, for use with the temporary edit file.
@ -300,7 +289,7 @@ impl Task {
Ok(Self { Ok(Self {
path, path,
file, file : Some(file),
data, data,
}) })
} }
@ -360,17 +349,36 @@ impl Task {
/// Saves the in memory task data to the corresponding file. /// Saves the in memory task data to the corresponding file.
pub fn save(self) -> Result<(), error::Error> { pub fn save(self) -> Result<(), error::Error> {
// Enforce any additional invariants which need to be checked for both edits and now tasks
// at the point of save.
{
// Exclude numeric names in the interest of allowing commands that take in ID or name.
if self.data.name.chars().all(|c| c.is_numeric()) {
return Err(error::Error::Generic(String::from("Name must not be purely numeric")));
};
}
let Self { let Self {
path : _, path,
mut file, file,
data, data,
} = self; } = self;
let file_contents = toml::to_string(&data)?; let file_contents = toml::to_string(&data)?;
// Check if the file exists, if not it is a new task and the file must be written from the
// path.
match file {
Some(mut file) => {
file.set_len(0)?; file.set_len(0)?;
file.seek(io::SeekFrom::Start(0))?; file.seek(io::SeekFrom::Start(0))?;
file.write_all(file_contents.as_bytes())?; file.write_all(file_contents.as_bytes())?;
},
None => {
fs::write(path, file_contents.as_bytes())?;
}
}
Ok(()) Ok(())
} }