Skip to content

Commit

Permalink
Implement --week for set/get/clear/delete
Browse files Browse the repository at this point in the history
  • Loading branch information
mawkler committed Nov 12, 2024
1 parent f63822b commit fa586cf
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 42 deletions.
18 changes: 16 additions & 2 deletions src/cli/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ impl FromStr for Format {
#[derive(Debug, Subcommand)]
pub enum Line {
/// Delete line based on line number (1-indexed)
Delete { line_number: LineNumber },
Delete {
line_number: LineNumber,

/// Week number
#[arg(long, short)]
week: Option<u8>,
},
}

#[derive(Debug, Subcommand)]
pub enum Command {
/// Get the time sheet for the current week
Get {
/// Week number (NOT YET SUPPORTED)
/// Week number
#[arg(long, short)]
week: Option<u8>,
/// Output format
Expand All @@ -46,6 +52,10 @@ pub enum Command {
/// Number of hours to set
hours: f32,

/// Week number
#[arg(long, short)]
week: Option<u8>,

/// Name of the job
#[arg(long, short)]
job: String,
Expand Down Expand Up @@ -86,6 +96,10 @@ pub enum Command {
/// Also accepts short day names like "mon", "tue", etc.
#[arg(long, short, value_parser = parse_days_of_week)]
day: Option<Days>,

/// Week number
#[arg(long, short)]
week: Option<u8>,
},

/// Submit time sheet for week
Expand Down
54 changes: 34 additions & 20 deletions src/cli/commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::arguments::Format;
use crate::domain::models::day::Days;
use crate::domain::models::line_number::LineNumber;
use crate::domain::models::week::WeekNumber;
use crate::{
domain::time_sheet_service::{SetTimeError, TimeSheetService},
infrastructure::{
Expand Down Expand Up @@ -41,23 +42,15 @@ impl<'a> CommandClient<'a> {
}

// TODO: allow setting week
pub(crate) async fn get_table(&self, week: Option<u8>) -> anyhow::Result<()> {
if week.is_some() {
panic!("--week flag is not yet supported")
}

let time_sheet = self.repository.lock().await.get_time_sheet().await?;
pub(crate) async fn get_table(&self, week: &WeekNumber) -> anyhow::Result<()> {
let time_sheet = self.repository.lock().await.get_week(week).await?;

println!("{time_sheet}");
Ok(())
}

async fn get_json(&self, week: Option<u8>) -> anyhow::Result<()> {
if week.is_some() {
panic!("--week flag is not yet supported")
}

let time_sheet = self.repository.lock().await.get_time_sheet().await?;
async fn get_json(&self, week: &WeekNumber) -> anyhow::Result<()> {
let time_sheet = self.repository.lock().await.get_week(week).await?;
let json =
serde_json::to_string(&time_sheet).context("Failed to deserialize time sheet")?;

Expand All @@ -67,15 +60,22 @@ impl<'a> CommandClient<'a> {

pub(crate) async fn get(&self, week: Option<u8>, format: Format) {
match format {
Format::Json => self.get_json(week).await.context("JSON"),
Format::Table => self.get_table(week).await.context("table"),
Format::Json => self.get_json(&week.into()).await.context("JSON"),
Format::Table => self.get_table(&week.into()).await.context("table"),
}
.unwrap_or_else(|err| {
exit_with_error!("Failed to get time sheet as {}", error_stack_fmt(&err));
})
}

pub(crate) async fn set(&mut self, hours: f32, days: Option<Days>, job: &str, task: &str) {
pub(crate) async fn set(
&mut self,
hours: f32,
days: Option<Days>,
week: Option<u8>,
job: &str,
task: &str,
) {
if days.as_ref().is_some_and(|days| days.is_empty()) {
exit_with_error!("`--day` is set but no day was provided");
}
Expand All @@ -84,7 +84,7 @@ impl<'a> CommandClient<'a> {
self.time_sheet_service
.lock()
.await
.set_time(hours, &day, job, task)
.set_time(hours, &day, &week.into(), job, task)
.await
.unwrap_or_else(|err| {
if let SetTimeError::Unknown(err) = err {
Expand All @@ -95,15 +95,21 @@ impl<'a> CommandClient<'a> {
});
}

pub(crate) async fn clear(&mut self, job: &str, task: &str, days: Option<Days>) {
pub(crate) async fn clear(
&mut self,
job: &str,
task: &str,
days: Option<Days>,
week: Option<u8>,
) {
if days.as_ref().is_some_and(|days| days.is_empty()) {
exit_with_error!("`--day` is set but no day was provided");
}

self.time_sheet_service
.lock()
.await
.clear(job, task, &get_days(days))
.clear(job, task, &get_days(days), &week.into())
.await
.unwrap_or_else(|err| {
if let SetTimeError::Unknown(err) = err {
Expand All @@ -120,11 +126,11 @@ impl<'a> CommandClient<'a> {
});
}

pub(crate) async fn delete(&mut self, line_number: &LineNumber) {
pub(crate) async fn delete(&mut self, line_number: &LineNumber, week: Option<u8>) {
self.repository
.lock()
.await
.delete_line(line_number)
.delete_line(line_number, &week.into())
.await
.unwrap_or_else(|err| {
let source = error_stack_fmt(&err);
Expand All @@ -151,3 +157,11 @@ fn get_days(days: Option<Days>) -> Days {
HashSet::from([today])
})
}

impl From<Option<u8>> for WeekNumber {
fn from(week: Option<u8>) -> Self {
// Fall back to today's week
week.unwrap_or_else(|| Local::now().date_naive().iso_week().week() as u8)
.into()
}
}
22 changes: 11 additions & 11 deletions src/cli/day_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ mod tests {
use super::*;

#[test]
fn range_parser_test() {
fn range_parser() {
let range = "mon-thu";
let (_, range) = day_range(range).unwrap();

Expand All @@ -107,7 +107,7 @@ mod tests {
}

#[test]
fn invalid_range_parser_test() {
fn invalid_range_parser() {
let range = "tue-mon";
let (_, result) = day_range(range).unwrap();

Expand All @@ -116,15 +116,15 @@ mod tests {
}

#[test]
fn individual_days_test() {
fn individual_days() {
let days = parse_days_of_week("mon, thu").unwrap();

let expected = [Day::Monday, Day::Thursday];
assert_eq!(days, Days::from(expected));
}

#[test]
fn days_and_range_test() {
fn days_and_range() {
let days = parse_days_of_week("mon, wed, fri-sun").unwrap();

let expected = [
Expand All @@ -138,36 +138,36 @@ mod tests {
}

#[test]
fn overlapping_day_ranges_test() {
fn overlapping_day_ranges() {
let days = parse_days_of_week("mon, mon-wed").unwrap();

let expected = [Day::Monday, Day::Tuesday, Day::Wednesday];
assert_eq!(days, Days::from(expected));
}

#[test]
fn empty_range_test() {
fn empty_range() {
let days = parse_days_of_week("").unwrap();

assert_eq!(days, Days::from([]));
}

#[test]
fn input_with_no_days_test() {
fn input_with_no_days() {
let days = parse_days_of_week("foo:bar").unwrap();

assert_eq!(days, Days::from([]));
}

#[test]
fn invalid_range_test() {
fn invalid_range() {
let err: String = parse_days_of_week("tue-mon").unwrap_err().to_string();

assert_eq!(err, "Invalid range");
}

#[test]
fn partial_day_names_test() {
fn partial_day_names() {
// Ensures that "mo", "mon", "mond", etc. all map to `Day::Monday`
let days = [
("monday", Day::Monday),
Expand All @@ -194,7 +194,7 @@ mod tests {
}

#[test]
fn days_in_range_test() {
fn gets_days_in_range() {
let range = (Day::Tuesday, Day::Friday);
let result = days_in_range(range).unwrap();
let expected = [Day::Tuesday, Day::Wednesday, Day::Thursday, Day::Friday];
Expand All @@ -203,7 +203,7 @@ mod tests {
}

#[test]
fn days_in_invalid_range_test() {
fn days_in_invalid_range() {
let range = (Day::Tuesday, Day::Monday);
let result = days_in_range(range);

Expand Down
1 change: 1 addition & 0 deletions src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ pub(crate) mod models {
pub(crate) mod hours;
pub(crate) mod line_number;
pub(crate) mod time_sheet;
pub(crate) mod week;
}
pub(crate) mod time_sheet_service;
48 changes: 48 additions & 0 deletions src/domain/models/week.rs
Original file line number Diff line number Diff line change
@@ -1 +1,49 @@
use std::fmt::Display;

use chrono::{NaiveDate, Weekday};

// TODO: switch from `u8` to `IsoWeek`
#[derive(Debug, Clone)]
pub(crate) struct WeekNumber(u8);

impl WeekNumber {
pub(crate) fn first_day(&self, year: i32) -> Option<NaiveDate> {
NaiveDate::from_isoywd_opt(year, self.0.into(), Weekday::Mon)
}
}

impl From<u8> for WeekNumber {
fn from(week: u8) -> Self {
Self(week)
}
}

impl Display for WeekNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn gets_date_of_first_day_of_week() {
let dates = [
(2024, 46, "2024-11-11"),
(2030, 1, "2029-12-31"),
(2028, 23, "2028-06-05"),
];

for (year, week, expected_date) in dates {
let week_number = WeekNumber(week);
let date = week_number.first_day(year).unwrap();

assert_eq!(
date,
NaiveDate::parse_from_str(expected_date, "%Y-%m-%d").unwrap()
)
}
}
}
9 changes: 6 additions & 3 deletions src/domain/time_sheet_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::rc::Rc;
use tokio::sync::Mutex;

use super::models::day::Days;
use super::models::week::WeekNumber;

#[derive(thiserror::Error, Debug)]
pub(crate) enum SetTimeError {
Expand Down Expand Up @@ -36,20 +37,22 @@ impl TimeSheetService<'_> {
job: &str,
task: &str,
days: &Days,
week: &WeekNumber,
) -> Result<(), SetTimeError> {
self.set_time(0.0, days, job, task).await
self.set_time(0.0, days, week, job, task).await
}

/// Sets time (initializes the week if it is uninitialized)
pub(crate) async fn set_time(
&mut self,
hours: f32,
days: &Days,
week: &WeekNumber,
job: &str,
task: &str,
) -> Result<(), SetTimeError> {
let mut repository = self.repository.lock().await;
if let Err(err) = repository.set_time(hours, days, job, task).await {
if let Err(err) = repository.set_time(hours, days, week, job, task).await {
return match err {
AddLineError::WeekUninitialized(AddRowError::Unknown(err)) => todo!("{}", err),
AddLineError::WeekUninitialized(AddRowError::WeekUninitialized) => {
Expand All @@ -58,7 +61,7 @@ impl TimeSheetService<'_> {
repository.create_new_timesheet().await?;

repository
.set_time(hours, days, job, task)
.set_time(hours, days, week, job, task)
.await
.map_err(|err| {
let msg = format!(
Expand Down
Loading

0 comments on commit fa586cf

Please sign in to comment.