use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use byte_unit::Byte;
use chrono::prelude::*;
use indexmap::{IndexMap, IndexSet};
use itertools::{Either, Itertools};
use serde::{Deserialize, Deserializer, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_with::{
BoolFromInt, DefaultOnError, DefaultOnNull, DeserializeFromStr, SerializeDisplay, serde_as,
skip_serializing_none,
};
use strum::{Display, EnumString};
use crate::Error;
use crate::serde::{byte_object, non_empty_str};
use crate::service::bugzilla::{BugField, FilterField, GroupField, UNSET_VALUES};
use crate::traits::RenderSearch;
use super::{Base64, stringify};
#[serde_as]
#[derive(Deserialize, Debug, PartialEq, Eq, Hash)]
pub struct Attachment {
pub id: u64,
pub bug_id: u64,
pub file_name: String,
pub summary: String,
#[serde(deserialize_with = "byte_object")]
pub size: Byte,
pub creator: String,
pub content_type: String,
#[serde_as(as = "BoolFromInt")]
pub is_private: bool,
#[serde_as(as = "BoolFromInt")]
pub is_obsolete: bool,
#[serde_as(as = "BoolFromInt")]
pub is_patch: bool,
#[serde(rename = "creation_time")]
pub created: DateTime<Utc>,
#[serde(rename = "last_change_time")]
pub updated: DateTime<Utc>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub flags: Vec<BugFlag>,
#[serde(default)]
data: Base64,
}
impl AsRef<[u8]> for Attachment {
fn as_ref(&self) -> &[u8] {
self.data.as_ref()
}
}
impl Attachment {
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn is_deleted(&self) -> bool {
self.size == 0_u64
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum BugUpdate<'a> {
Comment(&'a Comment),
Event(&'a Event),
}
impl BugUpdate<'_> {
fn date(&self) -> &DateTime<Utc> {
match self {
Self::Comment(comment) => &comment.created,
Self::Event(event) => &event.when,
}
}
}
impl Ord for BugUpdate<'_> {
fn cmp(&self, other: &Self) -> Ordering {
self.date().cmp(other.date())
}
}
impl PartialOrd for BugUpdate<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct Comment {
pub id: u64,
pub bug_id: u64,
pub attachment_id: Option<u64>,
pub count: usize,
pub text: String,
pub creator: String,
#[serde(rename = "creation_time")]
pub created: DateTime<Utc>,
pub is_private: bool,
#[serde(default)]
pub tags: IndexSet<String>,
}
impl Comment {
pub fn reply(&self) -> String {
let creator = self
.creator
.split_once('@')
.map(|x| x.0)
.unwrap_or(&self.creator);
let mut data = vec![format!(
"(In reply to {} from comment #{})",
creator, self.count
)];
for line in self.text.lines() {
data.push(format!("> {line}"));
}
data.iter().join("\n")
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Clone)]
pub struct Event {
pub who: String,
pub when: DateTime<Utc>,
pub changes: Vec<Change>,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Clone)]
pub struct Change {
pub field_name: String,
#[serde(deserialize_with = "non_empty_str")]
pub removed: Option<String>,
#[serde(deserialize_with = "non_empty_str")]
pub added: Option<String>,
pub attachment_id: Option<u64>,
}
#[derive(
Display,
EnumString,
DeserializeFromStr,
SerializeDisplay,
Debug,
PartialEq,
Eq,
Hash,
Clone,
Copy,
)]
pub enum FlagStatus {
#[strum(serialize = "+")]
Granted,
#[strum(serialize = "-")]
Denied,
#[strum(serialize = "?")]
Requested,
#[strum(serialize = "X")]
Remove,
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Clone)]
pub struct Flag {
pub name: String,
pub status: FlagStatus,
}
impl FromStr for Flag {
type Err = Error;
fn from_str(s: &str) -> crate::Result<Self> {
if s.is_empty() {
return Err(Error::InvalidValue("empty flag".to_string()));
}
let name = &s[..s.len() - 1];
let status = &s[s.len() - 1..];
let status = status
.parse()
.map_err(|_| Error::InvalidValue(format!("invalid flag status: {status}")))?;
Ok(Self {
name: name.to_string(),
status,
})
}
}
impl fmt::Display for Flag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}{}", self.name, self.status)
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Hash)]
pub struct BugFlag {
#[serde(flatten)]
pub flag: Flag,
pub setter: String,
#[serde(rename = "creation_date")]
pub created: DateTime<Utc>,
#[serde(rename = "modification_date")]
pub updated: DateTime<Utc>,
}
impl fmt::Display for BugFlag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.flag)
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
enum Alias {
List(IndexSet<String>),
String(String),
}
pub(crate) fn unset_value_str<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
non_empty_str(d).map(|o| o.filter(|s| !UNSET_VALUES.contains(s)))
}
pub(crate) fn alias_to_set<'de, D: Deserializer<'de>>(d: D) -> Result<IndexSet<String>, D::Error> {
Option::<Alias>::deserialize(d).map(|o| match o {
Some(Alias::List(values)) => values,
Some(Alias::String(value)) => [value].into_iter().collect(),
None => Default::default(),
})
}
#[serde_as]
#[skip_serializing_none]
#[derive(Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
#[serde(default)]
pub struct Bug {
pub id: u64,
#[serde(deserialize_with = "alias_to_set")]
#[serde(skip_serializing_if = "IndexSet::is_empty")]
pub alias: IndexSet<String>,
#[serde(deserialize_with = "non_empty_str")]
pub assigned_to: Option<String>,
#[serde(deserialize_with = "non_empty_str")]
pub qa_contact: Option<String>,
#[serde(deserialize_with = "non_empty_str")]
pub creator: Option<String>,
#[serde(rename = "creation_time")]
pub created: Option<DateTime<Utc>>,
#[serde(rename = "last_change_time")]
pub updated: Option<DateTime<Utc>>,
pub deadline: Option<NaiveDate>,
#[serde(deserialize_with = "non_empty_str")]
pub summary: Option<String>,
#[serde(deserialize_with = "non_empty_str")]
pub status: Option<String>,
#[serde(deserialize_with = "non_empty_str")]
pub resolution: Option<String>,
#[serde(deserialize_with = "non_empty_str")]
pub whiteboard: Option<String>,
#[serde(deserialize_with = "non_empty_str")]
pub product: Option<String>,
#[serde(deserialize_with = "non_empty_str")]
pub component: Option<String>,
#[serde(deserialize_with = "unset_value_str")]
pub version: Option<String>,
#[serde(deserialize_with = "unset_value_str")]
pub platform: Option<String>,
#[serde(deserialize_with = "unset_value_str")]
pub op_sys: Option<String>,
#[serde(rename = "target_milestone", deserialize_with = "unset_value_str")]
pub target: Option<String>,
#[serde(deserialize_with = "unset_value_str")]
pub priority: Option<String>,
#[serde(deserialize_with = "unset_value_str")]
pub severity: Option<String>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "IndexSet::is_empty")]
pub groups: IndexSet<String>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "IndexSet::is_empty")]
pub keywords: IndexSet<String>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "IndexSet::is_empty")]
pub cc: IndexSet<String>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "IndexSet::is_empty")]
pub blocks: IndexSet<u64>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "IndexSet::is_empty")]
pub depends_on: IndexSet<u64>,
#[serde(rename = "dupe_of")]
pub duplicate_of: Option<u64>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub flags: Vec<BugFlag>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "IndexSet::is_empty")]
pub tags: IndexSet<String>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(skip_serializing_if = "IndexSet::is_empty")]
pub see_also: IndexSet<String>,
#[serde(deserialize_with = "non_empty_str")]
pub url: Option<String>,
#[serde(skip)]
pub comments: Vec<Comment>,
#[serde(skip)]
pub attachments: Vec<Attachment>,
#[serde(skip)]
pub history: Vec<Event>,
#[serde(skip)]
pub custom_fields: IndexMap<BugzillaFieldName, String>,
}
impl Bug {
pub fn updates(&self) -> impl Iterator<Item = BugUpdate<'_>> {
let comments = self.comments.iter().map(BugUpdate::Comment);
let history = self.history.iter().map(BugUpdate::Event);
comments.chain(history).sorted()
}
}
impl RenderSearch<BugField> for Bug {
fn render(&self, fields: &[BugField]) -> String {
let field_to_string = |field: &BugField| -> String {
match field {
BugField::Alias => format!("{:<20}", self.alias.iter().join(",")),
BugField::Assignee => format!("{:<20}", stringify!(self.assigned_to)),
BugField::Blocks => format!("{:<20}", self.blocks.iter().join(",")),
BugField::Cc => format!("{:<20}", self.cc.iter().join(",")),
BugField::Component => format!("{:<20}", stringify!(self.component)),
BugField::Created => stringify!(self.created),
BugField::Creator => format!("{:<20}", stringify!(self.creator)),
BugField::Deadline => stringify!(self.deadline),
BugField::Depends => format!("{:<20}", self.depends_on.iter().join(",")),
BugField::DuplicateOf => format!("{:<9}", stringify!(self.duplicate_of)),
BugField::Flags => format!("{:<20}", self.flags.iter().join(",")),
BugField::Id => format!("{:<9}", self.id),
BugField::Keywords => format!("{:<20}", self.keywords.iter().join(",")),
BugField::Os => format!("{:<20}", stringify!(self.op_sys)),
BugField::Platform => format!("{:<20}", stringify!(self.platform)),
BugField::Priority => format!("{:<12}", stringify!(self.priority)),
BugField::Product => format!("{:<20}", stringify!(self.product)),
BugField::Qa => format!("{:<20}", stringify!(self.qa_contact)),
BugField::Resolution => format!("{:<20}", stringify!(self.resolution)),
BugField::SeeAlso => format!("{:<20}", self.see_also.iter().join(",")),
BugField::Severity => format!("{:<12}", stringify!(self.severity)),
BugField::Status => format!("{:<20}", stringify!(self.status)),
BugField::Summary => stringify!(self.summary),
BugField::Tags => format!("{:<20}", self.tags.iter().join(",")),
BugField::Target => format!("{:<20}", stringify!(self.target)),
BugField::Updated => stringify!(self.updated),
BugField::Url => format!("{:<20}", stringify!(self.url)),
BugField::Version => format!("{:<20}", stringify!(self.version)),
BugField::Whiteboard => format!("{:<20}", stringify!(self.whiteboard)),
}
};
match fields {
[] => panic!("no fields defined"),
[field] => field_to_string(field).trim().to_string(),
fields => fields.iter().map(field_to_string).join(" "),
}
}
}
impl RenderSearch<FilterField> for Bug {
fn render(&self, fields: &[FilterField]) -> String {
let (bug_fields, group_fields): (Vec<_>, Vec<_>) =
fields.iter().partition_map(|x| match x {
FilterField::Bug(x) => Either::Left(*x),
FilterField::Group(x) => Either::Right(*x),
});
match (&bug_fields[..], &group_fields[..]) {
(fields @ [_, ..], _) => self.render(fields),
([], x) if x.contains(&GroupField::All) || x.contains(&GroupField::Default) => {
self.render(&[BugField::Id, BugField::Summary])
}
_ => self.render(&[BugField::Id]),
}
}
}
#[derive(Serialize_repr, Deserialize_repr, Default, Debug)]
#[repr(u8)]
pub enum BugzillaFieldKind {
#[default]
Unknown,
String,
Value,
MultiValue,
Text,
DateTime,
BugId,
SeeAlso,
Keywords,
Date,
Integer,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct BugzillaFieldName {
#[serde(rename = "name")]
pub(crate) id: String,
#[serde(rename = "display_name")]
display: String,
}
impl PartialEq for BugzillaFieldName {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for BugzillaFieldName {}
impl Hash for BugzillaFieldName {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl Ord for BugzillaFieldName {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl PartialOrd for BugzillaFieldName {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for BugzillaFieldName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.display)
}
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug)]
pub struct BugzillaField {
id: u64,
#[serde_as(deserialize_as = "DefaultOnError")]
#[serde(default, rename = "type")]
pub kind: BugzillaFieldKind,
is_custom: bool,
#[serde(flatten)]
pub name: BugzillaFieldName,
is_mandatory: bool,
#[serde(default)]
values: Vec<BugzillaFieldValue>,
}
impl PartialEq for BugzillaField {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for BugzillaField {}
impl Hash for BugzillaField {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl fmt::Display for BugzillaField {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Name: {}", self.name.id)?;
write!(f, "Display name: {}", self.name.display)?;
if !self.values.is_empty() {
let values = self
.values
.iter()
.filter(|x| !x.name.is_empty())
.map(|x| x.name.as_str())
.collect::<IndexSet<_>>();
let values = values.iter().join(", ");
write!(f, "\nValues: {values}")?;
}
Ok(())
}
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug)]
pub struct BugzillaFieldValue {
#[serde_as(deserialize_as = "DefaultOnNull")]
name: String,
sort_key: Option<i64>,
description: Option<String>,
is_open: Option<bool>,
}
#[serde_as]
#[derive(Deserialize, Debug, PartialEq, Eq, Hash)]
pub struct User {
pub id: u64,
#[serde(deserialize_with = "non_empty_str")]
pub real_name: Option<String>,
pub name: String,
pub email: Option<String>,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(real_name) = &self.real_name {
write!(f, "{real_name} ({})", self.name)
} else {
write!(f, "{}", self.name)
}
}
}