use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::collections::BTreeMap;
use time::{OffsetDateTime, format_description::well_known::Iso8601};
use tracing::error;
use crate::{Jira, Result};
pub(crate) mod jira_datetime {
use serde::{Deserialize, Deserializer, Serializer};
use time::OffsetDateTime;
pub fn serialize<S>(dt: &Option<OffsetDateTime>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match dt {
Some(dt) => {
let format = time::format_description::parse(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory][offset_minute]"
).map_err(serde::ser::Error::custom)?;
let formatted = dt.format(&format).map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&formatted)
}
None => serializer.serialize_none(),
}
}
#[allow(dead_code)]
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?
.map(|s| {
let format = time::format_description::parse(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory][offset_minute]"
).map_err(|e| serde::de::Error::custom(format!("Format parse error: {}", e)))?;
OffsetDateTime::parse(&s, &format)
.or_else(|_| OffsetDateTime::parse(&s, &time::format_description::well_known::Iso8601::DEFAULT))
.map_err(|e| serde::de::Error::custom(format!("Date parse error: {}", e)))
})
.transpose()
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Errors {
#[serde(rename = "errorMessages", default)]
pub error_messages: Vec<String>,
#[serde(default)]
pub errors: BTreeMap<String, String>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Issue {
#[serde(rename = "self")]
pub self_link: String,
pub key: String,
pub id: String,
pub fields: BTreeMap<String, ::serde_json::Value>,
}
impl Issue {
pub fn field<F>(&self, name: &str) -> Option<Result<F>>
where
for<'de> F: Deserialize<'de>,
{
self.fields
.get(name)
.map(|value| Ok(serde_json::value::from_value::<F>(value.clone())?))
}
fn user_field(&self, name: &str) -> Option<Result<User>> {
self.field::<User>(name)
}
fn string_field(&self, name: &str) -> Option<Result<String>> {
self.field::<String>(name)
}
pub fn assignee(&self) -> Option<User> {
self.user_field("assignee").and_then(|value| value.ok())
}
pub fn creator(&self) -> Option<User> {
self.user_field("creator").and_then(|value| value.ok())
}
pub fn reporter(&self) -> Option<User> {
self.user_field("reporter").and_then(|value| value.ok())
}
pub fn status(&self) -> Option<Status> {
self.field::<Status>("status").and_then(|value| value.ok())
}
pub fn summary(&self) -> Option<String> {
self.string_field("summary").and_then(|value| value.ok())
}
pub fn description(&self) -> Option<String> {
if let Some(Ok(desc)) = self.string_field("description") {
return Some(desc);
}
if let Some(Ok(adf)) = self.field::<AdfDocument>("description") {
let plain_text = adf.to_plain_text();
return if plain_text.is_empty() {
None
} else {
Some(plain_text)
};
}
None
}
pub fn environment(&self) -> Option<String> {
if let Some(Ok(env)) = self.string_field("environment") {
return Some(env);
}
if let Some(Ok(adf)) = self.field::<AdfDocument>("environment") {
let plain_text = adf.to_plain_text();
return if plain_text.is_empty() {
None
} else {
Some(plain_text)
};
}
None
}
fn extract_offset_date_time(&self, field: &str) -> Option<OffsetDateTime> {
match self.string_field(field) {
Some(Ok(created)) => match OffsetDateTime::parse(created.as_ref(), &Iso8601::DEFAULT) {
Ok(offset_date_time) => Some(offset_date_time),
Err(error) => {
error!(
"Can't convert '{} = {:?}' into a OffsetDateTime. {:?}",
field, created, error
);
None
}
},
_ => None,
}
}
pub fn updated(&self) -> Option<OffsetDateTime> {
self.extract_offset_date_time("updated")
}
pub fn created(&self) -> Option<OffsetDateTime> {
self.extract_offset_date_time("created")
}
pub fn resolution_date(&self) -> Option<OffsetDateTime> {
self.extract_offset_date_time("resolutiondate")
}
pub fn issue_type(&self) -> Option<IssueType> {
self.field::<IssueType>("issuetype")
.and_then(|value| value.ok())
}
pub fn labels(&self) -> Vec<String> {
self.field::<Vec<String>>("labels")
.and_then(|value| value.ok())
.unwrap_or_default()
}
pub fn fix_versions(&self) -> Vec<Version> {
self.field::<Vec<Version>>("fixVersions")
.and_then(|value| value.ok())
.unwrap_or_default()
}
pub fn priority(&self) -> Option<Priority> {
self.field::<Priority>("priority")
.and_then(|value| value.ok())
}
pub fn links(&self) -> Option<Result<Vec<IssueLink>>> {
self.field::<Vec<IssueLink>>("issuelinks") }
pub fn project(&self) -> Option<Project> {
self.field::<Project>("project")
.and_then(|value| value.ok())
}
pub fn resolution(&self) -> Option<Resolution> {
self.field::<Resolution>("resolution")
.and_then(|value| value.ok())
}
pub fn attachment(&self) -> Vec<Attachment> {
self.field::<Vec<Attachment>>("attachment")
.and_then(|value| value.ok())
.unwrap_or_default()
}
pub fn comments(&self) -> Option<Comments> {
self.field::<Comments>("comment")
.and_then(|value| value.ok())
}
pub fn parent(&self) -> Option<Issue> {
self.field::<Issue>("parent").and_then(|value| value.ok())
}
pub fn timetracking(&self) -> Option<TimeTracking> {
self.field::<TimeTracking>("timetracking")
.and_then(|value| value.ok())
}
pub fn permalink(&self, jira: &Jira) -> String {
jira.host()
.join("/browse/")
.unwrap()
.join(&self.key)
.unwrap()
.to_string()
}
pub fn try_from_custom_issue<S: Serialize>(custom_issue: &S) -> serde_json::Result<Self> {
let serialized_data = serde_json::to_string(custom_issue)?;
serde_json::from_str(&serialized_data)
}
pub fn try_to_custom_issue<D: DeserializeOwned>(&self) -> serde_json::Result<D> {
let serialized_data = serde_json::to_string(self)?;
serde_json::from_str(&serialized_data)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Attachment {
pub id: String,
#[serde(rename = "self")]
pub self_link: String,
pub filename: String,
pub author: User,
pub created: String,
pub size: u64,
#[serde(rename = "mimeType")]
pub mime_type: String,
pub content: String,
pub thumbnail: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Comments {
pub comments: Vec<Comment>,
#[serde(rename = "self")]
pub self_link: String,
#[serde(rename = "maxResults")]
pub max_results: u32,
pub total: u32,
#[serde(rename = "startAt")]
pub start_at: u32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Comment {
pub id: Option<String>,
#[serde(rename = "self")]
pub self_link: String,
pub author: Option<User>,
#[serde(rename = "updateAuthor")]
pub update_author: Option<User>,
#[serde(default, with = "time::serde::iso8601::option")]
pub created: Option<OffsetDateTime>,
#[serde(default, with = "time::serde::iso8601::option")]
pub updated: Option<OffsetDateTime>,
pub body: TextContent,
pub visibility: Option<Visibility>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Visibility {
#[serde(rename = "type")]
pub visibility_type: String,
pub value: String,
}
#[derive(Clone, Debug)]
pub struct TextContent {
raw: serde_json::Value,
cached: String,
}
impl TextContent {
pub fn from_string(s: impl Into<String>) -> Self {
let text = s.into();
Self {
raw: serde_json::Value::String(text.clone()),
cached: text,
}
}
pub fn raw(&self) -> &serde_json::Value {
&self.raw
}
fn extract_text(value: &serde_json::Value) -> String {
if let Ok(text) = serde_json::from_value::<String>(value.clone()) {
return text;
}
if let Ok(adf) = serde_json::from_value::<AdfDocument>(value.clone()) {
return adf.to_plain_text();
}
String::new()
}
}
impl std::ops::Deref for TextContent {
type Target = str;
fn deref(&self) -> &str {
&self.cached
}
}
impl std::fmt::Display for TextContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.cached)
}
}
impl AsRef<str> for TextContent {
fn as_ref(&self) -> &str {
&self.cached
}
}
impl std::borrow::Borrow<str> for TextContent {
fn borrow(&self) -> &str {
&self.cached
}
}
impl PartialEq<str> for TextContent {
fn eq(&self, other: &str) -> bool {
self.cached == other
}
}
impl PartialEq<&str> for TextContent {
fn eq(&self, other: &&str) -> bool {
self.cached == *other
}
}
impl PartialEq<String> for TextContent {
fn eq(&self, other: &String) -> bool {
&self.cached == other
}
}
impl PartialEq for TextContent {
fn eq(&self, other: &Self) -> bool {
self.cached == other.cached
}
}
impl Eq for TextContent {}
impl From<String> for TextContent {
fn from(s: String) -> Self {
Self::from_string(s)
}
}
impl From<&str> for TextContent {
fn from(s: &str) -> Self {
Self::from_string(s)
}
}
impl Serialize for TextContent {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.raw.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for TextContent {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = serde_json::Value::deserialize(deserializer)?;
let cached = Self::extract_text(&raw);
Ok(Self { raw, cached })
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AdfText {
#[serde(rename = "type")]
pub node_type: String, pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub marks: Option<Vec<AdfMark>>,
}
impl AdfText {
pub fn new(text: impl Into<String>) -> Self {
Self {
node_type: "text".to_string(),
text: text.into(),
marks: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AdfMark {
#[serde(rename = "type")]
pub mark_type: String, }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AdfNode {
#[serde(rename = "type")]
pub node_type: String, #[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<AdfContent>>,
}
impl AdfNode {
pub fn paragraph(content: Vec<AdfText>) -> Self {
Self {
node_type: "paragraph".to_string(),
content: Some(content.into_iter().map(AdfContent::Text).collect()),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum AdfContent {
Text(AdfText),
Node(Box<AdfNode>),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AdfDocument {
pub version: u32,
#[serde(rename = "type")]
pub doc_type: String, pub content: Vec<AdfNode>,
}
impl AdfDocument {
pub fn from_text(text: impl Into<String>) -> Self {
let text = text.into();
let content = if text.is_empty() {
vec![AdfNode::paragraph(vec![])]
} else {
text.lines()
.map(|line| {
if line.is_empty() {
AdfNode::paragraph(vec![])
} else {
AdfNode::paragraph(vec![AdfText::new(line)])
}
})
.collect()
};
Self {
version: 1,
doc_type: "doc".to_string(),
content,
}
}
pub fn to_plain_text(&self) -> String {
self.content
.iter()
.filter_map(Self::extract_text_from_node)
.collect::<Vec<String>>()
.join("\n")
}
fn extract_text_from_node(node: &AdfNode) -> Option<String> {
if let Some(content) = &node.content {
let text: Vec<String> = content
.iter()
.filter_map(|item| match item {
AdfContent::Text(text_node) => Some(text_node.text.clone()),
AdfContent::Node(nested_node) => Self::extract_text_from_node(nested_node),
})
.collect();
if text.is_empty() {
None
} else {
Some(text.join(""))
}
} else {
None
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Changelog {
#[serde(rename = "values")]
pub histories: Vec<History>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct History {
pub author: User,
pub created: String,
pub items: Vec<HistoryItem>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct HistoryItem {
pub field: String,
pub from: Option<String>,
#[serde(rename = "fromString")]
pub from_string: Option<String>,
pub to: Option<String>,
#[serde(rename = "toString")]
pub to_string: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Project {
#[serde(rename = "self")]
pub self_link: String,
pub id: String,
pub key: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub project_type_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub lead: Option<User>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Vec<ProjectComponent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub versions: Option<Vec<Version>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_urls: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_category: Option<ProjectCategory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issue_types: Option<Vec<IssueType>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectComponent {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "self", skip_serializing_if = "Option::is_none")]
pub self_link: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateProject {
pub key: String,
pub name: String,
pub project_type_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lead: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assignee_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issue_security_scheme: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_scheme: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notification_scheme: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category_id: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UpdateProject {
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lead: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assignee_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category_id: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectCategory {
#[serde(rename = "self")]
pub self_link: String,
pub id: String,
pub name: String,
pub description: String,
}
#[derive(Serialize, Debug, Default)]
pub struct ProjectSearchOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_results: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_type_key: Option<String>,
}
impl ProjectSearchOptions {
pub fn serialize(&self) -> Result<Vec<(String, String)>> {
let mut params = Vec::new();
if let Some(ref query) = self.query {
params.push(("query".to_string(), query.clone()));
}
if let Some(start_at) = self.start_at {
params.push(("startAt".to_string(), start_at.to_string()));
}
if let Some(max_results) = self.max_results {
params.push(("maxResults".to_string(), max_results.to_string()));
}
if let Some(ref order_by) = self.order_by {
params.push(("orderBy".to_string(), order_by.clone()));
}
if let Some(category_id) = self.category_id {
params.push(("categoryId".to_string(), category_id.to_string()));
}
if let Some(ref project_type_key) = self.project_type_key {
params.push(("projectTypeKey".to_string(), project_type_key.clone()));
}
Ok(params)
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ProjectSearchResults {
pub start_at: u64,
pub max_results: u64,
pub total: u64,
pub values: Vec<Project>,
}
#[derive(Deserialize, Debug)]
pub struct ProjectRole {
#[serde(rename = "self")]
pub self_link: String,
pub name: String,
pub id: u64,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub actors: Option<Vec<RoleActor>>,
}
#[derive(Deserialize, Debug)]
pub struct RoleActor {
pub id: u64,
#[serde(rename = "displayName")]
pub display_name: String,
#[serde(rename = "type")]
pub actor_type: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct IssueLink {
pub id: String,
#[serde(rename = "self")]
pub self_link: String,
#[serde(rename = "outwardIssue")]
pub outward_issue: Option<Issue>,
#[serde(rename = "inwardIssue")]
pub inward_issue: Option<Issue>,
#[serde(rename = "type")]
pub link_type: LinkType,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LinkType {
pub id: String,
pub inward: String,
pub name: String,
pub outward: String,
#[serde(rename = "self")]
pub self_link: String,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateIssueLinkInput {
#[serde(rename = "type")]
pub link_type: IssueLinkType,
pub inward_issue: IssueKey,
pub outward_issue: IssueKey,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<LinkComment>,
}
#[derive(Serialize, Debug)]
pub struct IssueLinkType {
pub name: String,
}
#[derive(Serialize, Debug)]
pub struct IssueKey {
pub key: String,
}
#[derive(Debug)]
pub struct LinkComment {
body: String,
}
impl LinkComment {
pub fn new(body: impl Into<String>) -> Self {
Self { body: body.into() }
}
}
impl Serialize for LinkComment {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let adf = AdfDocument::from_text(&self.body);
let mut state = serializer.serialize_struct("LinkComment", 1)?;
state.serialize_field("body", &adf)?;
state.end()
}
}
impl CreateIssueLinkInput {
pub fn new(
link_type: impl Into<String>,
inward: impl Into<String>,
outward: impl Into<String>,
) -> Self {
Self {
link_type: IssueLinkType {
name: link_type.into(),
},
inward_issue: IssueKey { key: inward.into() },
outward_issue: IssueKey {
key: outward.into(),
},
comment: None,
}
}
pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
self.comment = Some(LinkComment::new(comment));
self
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Version {
pub archived: bool,
pub id: String,
pub name: String,
#[serde(rename = "projectId")]
pub project_id: u64,
pub released: bool,
#[serde(rename = "self")]
pub self_link: String,
}
#[derive(Serialize, Debug)]
pub struct VersionCreationBody {
pub name: String,
#[serde(rename = "projectId")]
pub project_id: u64,
}
#[derive(Serialize, Debug)]
pub struct VersionMoveAfterBody {
pub after: String,
}
#[derive(Serialize, Debug)]
pub struct VersionUpdateBody {
pub released: bool,
pub archived: bool,
#[serde(rename = "moveUnfixedIssuesTo")]
pub move_unfixed_issues_to: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct User {
#[serde(rename = "accountId")]
pub account_id: Option<String>,
pub active: bool,
#[serde(rename = "avatarUrls")]
pub avatar_urls: Option<BTreeMap<String, String>>,
#[serde(rename = "displayName")]
pub display_name: String,
#[serde(rename = "emailAddress")]
pub email_address: Option<String>,
pub key: Option<String>,
pub name: Option<String>,
#[serde(rename = "self")]
pub self_link: String,
#[serde(rename = "timeZone")]
pub timezone: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Status {
pub description: String,
#[serde(rename = "iconUrl")]
pub icon_url: String,
pub id: String,
pub name: String,
#[serde(rename = "self")]
pub self_link: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Priority {
#[serde(rename = "iconUrl")]
pub icon_url: String,
pub id: String,
pub name: String,
#[serde(rename = "self")]
pub self_link: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct IssueType {
pub description: String,
#[serde(rename = "iconUrl")]
pub icon_url: String,
pub id: String,
pub name: String,
#[serde(rename = "self")]
pub self_link: String,
pub subtask: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SearchResults {
pub total: u64,
#[serde(rename = "maxResults")]
pub max_results: u64,
#[serde(rename = "startAt")]
pub start_at: u64,
pub expand: Option<String>,
pub issues: Vec<Issue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_last_page: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_page_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_is_accurate: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct V3SearchResults {
pub issues: Vec<Issue>,
#[serde(rename = "isLast")]
pub is_last: bool,
#[serde(rename = "nextPageToken")]
pub next_page_token: Option<String>,
}
impl V3SearchResults {
pub fn to_search_results(self, start_at: u64, max_results: u64) -> SearchResults {
let total = if self.is_last {
start_at + self.issues.len() as u64
} else {
start_at + self.issues.len() as u64 + max_results
};
SearchResults {
total,
max_results,
start_at,
expand: None,
issues: self.issues,
is_last_page: Some(self.is_last),
next_page_token: self.next_page_token,
total_is_accurate: Some(false), }
}
}
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct V3ApproximateCountResponse {
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IssueCount {
pub count: u64,
pub is_exact: bool,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TimeTracking {
pub original_estimate: Option<String>,
pub original_estimate_seconds: Option<u64>,
pub remaining_estimate: Option<String>,
pub remaining_estimate_seconds: Option<u64>,
pub time_spent: Option<String>,
pub time_spent_seconds: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TransitionOption {
pub id: String,
pub name: String,
pub to: TransitionTo,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TransitionTo {
pub name: String,
pub id: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TransitionOptions {
pub transitions: Vec<TransitionOption>,
}
#[derive(Serialize, Debug)]
pub struct TransitionTriggerOptions {
pub transition: Transition,
pub fields: BTreeMap<String, ::serde_json::Value>,
}
impl TransitionTriggerOptions {
pub fn new<I>(id: I) -> TransitionTriggerOptions
where
I: Into<String>,
{
TransitionTriggerOptions {
transition: Transition { id: id.into() },
fields: BTreeMap::new(),
}
}
pub fn builder<I>(id: I) -> TransitionTriggerOptionsBuilder
where
I: Into<String>,
{
TransitionTriggerOptionsBuilder::new(id)
}
}
pub struct TransitionTriggerOptionsBuilder {
pub transition: Transition,
pub fields: BTreeMap<String, ::serde_json::Value>,
}
impl TransitionTriggerOptionsBuilder {
pub fn new<I>(id: I) -> TransitionTriggerOptionsBuilder
where
I: Into<String>,
{
TransitionTriggerOptionsBuilder {
transition: Transition { id: id.into() },
fields: BTreeMap::new(),
}
}
pub fn field<N, V>(&mut self, name: N, value: V) -> &mut TransitionTriggerOptionsBuilder
where
N: Into<String>,
V: Serialize,
{
self.fields.insert(
name.into(),
serde_json::to_value(value).expect("Value to serialize"),
);
self
}
pub fn resolution<R>(&mut self, name: R) -> &mut TransitionTriggerOptionsBuilder
where
R: Into<String>,
{
self.field("resolution", Resolution { name: name.into() });
self
}
pub fn comment<C>(&mut self, comment: C) -> &mut TransitionTriggerOptionsBuilder
where
C: Into<String>,
{
let adf = AdfDocument::from_text(comment.into());
self.field("comment", serde_json::json!({"body": adf}));
self
}
pub fn build(&self) -> TransitionTriggerOptions {
TransitionTriggerOptions {
transition: self.transition.clone(),
fields: self.fields.clone(),
}
}
}
#[derive(Serialize, Debug, Deserialize)]
pub struct Resolution {
name: String,
}
#[derive(Serialize, Clone, Debug)]
pub struct Transition {
pub id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Worklog {
#[serde(rename = "self")]
pub self_link: String,
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<User>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_author: Option<User>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<TextContent>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "time::serde::iso8601::option"
)]
pub created: Option<OffsetDateTime>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "time::serde::iso8601::option"
)]
pub updated: Option<OffsetDateTime>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "time::serde::iso8601::option"
)]
pub started: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_spent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_spent_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issue_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorklogInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", with = "jira_datetime")]
pub started: Option<OffsetDateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_spent_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_spent: Option<String>,
}
impl WorklogInput {
pub(crate) fn to_v3_json(&self) -> serde_json::Value {
let mut obj = serde_json::Map::new();
let comment_text = self.comment.as_deref().unwrap_or("");
let adf_comment = AdfDocument::from_text(comment_text);
obj.insert(
"comment".to_string(),
serde_json::to_value(adf_comment).unwrap(),
);
if let Some(started) = self.started {
let format = time::format_description::parse(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3][offset_hour sign:mandatory][offset_minute]"
).unwrap();
let formatted = started.format(&format).unwrap();
obj.insert("started".to_string(), serde_json::Value::String(formatted));
}
if let Some(time_spent_seconds) = self.time_spent_seconds {
obj.insert(
"timeSpentSeconds".to_string(),
serde_json::Value::Number(time_spent_seconds.into()),
);
}
if let Some(ref time_spent) = self.time_spent {
obj.insert(
"timeSpent".to_string(),
serde_json::Value::String(time_spent.clone()),
);
}
serde_json::Value::Object(obj)
}
}
impl WorklogInput {
pub fn new(time_spent_seconds: u64) -> Self {
Self {
comment: None,
started: None,
time_spent_seconds: Some(time_spent_seconds),
time_spent: None,
}
}
pub fn from_minutes(minutes: u64) -> Self {
Self::new(minutes * 60)
}
pub fn from_hours(hours: u64) -> Self {
Self::new(hours * 3600)
}
pub fn from_days(days: u64) -> Self {
Self::new(days * 8 * 3600)
}
pub fn from_weeks(weeks: u64) -> Self {
Self::new(weeks * 5 * 8 * 3600)
}
pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
self.comment = Some(comment.into());
self
}
pub fn comment(&self) -> Option<&str> {
self.comment.as_deref()
}
pub fn with_started(mut self, started: OffsetDateTime) -> Self {
self.started = Some(started);
self
}
pub fn started_hours_ago(mut self, hours: i64) -> Self {
let started = OffsetDateTime::now_utc() - time::Duration::hours(hours);
self.started = Some(started);
self
}
pub fn started_minutes_ago(mut self, minutes: i64) -> Self {
let started = OffsetDateTime::now_utc() - time::Duration::minutes(minutes);
self.started = Some(started);
self
}
pub fn started_days_ago(mut self, days: i64) -> Self {
let started = OffsetDateTime::now_utc() - time::Duration::days(days);
self.started = Some(started);
self
}
pub fn started_weeks_ago(mut self, weeks: i64) -> Self {
let started = OffsetDateTime::now_utc() - time::Duration::weeks(weeks);
self.started = Some(started);
self
}
pub fn started_at(mut self, datetime: OffsetDateTime) -> Self {
self.started = Some(datetime);
self
}
pub fn with_time_spent(mut self, time_spent: impl Into<String>) -> Self {
self.time_spent = Some(time_spent.into());
self
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WorklogList {
pub start_at: u64,
pub max_results: u64,
pub total: u64,
pub worklogs: Vec<Worklog>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Session {
pub name: String,
}