use derive_builder::Builder;
use reqwest::Method;
use std::borrow::Cow;
use crate::api::{Endpoint, ReturnsJsonResponse};
use serde::Serialize;
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct IssueRelation {
pub id: u64,
pub issue_id: u64,
pub issue_to_id: u64,
pub relation_type: IssueRelationType,
pub delay: Option<u64>,
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct ListIssueRelations {
issue_id: u64,
}
impl ReturnsJsonResponse for ListIssueRelations {}
impl ListIssueRelations {
#[must_use]
pub fn builder() -> ListIssueRelationsBuilder {
ListIssueRelationsBuilder::default()
}
}
impl Endpoint for ListIssueRelations {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
format!("issues/{}/relations.json", self.issue_id).into()
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct GetIssueRelation {
id: u64,
}
impl ReturnsJsonResponse for GetIssueRelation {}
impl GetIssueRelation {
#[must_use]
pub fn builder() -> GetIssueRelationBuilder {
GetIssueRelationBuilder::default()
}
}
impl Endpoint for GetIssueRelation {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
format!("relations/{}.json", self.id).into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IssueRelationType {
Relates,
Duplicates,
Duplicated,
Blocks,
Blocked,
Precedes,
Follows,
CopiedTo,
CopiedFrom,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Builder, Serialize)]
#[builder(setter(strip_option))]
pub struct CreateIssueRelation {
#[serde(skip_serializing)]
issue_id: u64,
issue_to_id: u64,
relation_type: IssueRelationType,
#[builder(default)]
delay: Option<u64>,
}
impl ReturnsJsonResponse for CreateIssueRelation {}
impl CreateIssueRelation {
#[must_use]
pub fn builder() -> CreateIssueRelationBuilder {
CreateIssueRelationBuilder::default()
}
}
impl Endpoint for CreateIssueRelation {
fn method(&self) -> Method {
Method::POST
}
fn endpoint(&self) -> Cow<'static, str> {
format!("issues/{}/relations.json", self.issue_id).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some((
"application/json",
serde_json::to_vec(&RelationWrapper::<CreateIssueRelation> {
relation: (*self).to_owned(),
})?,
)))
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct DeleteIssueRelation {
id: u64,
}
impl DeleteIssueRelation {
#[must_use]
pub fn builder() -> DeleteIssueRelationBuilder {
DeleteIssueRelationBuilder::default()
}
}
impl Endpoint for DeleteIssueRelation {
fn method(&self) -> Method {
Method::DELETE
}
fn endpoint(&self) -> Cow<'static, str> {
format!("relations/{}.json", self.id).into()
}
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct RelationsWrapper<T> {
pub relations: Vec<T>,
}
#[derive(Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct RelationWrapper<T> {
pub relation: T,
}
#[cfg(test)]
mod test {
use super::*;
use crate::api::issues::test::ISSUES_LOCK;
use crate::api::issues::{CreateIssue, Issue, IssueWrapper};
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;
static ISSUE_RELATION_LOCK: RwLock<()> = const_rwlock(());
#[traced_test]
#[test]
fn test_list_issue_relations_no_pagination() -> Result<(), Box<dyn Error>> {
let _r_issue_relation = ISSUE_RELATION_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListIssueRelations::builder().issue_id(50017).build()?;
redmine.json_response_body::<_, RelationsWrapper<IssueRelation>>(&endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_get_issue_relation() -> Result<(), Box<dyn Error>> {
let _r_issue_relation = ISSUE_RELATION_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = GetIssueRelation::builder().id(10).build()?;
redmine.json_response_body::<_, RelationWrapper<IssueRelation>>(&endpoint)?;
Ok(())
}
#[function_name::named]
#[traced_test]
#[test]
fn test_create_issue_relation() -> Result<(), Box<dyn Error>> {
let _w_issues = ISSUES_LOCK.write();
let _w_issue_relation = ISSUE_RELATION_LOCK.write();
let name = format!("unittest_{}", function_name!());
with_project(&name, |redmine, project_id, _name| {
let create_issue1_endpoint = CreateIssue::builder()
.project_id(project_id)
.subject("Test issue 1")
.build()?;
let IssueWrapper { issue: issue1 }: IssueWrapper<Issue> =
redmine.json_response_body::<_, _>(&create_issue1_endpoint)?;
let create_issue2_endpoint = CreateIssue::builder()
.project_id(project_id)
.subject("Test issue 2")
.build()?;
let IssueWrapper { issue: issue2 }: IssueWrapper<Issue> =
redmine.json_response_body::<_, _>(&create_issue2_endpoint)?;
let create_endpoint = super::CreateIssueRelation::builder()
.issue_id(issue1.id)
.issue_to_id(issue2.id)
.relation_type(IssueRelationType::Relates)
.build()?;
redmine.json_response_body::<_, RelationWrapper<IssueRelation>>(&create_endpoint)?;
Ok(())
})?;
Ok(())
}
#[function_name::named]
#[traced_test]
#[test]
fn test_delete_issue_relation() -> Result<(), Box<dyn Error>> {
let _w_issues = ISSUES_LOCK.write();
let _w_issue_relation = ISSUE_RELATION_LOCK.write();
let name = format!("unittest_{}", function_name!());
with_project(&name, |redmine, project_id, _name| {
let create_issue1_endpoint = CreateIssue::builder()
.project_id(project_id)
.subject("Test issue 1")
.build()?;
let IssueWrapper { issue: issue1 }: IssueWrapper<Issue> =
redmine.json_response_body::<_, _>(&create_issue1_endpoint)?;
let create_issue2_endpoint = CreateIssue::builder()
.project_id(project_id)
.subject("Test issue 2")
.build()?;
let IssueWrapper { issue: issue2 }: IssueWrapper<Issue> =
redmine.json_response_body::<_, _>(&create_issue2_endpoint)?;
let create_endpoint = super::CreateIssueRelation::builder()
.issue_id(issue1.id)
.issue_to_id(issue2.id)
.relation_type(IssueRelationType::Relates)
.build()?;
let RelationWrapper { relation }: RelationWrapper<IssueRelation> =
redmine.json_response_body::<_, _>(&create_endpoint)?;
let id = relation.id;
let delete_endpoint = super::DeleteIssueRelation::builder().id(id).build()?;
redmine.ignore_response_body::<_>(&delete_endpoint)?;
Ok(())
})?;
Ok(())
}
#[traced_test]
#[test]
fn test_completeness_issue_relation_type() -> Result<(), Box<dyn Error>> {
let _r_issue_relation = ISSUE_RELATION_LOCK.read();
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env()?;
let endpoint = ListIssueRelations::builder().issue_id(50017).build()?;
let RelationsWrapper { relations: values } =
redmine.json_response_body::<_, RelationsWrapper<serde_json::Value>>(&endpoint)?;
for value in values {
let o: IssueRelation = serde_json::from_value(value.clone())?;
let reserialized = serde_json::to_value(o)?;
assert_eq!(value, reserialized);
}
Ok(())
}
}