use std::collections::BTreeSet;
use chrono::{DateTime, NaiveDate, Utc};
use derive_builder::Builder;
use itertools::Itertools;
use crate::api::common::NameOrId;
use crate::api::endpoint_prelude::*;
use crate::api::ParamValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueStateEvent {
Close,
Reopen,
}
impl IssueStateEvent {
pub(crate) fn as_str(self) -> &'static str {
match self {
IssueStateEvent::Close => "close",
IssueStateEvent::Reopen => "reopen",
}
}
}
impl ParamValue<'static> for IssueStateEvent {
fn as_value(self) -> Cow<'static, str> {
self.as_str().into()
}
}
#[derive(Debug, Clone)]
enum IssueAssignees {
Unassigned,
Assignees(BTreeSet<u64>),
}
#[derive(Debug, Clone)]
enum IssueLabels<'a> {
Unlabeled,
Labeled(BTreeSet<Cow<'a, str>>),
}
impl<'a, 'b: 'a> ParamValue<'a> for &'b IssueLabels<'a> {
fn as_value(self) -> Cow<'a, str> {
match self {
IssueLabels::Unlabeled => "".into(),
IssueLabels::Labeled(labels) => format!("{}", labels.iter().format(",")).into(),
}
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct EditIssue<'a> {
#[builder(setter(into))]
project: NameOrId<'a>,
issue: u64,
#[builder(setter(into), default)]
title: Option<Cow<'a, str>>,
#[builder(setter(into), default)]
description: Option<Cow<'a, str>>,
#[builder(setter(name = "_assignee_ids"), default, private)]
assignee_ids: Option<IssueAssignees>,
#[builder(default)]
milestone_id: Option<u64>,
#[builder(setter(name = "_labels"), default, private)]
labels: Option<IssueLabels<'a>>,
#[builder(default)]
state_event: Option<IssueStateEvent>,
#[builder(default)]
updated_at: Option<DateTime<Utc>>,
#[builder(default)]
due_date: Option<NaiveDate>,
#[builder(default)]
weight: Option<u64>,
#[builder(default)]
discussion_locked: Option<bool>,
#[builder(default)]
epic_id: Option<u64>,
#[deprecated(note = "use `epic_id` instead")]
#[builder(default)]
epic_iid: Option<u64>,
}
impl<'a> EditIssue<'a> {
pub fn builder() -> EditIssueBuilder<'a> {
EditIssueBuilder::default()
}
}
impl<'a> EditIssueBuilder<'a> {
#[deprecated(note = "use `issue` instead")]
pub fn issue_iid(&mut self, issue_iid: u64) -> &mut Self {
self.issue = Some(issue_iid);
self
}
pub fn unassign(&mut self) -> &mut Self {
self.assignee_ids = Some(Some(IssueAssignees::Unassigned));
self
}
pub fn assignee_id(&mut self, assignee: u64) -> &mut Self {
let assignees =
if let Some(Some(IssueAssignees::Assignees(mut set))) = self.assignee_ids.take() {
set.insert(assignee);
set
} else {
let mut set = BTreeSet::new();
set.insert(assignee);
set
};
self.assignee_ids = Some(Some(IssueAssignees::Assignees(assignees)));
self
}
pub fn assignee_ids<I>(&mut self, iter: I) -> &mut Self
where
I: Iterator<Item = u64>,
{
let assignees =
if let Some(Some(IssueAssignees::Assignees(mut set))) = self.assignee_ids.take() {
set.extend(iter);
set
} else {
iter.collect()
};
self.assignee_ids = Some(Some(IssueAssignees::Assignees(assignees)));
self
}
pub fn remove_labels(&mut self) -> &mut Self {
self.labels = Some(Some(IssueLabels::Unlabeled));
self
}
pub fn label<L>(&mut self, label: L) -> &mut Self
where
L: Into<Cow<'a, str>>,
{
let label = label.into();
let labels = if let Some(Some(IssueLabels::Labeled(mut set))) = self.labels.take() {
set.insert(label);
set
} else {
let mut set = BTreeSet::new();
set.insert(label);
set
};
self.labels = Some(Some(IssueLabels::Labeled(labels)));
self
}
pub fn labels<I, L>(&mut self, iter: I) -> &mut Self
where
I: IntoIterator<Item = L>,
L: Into<Cow<'a, str>>,
{
let iter = iter.into_iter().map(Into::into);
let labels = if let Some(Some(IssueLabels::Labeled(mut set))) = self.labels.take() {
set.extend(iter);
set
} else {
iter.collect()
};
self.labels = Some(Some(IssueLabels::Labeled(labels)));
self
}
}
impl<'a> Endpoint for EditIssue<'a> {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("projects/{}/issues/{}", self.project, self.issue).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
let mut params = FormParams::default();
params
.push_opt("title", self.title.as_ref())
.push_opt("description", self.description.as_ref())
.push_opt("milestone_id", self.milestone_id)
.push_opt("labels", self.labels.as_ref())
.push_opt("state_event", self.state_event)
.push_opt("updated_at", self.updated_at)
.push_opt("due_date", self.due_date)
.push_opt("weight", self.weight)
.push_opt("discussion_locked", self.discussion_locked)
.push_opt("epic_id", self.epic_id);
if let Some(assignees) = self.assignee_ids.as_ref() {
match assignees {
IssueAssignees::Unassigned => {
params.push("assignee_ids[]", "0");
},
IssueAssignees::Assignees(ids) => {
params.extend(ids.iter().map(|&value| ("assignee_ids[]", value)));
},
}
}
#[allow(deprecated)]
{
params.push_opt("epic_iid", self.epic_iid);
}
params.into_body()
}
}
#[cfg(test)]
mod tests {
use chrono::{NaiveDate, TimeZone, Utc};
use http::Method;
use crate::api::projects::issues::{EditIssue, IssueStateEvent};
use crate::api::{self, Query};
use crate::test::client::{ExpectedUrl, SingleTestClient};
#[test]
fn issue_state_event_as_str() {
let items = &[
(IssueStateEvent::Close, "close"),
(IssueStateEvent::Reopen, "reopen"),
];
for (i, s) in items {
assert_eq!(i.as_str(), *s);
}
}
#[test]
fn project_and_issue_are_necessary() {
let err = EditIssue::builder().build().unwrap_err();
assert_eq!(err, "`project` must be initialized");
}
#[test]
fn project_is_necessary() {
let err = EditIssue::builder().issue(1).build().unwrap_err();
assert_eq!(err, "`project` must be initialized");
}
#[test]
fn issue_is_necessary() {
let err = EditIssue::builder().project(1).build().unwrap_err();
assert_eq!(err, "`issue` must be initialized");
}
#[test]
fn project_and_issue_are_sufficient() {
EditIssue::builder().project(1).issue(1).build().unwrap();
}
#[test]
#[allow(deprecated)]
fn project_and_issue_iid_are_sufficient() {
EditIssue::builder()
.project(1)
.issue_iid(1)
.build()
.unwrap();
}
#[test]
fn endpoint() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_title() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("title=title")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.title("title")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_description() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("description=description")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.description("description")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_assignee_ids() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str(concat!("assignee_ids%5B%5D=1", "&assignee_ids%5B%5D=2"))
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.assignee_id(1)
.assignee_ids([1, 2].iter().copied())
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_assignee_ids_unassign() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("assignee_ids%5B%5D=0")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.unassign()
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_milestone_id() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("milestone_id=1")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.milestone_id(1)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_labels() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("labels=label")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.label("label")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_labels_multiple() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("labels=label1%2Clabel2")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.labels(["label1", "label2"].iter().copied())
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_labels_remove() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("labels=")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.remove_labels()
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_state_event() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("state_event=close")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.state_event(IssueStateEvent::Close)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_updated_at() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("updated_at=2020-01-01T00%3A00%3A00Z")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.updated_at(Utc.ymd(2020, 1, 1).and_hms_milli(0, 0, 0, 0))
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_due_date() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("due_date=2020-01-01")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.due_date(NaiveDate::from_ymd(2020, 1, 1))
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_weight() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("weight=1")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.weight(1)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_discussion_locked() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("discussion_locked=true")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.discussion_locked(true)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_epic_id() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("epic_id=1")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.epic_id(1)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
#[allow(deprecated)]
fn endpoint_epic_iid() {
let endpoint = ExpectedUrl::builder()
.method(Method::PUT)
.endpoint("projects/simple%2Fproject/issues/1")
.content_type("application/x-www-form-urlencoded")
.body_str("epic_iid=1")
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = EditIssue::builder()
.project("simple/project")
.issue(1)
.epic_iid(1)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
}