use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use tracing::info;
use url::form_urlencoded;
use crate::{Board, EmptyResponse, Jira, Result, SearchOptions};
#[derive(Debug)]
pub struct Sprints {
jira: Jira,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
pub struct Sprint {
pub id: u64,
#[serde(rename = "self")]
pub self_link: String,
pub name: String,
pub state: Option<String>,
#[serde(default, rename = "startDate", with = "time::serde::iso8601::option")]
pub start_date: Option<OffsetDateTime>,
#[serde(default, rename = "endDate", with = "time::serde::iso8601::option")]
pub end_date: Option<OffsetDateTime>,
#[serde(
default,
rename = "completeDate",
with = "time::serde::iso8601::option"
)]
pub complete_date: Option<OffsetDateTime>,
#[serde(rename = "originBoardId")]
pub origin_board_id: Option<u64>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
struct CreateSprint {
pub name: String,
#[serde(rename = "originBoardId")]
pub origin_board_id: Option<u64>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Clone)]
pub struct UpdateSprint {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "startDate",
with = "crate::rep::jira_datetime"
)]
pub start_date: Option<OffsetDateTime>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "endDate",
with = "crate::rep::jira_datetime"
)]
pub end_date: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>, }
#[derive(Deserialize, Debug)]
pub struct SprintResults {
#[serde(rename = "maxResults")]
pub max_results: u64,
#[serde(rename = "startAt")]
pub start_at: u64,
#[serde(rename = "isLast")]
pub is_last: bool,
pub values: Vec<Sprint>,
}
#[derive(Serialize, Debug)]
struct MoveIssues {
issues: Vec<String>,
}
impl Sprints {
pub fn new(jira: &Jira) -> Sprints {
Sprints { jira: jira.clone() }
}
pub fn create<T: Into<String>>(&self, board: Board, name: T) -> Result<Sprint> {
let data: CreateSprint = CreateSprint {
name: name.into(),
origin_board_id: Some(board.id),
};
info!("{:?}", data);
self.jira.post("agile", "/sprint", data)
}
pub fn get<I>(&self, id: I) -> Result<Sprint>
where
I: Into<String>,
{
self.jira.get("agile", &format!("/sprint/{}", id.into()))
}
pub fn move_issues(&self, sprint_id: u64, issues: Vec<String>) -> Result<EmptyResponse> {
let path = format!("/sprint/{sprint_id}/issue");
let data = MoveIssues { issues };
self.jira.post("agile", &path, data)
}
pub fn list(&self, board: &Board, options: &SearchOptions) -> Result<SprintResults> {
let mut path = vec![format!("/board/{}/sprint", board.id)];
let query_options = options.serialize().unwrap_or_default();
let query = form_urlencoded::Serializer::new(query_options).finish();
path.push(query);
self.jira
.get::<SprintResults>("agile", path.join("?").as_ref())
}
pub fn iter<'a>(
&self,
board: &'a Board,
options: &'a SearchOptions,
) -> Result<SprintsIter<'a>> {
SprintsIter::new(board, options, &self.jira)
}
pub fn update<I>(&self, id: I, data: UpdateSprint) -> Result<Sprint>
where
I: Into<u64>,
{
self.jira
.post("agile", &format!("/sprint/{}", id.into()), data)
}
pub fn delete<I>(&self, id: I) -> Result<()>
where
I: Into<u64>,
{
self.jira
.delete::<EmptyResponse>("agile", &format!("/sprint/{}", id.into()))?;
Ok(())
}
}
#[derive(Debug)]
pub struct SprintsIter<'a> {
jira: Jira,
board: &'a Board,
results: SprintResults,
search_options: &'a SearchOptions,
}
impl<'a> SprintsIter<'a> {
fn new(board: &'a Board, options: &'a SearchOptions, jira: &Jira) -> Result<Self> {
let results = jira.sprints().list(board, options)?;
Ok(SprintsIter {
board,
jira: jira.clone(),
results,
search_options: options,
})
}
fn more(&self) -> bool {
!self.results.is_last
}
}
impl Iterator for SprintsIter<'_> {
type Item = Sprint;
fn next(&mut self) -> Option<Sprint> {
self.results.values.pop().or_else(|| {
if self.more() {
match self.jira.sprints().list(
self.board,
&self
.search_options
.as_builder()
.max_results(self.results.max_results)
.start_at(self.results.start_at + self.results.max_results)
.build(),
) {
Ok(new_results) => {
self.results = new_results;
self.results.values.pop()
}
Err(e) => {
tracing::error!("Sprints pagination failed: {}", e);
None
}
}
} else {
None
}
})
}
}
#[cfg(feature = "async")]
use crate::r#async::Jira as AsyncJira;
#[cfg(feature = "async")]
#[derive(Debug)]
pub struct AsyncSprints {
jira: AsyncJira,
}
#[cfg(feature = "async")]
impl AsyncSprints {
pub fn new(jira: &AsyncJira) -> AsyncSprints {
AsyncSprints { jira: jira.clone() }
}
pub async fn create<T: Into<String>>(&self, board: Board, name: T) -> Result<Sprint> {
let data: CreateSprint = CreateSprint {
name: name.into(),
origin_board_id: Some(board.id),
};
self.jira.post("agile", "/sprint", data).await
}
pub async fn get<I>(&self, id: I) -> Result<Sprint>
where
I: Into<String>,
{
self.jira
.get("agile", &format!("/sprint/{}", id.into()))
.await
}
pub async fn move_issues(&self, sprint_id: u64, issues: Vec<String>) -> Result<EmptyResponse> {
let path = format!("/sprint/{sprint_id}/issue");
let data = MoveIssues { issues };
self.jira.post("agile", &path, data).await
}
pub async fn list(&self, board: &Board, options: &SearchOptions) -> Result<SprintResults> {
let mut path = vec![format!("/board/{}/sprint", board.id)];
let query_options = options.serialize().unwrap_or_default();
let query = form_urlencoded::Serializer::new(query_options).finish();
path.push(query);
self.jira
.get::<SprintResults>("agile", path.join("?").as_ref())
.await
}
pub async fn update<I>(&self, id: I, data: UpdateSprint) -> Result<Sprint>
where
I: Into<u64>,
{
self.jira
.post("agile", &format!("/sprint/{}", id.into()), data)
.await
}
pub async fn delete<I>(&self, id: I) -> Result<()>
where
I: Into<u64>,
{
self.jira
.delete::<EmptyResponse>("agile", &format!("/sprint/{}", id.into()))
.await?;
Ok(())
}
}