use derive_builder::Builder;
use reqwest::Method;
use std::borrow::Cow;
use crate::api::enumerations::TimeEntryActivityEssentials;
use crate::api::issue_categories::IssueCategoryEssentials;
use crate::api::issues::AssigneeEssentials;
use crate::api::trackers::TrackerEssentials;
use crate::api::versions::VersionEssentials;
use crate::api::{Endpoint, Pageable, QueryParams, ReturnsJsonResponse};
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Module {
pub id: u64,
pub name: String,
}
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, Clone)]
pub struct ProjectEssentials {
pub id: u64,
pub name: String,
}
impl From<Project> for ProjectEssentials {
fn from(v: Project) -> Self {
ProjectEssentials {
id: v.id,
name: v.name,
}
}
}
impl From<&Project> for ProjectEssentials {
fn from(v: &Project) -> Self {
ProjectEssentials {
id: v.id,
name: v.name.to_owned(),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct Project {
pub id: u64,
pub name: String,
pub identifier: String,
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
pub is_public: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<ProjectEssentials>,
pub inherit_members: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_assignee: Option<AssigneeEssentials>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_version: Option<VersionEssentials>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_version_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracker_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled_module_names: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issue_custom_field_id: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_field_values: Option<HashMap<u64, String>>,
pub status: u64,
#[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,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issue_categories: Option<Vec<IssueCategoryEssentials>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time_entry_activities: Option<Vec<TimeEntryActivityEssentials>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled_modules: Option<Vec<Module>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trackers: Option<Vec<TrackerEssentials>>,
}
#[derive(Debug, Clone)]
pub enum ProjectsInclude {
Trackers,
IssueCategories,
EnabledModules,
}
impl std::fmt::Display for ProjectsInclude {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Trackers => {
write!(f, "trackers")
}
Self::IssueCategories => {
write!(f, "issue_categories")
}
Self::EnabledModules => {
write!(f, "enabled_modules")
}
}
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct ListProjects {
#[builder(default)]
include: Option<Vec<ProjectsInclude>>,
}
impl ReturnsJsonResponse for ListProjects {}
impl Pageable for ListProjects {
fn response_wrapper_key(&self) -> String {
"projects".to_string()
}
}
impl ListProjects {
#[must_use]
pub fn builder() -> ListProjectsBuilder {
ListProjectsBuilder::default()
}
}
impl Endpoint for ListProjects {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
"projects.json".into()
}
fn parameters(&self) -> QueryParams {
let mut params = QueryParams::default();
params.push_opt("include", self.include.as_ref());
params
}
}
#[derive(Debug, Clone)]
pub enum ProjectInclude {
Trackers,
IssueCategories,
EnabledModules,
TimeEntryActivities,
}
impl std::fmt::Display for ProjectInclude {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Trackers => {
write!(f, "trackers")
}
Self::IssueCategories => {
write!(f, "issue_categories")
}
Self::EnabledModules => {
write!(f, "enabled_modules")
}
Self::TimeEntryActivities => {
write!(f, "time_entry_activities")
}
}
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct GetProject<'a> {
#[builder(setter(into))]
project_id_or_name: Cow<'a, str>,
#[builder(default)]
include: Option<Vec<ProjectInclude>>,
}
impl<'a> ReturnsJsonResponse for GetProject<'a> {}
impl<'a> GetProject<'a> {
#[must_use]
pub fn builder() -> GetProjectBuilder<'a> {
GetProjectBuilder::default()
}
}
impl<'a> Endpoint for GetProject<'a> {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
format!("projects/{}.json", &self.project_id_or_name).into()
}
fn parameters(&self) -> QueryParams {
let mut params = QueryParams::default();
params.push_opt("include", self.include.as_ref());
params
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct ArchiveProject<'a> {
#[builder(setter(into))]
project_id_or_name: Cow<'a, str>,
}
impl<'a> ArchiveProject<'a> {
#[must_use]
pub fn builder() -> ArchiveProjectBuilder<'a> {
ArchiveProjectBuilder::default()
}
}
impl<'a> Endpoint for ArchiveProject<'a> {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("projects/{}/archive.json", &self.project_id_or_name).into()
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct UnarchiveProject<'a> {
#[builder(setter(into))]
project_id_or_name: Cow<'a, str>,
}
impl<'a> UnarchiveProject<'a> {
#[must_use]
pub fn builder() -> UnarchiveProjectBuilder<'a> {
UnarchiveProjectBuilder::default()
}
}
impl<'a> Endpoint for UnarchiveProject<'a> {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("projects/{}/unarchive.json", &self.project_id_or_name).into()
}
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Builder, Serialize)]
#[builder(setter(strip_option))]
pub struct CreateProject<'a> {
#[builder(setter(into))]
name: Cow<'a, str>,
#[builder(setter(into))]
identifier: Cow<'a, str>,
#[builder(setter(into), default)]
description: Option<Cow<'a, str>>,
#[builder(setter(into), default)]
homepage: Option<Cow<'a, str>>,
#[builder(default)]
is_public: Option<bool>,
#[builder(default)]
parent_id: Option<u64>,
#[builder(default)]
inherit_members: Option<bool>,
#[builder(default)]
default_assigned_to_id: Option<u64>,
#[builder(default)]
default_version_id: Option<u64>,
#[builder(default)]
tracker_ids: Option<Vec<u64>>,
#[builder(default)]
enabled_module_names: Option<Vec<Cow<'a, str>>>,
#[builder(default)]
issue_custom_field_id: Option<Vec<u64>>,
#[builder(default)]
custom_field_values: Option<HashMap<u64, Cow<'a, str>>>,
}
impl<'a> ReturnsJsonResponse for CreateProject<'a> {}
impl<'a> CreateProject<'a> {
#[must_use]
pub fn builder() -> CreateProjectBuilder<'a> {
CreateProjectBuilder::default()
}
}
impl<'a> Endpoint for CreateProject<'a> {
fn method(&self) -> Method {
Method::POST
}
fn endpoint(&self) -> Cow<'static, str> {
"projects.json".into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some((
"application/json",
serde_json::to_vec(&ProjectWrapper::<CreateProject> {
project: (*self).to_owned(),
})?,
)))
}
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Builder, Serialize)]
#[builder(setter(strip_option))]
pub struct UpdateProject<'a> {
#[serde(skip_serializing)]
#[builder(setter(into))]
project_id_or_name: Cow<'a, str>,
#[builder(setter(into), default)]
name: Option<Cow<'a, str>>,
#[builder(setter(into), default)]
identifier: Option<Cow<'a, str>>,
#[builder(setter(into), default)]
description: Option<Cow<'a, str>>,
#[builder(setter(into), default)]
homepage: Option<Cow<'a, str>>,
#[builder(default)]
is_public: Option<bool>,
#[builder(default)]
parent_id: Option<u64>,
#[builder(default)]
inherit_members: Option<bool>,
#[builder(default)]
default_assigned_to_id: Option<u64>,
#[builder(default)]
default_version_id: Option<u64>,
#[builder(default)]
tracker_ids: Option<Vec<u64>>,
#[builder(default)]
enabled_module_names: Option<Vec<Cow<'a, str>>>,
#[builder(default)]
issue_custom_field_id: Option<Vec<u64>>,
#[builder(default)]
custom_field_values: Option<HashMap<u64, Cow<'a, str>>>,
}
impl<'a> UpdateProject<'a> {
#[must_use]
pub fn builder() -> UpdateProjectBuilder<'a> {
UpdateProjectBuilder::default()
}
}
impl<'a> Endpoint for UpdateProject<'a> {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("projects/{}.json", self.project_id_or_name).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some((
"application/json",
serde_json::to_vec(&ProjectWrapper::<UpdateProject> {
project: (*self).to_owned(),
})?,
)))
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct DeleteProject<'a> {
#[builder(setter(into))]
project_id_or_name: Cow<'a, str>,
}
impl<'a> DeleteProject<'a> {
#[must_use]
pub fn builder() -> DeleteProjectBuilder<'a> {
DeleteProjectBuilder::default()
}
}
impl<'a> Endpoint for DeleteProject<'a> {
fn method(&self) -> Method {
Method::DELETE
}
fn endpoint(&self) -> Cow<'static, str> {
format!("projects/{}.json", &self.project_id_or_name).into()
}
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct ProjectsWrapper<T> {
pub projects: Vec<T>,
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct ProjectWrapper<T> {
pub project: T,
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
use crate::api::test_helpers::with_project;
use parking_lot::{const_rwlock, RwLock};
use pretty_assertions::assert_eq;
use std::error::Error;
use tracing_test::traced_test;
pub static PROJECT_LOCK: RwLock<()> = const_rwlock(());
#[traced_test]
#[test]
fn test_list_projects_no_pagination() -> Result<(), Box<dyn Error>> {
let _r_project = PROJECT_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListProjects::builder().build()?;
redmine.json_response_body::<_, ProjectsWrapper<Project>>(&endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_list_projects_first_page() -> Result<(), Box<dyn Error>> {
let _r_project = PROJECT_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListProjects::builder().build()?;
redmine.json_response_body_page::<_, Project>(&endpoint, 0, 25)?;
Ok(())
}
#[traced_test]
#[test]
fn test_list_projects_all_pages() -> Result<(), Box<dyn Error>> {
let _r_project = PROJECT_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListProjects::builder().build()?;
redmine.json_response_body_all_pages::<_, Project>(&endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_get_project() -> Result<(), Box<dyn Error>> {
let _r_project = PROJECT_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = GetProject::builder()
.project_id_or_name("sandbox")
.build()?;
redmine.json_response_body::<_, ProjectWrapper<Project>>(&endpoint)?;
Ok(())
}
#[function_name::named]
#[traced_test]
#[test]
fn test_create_project() -> Result<(), Box<dyn Error>> {
let name = format!("unittest_{}", function_name!());
with_project(&name, |_, _, _| Ok(()))?;
Ok(())
}
#[function_name::named]
#[traced_test]
#[test]
fn test_update_project() -> Result<(), Box<dyn Error>> {
let name = format!("unittest_{}", function_name!());
with_project(&name, |redmine, _id, name| {
let update_endpoint = super::UpdateProject::builder()
.project_id_or_name(name)
.description("Test-Description")
.build()?;
redmine.ignore_response_body::<_>(&update_endpoint)?;
Ok(())
})?;
Ok(())
}
#[traced_test]
#[test]
fn test_completeness_project_type() -> Result<(), Box<dyn Error>> {
let _r_project = PROJECT_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListProjects::builder().build()?;
let ProjectsWrapper { projects: values } =
redmine.json_response_body::<_, ProjectsWrapper<serde_json::Value>>(&endpoint)?;
for value in values {
let o: Project = serde_json::from_value(value.clone())?;
let reserialized = serde_json::to_value(o)?;
assert_eq!(value, reserialized);
}
Ok(())
}
#[traced_test]
#[test]
fn test_completeness_project_type_all_pages_all_project_details() -> Result<(), Box<dyn Error>>
{
let _r_project = PROJECT_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListProjects::builder()
.include(vec![
ProjectsInclude::Trackers,
ProjectsInclude::IssueCategories,
ProjectsInclude::EnabledModules,
])
.build()?;
let projects = redmine.json_response_body_all_pages::<_, Project>(&endpoint)?;
for project in projects {
let get_endpoint = GetProject::builder()
.project_id_or_name(project.id.to_string())
.include(vec![
ProjectInclude::Trackers,
ProjectInclude::IssueCategories,
ProjectInclude::EnabledModules,
ProjectInclude::TimeEntryActivities,
])
.build()?;
let ProjectWrapper { project: value } = redmine
.json_response_body::<_, ProjectWrapper<serde_json::Value>>(&get_endpoint)?;
let o: Project = serde_json::from_value(value.clone())?;
let reserialized = serde_json::to_value(o)?;
assert_eq!(value, reserialized);
}
Ok(())
}
}