use derive_builder::Builder;
use reqwest::Method;
use std::borrow::Cow;
use crate::api::custom_fields::CustomFieldEssentialsWithValue;
use crate::api::enumerations::TimeEntryActivityEssentials;
use crate::api::issues::IssueEssentials;
use crate::api::projects::ProjectEssentials;
use crate::api::users::UserEssentials;
use crate::api::{Endpoint, Pageable, QueryParams, ReturnsJsonResponse};
use serde::Serialize;
#[derive(Debug, Serialize, serde::Deserialize)]
pub struct TimeEntry {
pub id: u64,
pub user: UserEssentials,
pub hours: f64,
pub activity: TimeEntryActivityEssentials,
#[serde(default)]
pub comments: Option<String>,
pub issue: Option<IssueEssentials>,
pub project: Option<ProjectEssentials>,
pub spent_on: Option<time::Date>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
#[serde(
serialize_with = "crate::api::serialize_rfc3339",
deserialize_with = "crate::api::deserialize_rfc3339"
)]
pub created_on: time::OffsetDateTime,
#[serde(
serialize_with = "crate::api::serialize_rfc3339",
deserialize_with = "crate::api::deserialize_rfc3339"
)]
pub updated_on: time::OffsetDateTime,
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct ListTimeEntries<'a> {
#[builder(default)]
user_id: Option<u64>,
#[builder(setter(into), default)]
project_id_or_name: Option<Cow<'a, str>>,
#[builder(default)]
issue_id: Option<u64>,
#[builder(default)]
activity_id: Option<u64>,
#[builder(default)]
spent_on: Option<time::Date>,
#[builder(default)]
from: Option<time::Date>,
#[builder(default)]
to: Option<time::Date>,
}
impl<'a> ReturnsJsonResponse for ListTimeEntries<'a> {}
impl<'a> Pageable for ListTimeEntries<'a> {
fn response_wrapper_key(&self) -> String {
"time_entries".to_string()
}
}
impl<'a> ListTimeEntries<'a> {
#[must_use]
pub fn builder() -> ListTimeEntriesBuilder<'a> {
ListTimeEntriesBuilder::default()
}
}
impl<'a> Endpoint for ListTimeEntries<'a> {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
"time_entries.json".into()
}
fn parameters(&self) -> QueryParams {
let mut params = QueryParams::default();
params.push_opt("user_id", self.user_id);
params.push_opt("project_id", self.project_id_or_name.as_ref());
params.push_opt("issue_id", self.issue_id);
params.push_opt("activity_id", self.activity_id);
params.push_opt("spent_on", self.spent_on);
params.push_opt("from", self.from);
params.push_opt("to", self.to);
params
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct GetTimeEntry {
id: u64,
}
impl ReturnsJsonResponse for GetTimeEntry {}
impl GetTimeEntry {
#[must_use]
pub fn builder() -> GetTimeEntryBuilder {
GetTimeEntryBuilder::default()
}
}
impl Endpoint for GetTimeEntry {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
format!("time_entries/{}.json", self.id).into()
}
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Builder, Serialize)]
#[builder(setter(strip_option), build_fn(validate = "Self::validate"))]
pub struct CreateTimeEntry<'a> {
#[builder(default)]
issue_id: Option<u64>,
#[builder(default)]
project_id: Option<u64>,
#[builder(default)]
spent_on: Option<time::Date>,
hours: f64,
#[builder(default)]
activity_id: Option<u64>,
#[builder(default)]
comments: Option<Cow<'a, str>>,
#[builder(default)]
user_id: Option<u64>,
}
impl<'a> ReturnsJsonResponse for CreateTimeEntry<'a> {}
impl<'a> CreateTimeEntryBuilder<'a> {
fn validate(&self) -> Result<(), String> {
if self.issue_id.is_none() && self.project_id.is_none() {
Err("Either issue_id or project_id need to be specified".to_string())
} else {
Ok(())
}
}
}
impl<'a> CreateTimeEntry<'a> {
#[must_use]
pub fn builder() -> CreateTimeEntryBuilder<'a> {
CreateTimeEntryBuilder::default()
}
}
impl<'a> Endpoint for CreateTimeEntry<'a> {
fn method(&self) -> Method {
Method::POST
}
fn endpoint(&self) -> Cow<'static, str> {
"time_entries.json".into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some((
"application/json",
serde_json::to_vec(&TimeEntryWrapper::<CreateTimeEntry> {
time_entry: (*self).to_owned(),
})?,
)))
}
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Builder, Serialize)]
#[builder(setter(strip_option))]
pub struct UpdateTimeEntry<'a> {
#[serde(skip_serializing)]
id: u64,
#[builder(default)]
issue_id: Option<u64>,
#[builder(default)]
project_id: Option<u64>,
#[builder(default)]
spent_on: Option<time::Date>,
#[builder(default)]
hours: Option<f64>,
#[builder(default)]
activity_id: Option<u64>,
#[builder(default)]
comments: Option<Cow<'a, str>>,
#[builder(default)]
user_id: Option<u64>,
}
impl<'a> UpdateTimeEntry<'a> {
#[must_use]
pub fn builder() -> UpdateTimeEntryBuilder<'a> {
UpdateTimeEntryBuilder::default()
}
}
impl<'a> Endpoint for UpdateTimeEntry<'a> {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("time_entries/{}.json", self.id).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some((
"application/json",
serde_json::to_vec(&TimeEntryWrapper::<UpdateTimeEntry> {
time_entry: (*self).to_owned(),
})?,
)))
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct DeleteTimeEntry {
id: u64,
}
impl DeleteTimeEntry {
#[must_use]
pub fn builder() -> DeleteTimeEntryBuilder {
DeleteTimeEntryBuilder::default()
}
}
impl Endpoint for DeleteTimeEntry {
fn method(&self) -> Method {
Method::DELETE
}
fn endpoint(&self) -> Cow<'static, str> {
format!("time_entries/{}.json", &self.id).into()
}
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct TimeEntriesWrapper<T> {
pub time_entries: Vec<T>,
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct TimeEntryWrapper<T> {
pub time_entry: T,
}
#[cfg(test)]
mod test {
use super::*;
use parking_lot::{const_rwlock, RwLock};
use pretty_assertions::assert_eq;
use std::error::Error;
use tracing_test::traced_test;
static TIME_ENTRY_LOCK: RwLock<()> = const_rwlock(());
#[traced_test]
#[test]
fn test_list_time_entries_no_pagination() -> Result<(), Box<dyn Error>> {
let _r_time_entries = TIME_ENTRY_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListTimeEntries::builder().build()?;
redmine.json_response_body::<_, TimeEntriesWrapper<TimeEntry>>(&endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_list_time_entries_first_page() -> Result<(), Box<dyn Error>> {
let _r_time_entries = TIME_ENTRY_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListTimeEntries::builder().build()?;
redmine.json_response_body_page::<_, TimeEntry>(&endpoint, 0, 25)?;
Ok(())
}
#[traced_test]
#[test]
fn test_get_time_entry() -> Result<(), Box<dyn Error>> {
let _r_time_entries = TIME_ENTRY_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = GetTimeEntry::builder().id(832).build()?;
redmine.json_response_body::<_, TimeEntryWrapper<TimeEntry>>(&endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_create_time_entry() -> Result<(), Box<dyn Error>> {
let _w_time_entries = TIME_ENTRY_LOCK.write();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let create_endpoint = super::CreateTimeEntry::builder()
.issue_id(25095)
.hours(1.0)
.activity_id(8)
.build()?;
redmine.json_response_body::<_, TimeEntryWrapper<TimeEntry>>(&create_endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_update_time_entry() -> Result<(), Box<dyn Error>> {
let _w_time_entries = TIME_ENTRY_LOCK.write();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let create_endpoint = super::CreateTimeEntry::builder()
.issue_id(25095)
.hours(1.0)
.activity_id(8)
.build()?;
let TimeEntryWrapper { time_entry } =
redmine.json_response_body::<_, TimeEntryWrapper<TimeEntry>>(&create_endpoint)?;
let update_endpoint = super::UpdateTimeEntry::builder()
.id(time_entry.id)
.hours(2.0)
.build()?;
redmine.ignore_response_body::<_>(&update_endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_completeness_time_entry_type() -> Result<(), Box<dyn Error>> {
let _r_time_entries = TIME_ENTRY_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListTimeEntries::builder().build()?;
let TimeEntriesWrapper {
time_entries: values,
} = redmine.json_response_body::<_, TimeEntriesWrapper<serde_json::Value>>(&endpoint)?;
for value in values {
let o: TimeEntry = serde_json::from_value(value.clone())?;
let reserialized = serde_json::to_value(o)?;
assert_eq!(value, reserialized);
}
Ok(())
}
}