use crate::model::{ListOptions, Note};
use crate::storage::{load_notes, save_notes};
use chrono::Utc;
use std::collections::{HashMap, HashSet};
fn now_timestamp() -> String {
Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()
}
fn note_not_found(id: u64) -> String {
format!("no note with id {}", id)
}
fn modify_note<F>(id: u64, f: F) -> Result<Note, String>
where
F: FnOnce(&mut Note) -> bool,
{
let mut notes = load_notes()?;
if let Some(note) = notes.iter_mut().find(|n| n.id == id) {
let changed = f(note);
if changed {
note.updated_at = now_timestamp();
}
let out = note.clone();
if changed {
save_notes(¬es)?;
}
return Ok(out);
}
Err(note_not_found(id))
}
pub fn add_note(text: &str) -> Result<Note, String> {
let mut notes = load_notes()?;
let max_id = notes.iter().map(|n| n.id).max().unwrap_or(0);
let note = Note {
id: max_id + 1,
text: text.to_string(),
created_at: now_timestamp(),
updated_at: String::new(),
tags: Vec::new(),
};
notes.push(note.clone());
save_notes(¬es)?;
Ok(note)
}
pub fn remove_note(id: u64) -> Result<Note, String> {
let mut notes = load_notes()?;
if let Some(pos) = notes.iter().position(|n| n.id == id) {
let note = notes.remove(pos);
save_notes(¬es)?;
return Ok(note);
}
Err(note_not_found(id))
}
pub fn remove_notes(ids: &[u64], force: bool) -> Result<Vec<Note>, String> {
let mut notes = load_notes()?;
let mut target_ids: HashSet<u64> = ids.iter().copied().collect();
if !force {
let existing: HashSet<u64> = notes.iter().map(|n| n.id).collect();
let not_found: Vec<u64> = ids
.iter()
.copied()
.filter(|id| !existing.contains(id))
.collect();
if !not_found.is_empty() {
let joined = not_found
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(", ");
return Err(format!("no note with id {}; no notes were removed", joined));
}
}
let mut removed = Vec::new();
notes.retain(|n| {
if target_ids.remove(&n.id) {
removed.push(n.clone());
false
} else {
true
}
});
save_notes(¬es)?;
Ok(removed)
}
pub fn search_notes(query: &str) -> Result<Vec<Note>, String> {
let notes = load_notes()?;
let q = query.to_lowercase();
Ok(notes
.into_iter()
.filter(|n| {
n.text.to_lowercase().contains(&q)
|| n.tags.iter().any(|t| t.to_lowercase().contains(&q))
})
.collect())
}
pub fn edit_note(id: u64, text: &str) -> Result<Note, String> {
let text = text.to_string();
modify_note(id, |note| {
note.text = text;
true
})
}
pub fn append_note(id: u64, text: &str) -> Result<Note, String> {
let suffix = text.to_string();
modify_note(id, |note| {
note.text = format!("{} {}", note.text, suffix);
true
})
}
pub fn get_note(id: u64) -> Result<Note, String> {
let notes = load_notes()?;
notes
.into_iter()
.find(|n| n.id == id)
.ok_or_else(|| note_not_found(id))
}
pub fn clear_notes() -> Result<(), String> {
save_notes(&[])
}
pub fn import_notes(mut incoming: Vec<Note>) -> Result<(), String> {
let mut notes = load_notes()?;
let mut max_id = notes.iter().map(|n| n.id).max().unwrap_or(0);
for note in &mut incoming {
max_id += 1;
note.id = max_id;
}
notes.extend(incoming);
save_notes(¬es)
}
pub fn tag_note(id: u64, tags: &[String]) -> Result<Note, String> {
modify_note(id, |note| {
let mut changed = false;
for tag in tags {
if !note
.tags
.iter()
.any(|t| t.to_lowercase() == tag.to_lowercase())
{
note.tags.push(tag.clone());
changed = true;
}
}
changed
})
}
pub fn untag_note(id: u64, tag: &str) -> Result<Note, String> {
let needle = tag.to_lowercase();
modify_note(id, |note| {
let before = note.tags.len();
note.tags.retain(|t| t.to_lowercase() != needle);
note.tags.len() < before
})
}
pub fn collect_tags(notes: &[Note]) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for note in notes {
for tag in ¬e.tags {
*counts.entry(tag.to_lowercase()).or_insert(0) += 1;
}
}
counts
}
pub fn list_notes(opts: &ListOptions) -> Result<Vec<Note>, String> {
let mut notes = load_notes()?;
fn updated_sort_key(note: &Note) -> &str {
if note.updated_at.is_empty() {
note.created_at.as_str()
} else {
note.updated_at.as_str()
}
}
if !opts.tag.is_empty() {
let needle = opts.tag.to_lowercase();
notes.retain(|n| n.tags.iter().any(|t| t.to_lowercase() == needle));
}
match opts.sort.as_str() {
"" | "id" => notes.sort_by_key(|n| n.id),
"date" => notes.sort_by(|a, b| b.created_at.cmp(&a.created_at)),
"updated" => {
notes.sort_by(|a, b| updated_sort_key(b).cmp(updated_sort_key(a)));
}
other => {
return Err(format!(
"unknown sort \"{}\": use id, date, or updated",
other
));
}
}
if opts.limit > 0 && notes.len() > opts.limit {
notes.truncate(opts.limit);
}
Ok(notes)
}