mod bindings;
use crate::bindings::exports::pack::name::api::*;
use chrono::{DateTime, NaiveDateTime, Utc};
use once_cell::sync::Lazy;
use std::cell::RefCell;
use std::{cmp, collections::HashMap, num::TryFromIntError};
use uuid::Uuid;
struct Component;
struct State {
items: HashMap<String, Item>,
}
const DATE_TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S %z";
const QUERY_MAX_LIMIT: u32 = 100;
const QUERY_DEFAULT_LIMIT: u32 = 10;
thread_local! {
static STATE: RefCell<Lazy<State>> = RefCell::new(
Lazy::new(|| State {
items: HashMap::new(),
})
)}
fn unix_time_from_option_string(s: Option<String>) -> Result<Option<i64>, String> {
s.map(|s| {
let unix_time = DateTime::parse_from_str(&s, DATE_TIME_FORMAT)
.map_err(|e| {
let error_message = format!(
"ERROR: '{s}' is NOT in the required format of '{}': {:?}.",
DATE_TIME_FORMAT,
e.to_string()
);
println!("{error_message}");
error_message
})?
.timestamp();
Ok(unix_time) as Result<i64, String>
})
.transpose()
}
#[inline]
fn print_count(state: &State) {
println!(
"You have {} items left in your todo list.",
state.items.len()
);
}
trait ExtensionsForUpdateItem {
fn change_is_present(&self) -> bool;
}
impl ExtensionsForUpdateItem for UpdateItem {
fn change_is_present(&self) -> bool {
self.title.is_some()
|| self.priority.is_some()
|| self.status.is_some()
|| self.deadline.is_some()
}
}
trait Ordinal {
fn ordinal(&self) -> u8;
}
impl Ordinal for Priority {
fn ordinal(&self) -> u8 {
match self {
Priority::Low => 0,
Priority::Medium => 1,
Priority::High => 2,
}
}
}
impl Guest for Component {
fn add(item: NewItem) -> Result<Item, String> {
let title = item.title.trim();
if title.is_empty() {
return Err("Title cannot be empty".to_string());
}
let deadline = unix_time_from_option_string(item.deadline)?;
let id = Uuid::new_v4().to_string();
let now = Utc::now().timestamp();
let item = Item {
id,
title: title.to_string(),
priority: item.priority,
deadline,
status: Status::Backlog,
created_timestamp: now,
updated_timestamp: now,
};
println!("New item created: {:?}", item);
let result = item.clone();
STATE.with_borrow_mut(|state| {
state.items.insert(item.id.clone(), item);
});
Ok(result)
}
fn update(id: String, change: UpdateItem) -> Result<Item, String> {
if change.change_is_present() {
let deadline_update = unix_time_from_option_string(change.deadline)?;
STATE.with_borrow_mut(|state| {
if let Some(item) = state.items.get_mut(&id) {
let mut modified = false;
if let Some(title_update) = change.title {
let title_update = title_update.trim();
if !{ title_update.is_empty() } && item.title != title_update {
item.title = title_update.to_string();
modified = true;
}
}
if let Some(priority_update) = change.priority {
if item.priority != priority_update {
item.priority = priority_update;
modified = true;
}
}
if let Some(status_update) = change.status {
if item.status != status_update {
item.status = status_update;
modified = true;
}
}
if item.deadline != deadline_update {
item.deadline = deadline_update;
modified = true;
}
if modified {
item.updated_timestamp = Utc::now().timestamp();
println!("Updated item with ID '{}'.", id);
} else {
println!("No update applied to item with ID '{}'.", id);
}
Ok(item.clone())
} else {
let error_message = format!("Item with ID '{}' not found!", id);
println!("{error_message}");
Err(error_message)
}
})
} else {
Err("At least one change must be present.".to_string())
}
}
fn search(query: Query) -> Result<Vec<Item>, String> {
let deadline = unix_time_from_option_string(query.deadline)?;
let limit: usize = query
.limit
.map(|n| {
if n > QUERY_MAX_LIMIT {
QUERY_MAX_LIMIT
} else {
n
}
})
.unwrap_or(QUERY_DEFAULT_LIMIT)
.try_into()
.map_err(|e: TryFromIntError| e.to_string())?;
STATE.with_borrow_mut(|state| {
let mut result: Vec<_> = state
.items
.values()
.filter(|item| {
query
.keyword
.as_ref()
.map(|keyword| item.title.contains(keyword))
.unwrap_or(true)
&& query
.priority
.map(|priority| item.priority == priority)
.unwrap_or(true)
&& query
.status
.map(|status| item.status == status)
.unwrap_or(true)
&& deadline
.map(|deadline| {
if let Some(before) = item.deadline {
before <= deadline
} else {
true
}
})
.unwrap_or(true)
})
.cloned()
.collect();
match query.sort {
Some(QuerySort::Priority) => {
result.sort_by_key(|item| cmp::Reverse(item.priority.ordinal()));
}
Some(QuerySort::Deadline) => {
result.sort_by_key(|item| cmp::Reverse(item.deadline));
}
None => {
result.sort_by_key(|item| item.title.clone());
}
};
result = result.into_iter().take(limit).collect();
if result.is_empty() {
println!("No matching todo found.");
} else {
print!("Found {} matching items: ", result.len());
result.iter().for_each(|item| {
let deadline = item
.deadline
.and_then(|i: i64| NaiveDateTime::from_timestamp_opt(i, 0))
.map(|utc| {
DateTime::<Utc>::from_naive_utc_and_offset(utc, Utc)
.format(DATE_TIME_FORMAT)
.to_string()
})
.unwrap_or("<No deadline set>".to_string());
println!("{:?} {}", item, deadline);
});
}
Ok(result)
})
}
fn count() -> u32 {
STATE.with_borrow_mut(|state| {
let count = state.items.len() as u32;
println!("You have {} items in your todo list.", count);
count
})
}
fn delete(id: String) -> Result<(), String> {
STATE.with_borrow_mut(|state| {
if state.items.contains_key(&id) {
state.items.remove(&id);
println!("Deleted item with ID '{}'.", id);
print_count(state);
Ok(())
} else {
let error_message = format!("Item with ID '{}' not found!", id);
println!("{error_message}");
Err(error_message)
}
})
}
fn delete_done_items() {
STATE.with_borrow_mut(|state| {
let mut count = 0_u32;
state.items.retain(|_, item| {
if item.status == Status::Done {
count += 1;
false
} else {
true
}
});
println!("Deleted {} Done items.", count);
print_count(state);
});
}
fn delete_all() {
STATE.with_borrow_mut(|state| {
state.items.clear();
println!("Deleted all items.");
});
}
fn get(id: String) -> Result<Item, String> {
STATE.with_borrow_mut(|state| {
if let Some(item) = state.items.get(&id) {
println!("Found item with ID '{}'.", id);
Ok(item.clone())
} else {
Err(format!("Item with ID '{}' not found!", id))
}
})
}
}
bindings::export!(Component with_types_in bindings);