use serde::{Deserialize, Deserializer, Serialize};
use super::common::FlagUpdate;
#[derive(Clone, Copy)]
enum FilterField {
Product,
Component,
Status,
AssignedTo,
Creator,
Priority,
Severity,
Whiteboard,
TargetMilestone,
Version,
OpSys,
Platform,
Resolution,
QaContact,
Url,
}
impl FilterField {
fn from_struct_field(name: &str) -> Option<Self> {
match name {
"product" => Some(Self::Product),
"component" => Some(Self::Component),
"status" => Some(Self::Status),
"assigned_to" => Some(Self::AssignedTo),
"creator" => Some(Self::Creator),
"priority" => Some(Self::Priority),
"severity" => Some(Self::Severity),
"whiteboard" => Some(Self::Whiteboard),
"target_milestone" => Some(Self::TargetMilestone),
"version" => Some(Self::Version),
"op_sys" => Some(Self::OpSys),
"platform" => Some(Self::Platform),
"resolution" => Some(Self::Resolution),
"qa_contact" => Some(Self::QaContact),
"url" => Some(Self::Url),
_ => None,
}
}
}
fn deserialize_null_string<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
Option::<String>::deserialize(d).map(Option::unwrap_or_default)
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Bug {
pub id: u64,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub status: String,
#[serde(default)]
pub resolution: Option<String>,
#[serde(default)]
pub dupe_of: Option<u64>,
#[serde(default)]
pub deadline: Option<String>,
#[serde(default)]
pub product: Option<String>,
#[serde(default)]
pub component: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub assigned_to: Option<String>,
#[serde(default)]
pub priority: Option<String>,
#[serde(default)]
pub severity: Option<String>,
#[serde(default)]
pub creation_time: Option<String>,
#[serde(default)]
pub last_change_time: Option<String>,
#[serde(default)]
pub creator: Option<String>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub whiteboard: Option<String>,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default)]
pub blocks: Vec<u64>,
#[serde(default)]
pub depends_on: Vec<u64>,
#[serde(default)]
pub cc: Vec<String>,
#[serde(default)]
pub op_sys: Option<String>,
#[serde(default)]
pub rep_platform: Option<String>,
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct SearchParams {
pub product: Vec<String>,
pub component: Vec<String>,
pub status: Vec<String>,
pub assigned_to: Vec<String>,
pub creator: Vec<String>,
pub priority: Vec<String>,
pub severity: Vec<String>,
pub cc: Option<String>,
pub alias: Option<String>,
pub id: Vec<u64>,
pub limit: Option<u32>,
pub summary: Option<String>,
pub quicksearch: Option<String>,
pub include_fields: Option<String>,
pub exclude_fields: Option<String>,
pub raw_params: Vec<(String, String)>,
pub creation_time: Option<String>,
pub last_change_time: Option<String>,
pub whiteboard: Vec<String>,
pub target_milestone: Vec<String>,
pub version: Vec<String>,
pub op_sys: Vec<String>,
pub platform: Vec<String>,
pub resolution: Vec<String>,
pub qa_contact: Vec<String>,
pub url: Vec<String>,
}
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct Overrides<'a> {
pub limit: Option<u32>,
pub fields: Option<&'a str>,
pub exclude_fields: Option<&'a str>,
pub creation_time: Option<&'a str>,
pub last_change_time: Option<&'a str>,
pub whiteboard: Option<&'a [String]>,
pub target_milestone: Option<&'a [String]>,
pub version: Option<&'a [String]>,
pub op_sys: Option<&'a [String]>,
pub platform: Option<&'a [String]>,
pub resolution: Option<&'a [String]>,
pub qa_contact: Option<&'a [String]>,
pub url: Option<&'a [String]>,
}
macro_rules! filter_field_arm {
($self:ident, $field:ident, $assignee:ident $(, $mutability:tt)?) => {
match $field {
FilterField::Product => & $($mutability)? $self.product,
FilterField::Component => & $($mutability)? $self.component,
FilterField::Status => & $($mutability)? $self.status,
FilterField::AssignedTo => & $($mutability)? $self.$assignee,
FilterField::Creator => & $($mutability)? $self.creator,
FilterField::Priority => & $($mutability)? $self.priority,
FilterField::Severity => & $($mutability)? $self.severity,
FilterField::Whiteboard => & $($mutability)? $self.whiteboard,
FilterField::TargetMilestone => & $($mutability)? $self.target_milestone,
FilterField::Version => & $($mutability)? $self.version,
FilterField::OpSys => & $($mutability)? $self.op_sys,
FilterField::Platform => & $($mutability)? $self.platform,
FilterField::Resolution => & $($mutability)? $self.resolution,
FilterField::QaContact => & $($mutability)? $self.qa_contact,
FilterField::Url => & $($mutability)? $self.url,
}
};
}
impl SearchParams {
pub fn apply_overrides(&mut self, o: Overrides<'_>) {
if let Some(l) = o.limit {
self.limit = Some(l);
}
if let Some(f) = o.fields {
self.include_fields = Some(f.to_string());
}
if let Some(ef) = o.exclude_fields {
self.exclude_fields = Some(ef.to_string());
}
if let Some(ct) = o.creation_time {
self.creation_time = Some(ct.to_string());
}
if let Some(lct) = o.last_change_time {
self.last_change_time = Some(lct.to_string());
}
if let Some(v) = o.whiteboard {
self.whiteboard = v.to_vec();
}
if let Some(v) = o.target_milestone {
self.target_milestone = v.to_vec();
}
if let Some(v) = o.version {
self.version = v.to_vec();
}
if let Some(v) = o.op_sys {
self.op_sys = v.to_vec();
}
if let Some(v) = o.platform {
self.platform = v.to_vec();
}
if let Some(v) = o.resolution {
self.resolution = v.to_vec();
}
if let Some(v) = o.qa_contact {
self.qa_contact = v.to_vec();
}
if let Some(v) = o.url {
self.url = v.to_vec();
}
}
pub(crate) fn get_field(&self, name: &str) -> Option<&[String]> {
FilterField::from_struct_field(name).map(|field| self.get_filter_field(field))
}
#[cfg(test)]
pub(crate) fn get_field_mut(&mut self, name: &str) -> Option<&mut Vec<String>> {
FilterField::from_struct_field(name).map(|field| self.get_filter_field_mut(field))
}
fn get_filter_field(&self, field: FilterField) -> &[String] {
filter_field_arm!(self, field, assigned_to)
}
#[cfg(test)]
fn get_filter_field_mut(&mut self, field: FilterField) -> &mut Vec<String> {
filter_field_arm!(self, field, assigned_to, mut)
}
fn has_mapped_filters(&self) -> bool {
FIELD_MAPPINGS.iter().any(|mapping| {
self.get_field(mapping.struct_field)
.is_some_and(|field| !field.is_empty())
})
}
pub fn has_filters(&self) -> bool {
self.has_mapped_filters()
|| self.cc.is_some()
|| self.alias.is_some()
|| !self.id.is_empty()
|| self.summary.is_some()
|| self.quicksearch.is_some()
|| !self.raw_params.is_empty()
|| self.creation_time.is_some()
|| self.last_change_time.is_some()
}
pub fn has_structured_filters(&self) -> bool {
self.has_mapped_filters()
|| self.cc.is_some()
|| self.alias.is_some()
|| !self.id.is_empty()
|| !self.raw_params.is_empty()
|| self.creation_time.is_some()
|| self.last_change_time.is_some()
}
}
pub fn partition_filters(values: &[String]) -> (Vec<&str>, Vec<&str>) {
let mut positive = Vec::new();
let mut negated = Vec::new();
for v in values {
if let Some(stripped) = v.strip_prefix('!') {
negated.push(stripped);
} else {
positive.push(v.as_str());
}
}
(positive, negated)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NegationOp {
NotEquals,
NotSubstring,
}
impl NegationOp {
pub fn as_str(self) -> &'static str {
match self {
Self::NotEquals => "notequals",
Self::NotSubstring => "notsubstring",
}
}
}
#[non_exhaustive]
pub struct FieldMapping {
pub struct_field: &'static str,
pub url_param: &'static str,
pub internal_name: &'static str,
pub negation_operator: NegationOp,
}
pub const FIELD_MAPPINGS: &[FieldMapping] = &[
FieldMapping {
struct_field: "product",
url_param: "product",
internal_name: "product",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "component",
url_param: "component",
internal_name: "component",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "status",
url_param: "bug_status",
internal_name: "bug_status",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "assigned_to",
url_param: "assigned_to",
internal_name: "assigned_to",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "creator",
url_param: "reporter",
internal_name: "reporter",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "priority",
url_param: "priority",
internal_name: "priority",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "severity",
url_param: "bug_severity",
internal_name: "bug_severity",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "whiteboard",
url_param: "status_whiteboard",
internal_name: "status_whiteboard",
negation_operator: NegationOp::NotSubstring,
},
FieldMapping {
struct_field: "target_milestone",
url_param: "target_milestone",
internal_name: "target_milestone",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "version",
url_param: "version",
internal_name: "version",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "op_sys",
url_param: "op_sys",
internal_name: "op_sys",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "platform",
url_param: "rep_platform",
internal_name: "rep_platform",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "resolution",
url_param: "resolution",
internal_name: "resolution",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "qa_contact",
url_param: "qa_contact",
internal_name: "qa_contact",
negation_operator: NegationOp::NotEquals,
},
FieldMapping {
struct_field: "url",
url_param: "bug_file_loc",
internal_name: "bug_file_loc",
negation_operator: NegationOp::NotSubstring,
},
];
#[derive(Debug, Serialize)]
#[non_exhaustive]
pub struct CreateBugParams {
pub product: String,
pub component: String,
pub summary: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub severity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assigned_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub op_sys: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rep_platform: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub blocks: Vec<u64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub depends_on: Vec<u64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cc: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<String>,
}
#[derive(Debug, Default, Serialize)]
#[non_exhaustive]
pub struct IdListUpdate {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub add: Vec<u64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub remove: Vec<u64>,
}
impl IdListUpdate {
pub fn is_empty(&self) -> bool {
self.add.is_empty() && self.remove.is_empty()
}
}
#[derive(Debug, Default, Serialize)]
#[non_exhaustive]
pub struct StringListUpdate {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub add: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub remove: Vec<String>,
}
impl StringListUpdate {
pub fn is_empty(&self) -> bool {
self.add.is_empty() && self.remove.is_empty()
}
}
#[derive(Debug, Default, Serialize)]
#[non_exhaustive]
pub struct CommentUpdate {
pub body: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub is_private: bool,
}
#[derive(Debug, Default, Serialize)]
#[non_exhaustive]
pub struct UpdateBugParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dupe_of: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub estimated_time: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remaining_time: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub work_time: Option<f64>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub reset_assigned_to: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub reset_qa_contact: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub assigned_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub severity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub whiteboard: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub flags: Vec<FlagUpdate>,
#[serde(skip_serializing_if = "IdListUpdate::is_empty")]
pub blocks: IdListUpdate,
#[serde(skip_serializing_if = "IdListUpdate::is_empty")]
pub depends_on: IdListUpdate,
#[serde(skip_serializing_if = "StringListUpdate::is_empty")]
pub keywords: StringListUpdate,
#[serde(skip_serializing_if = "StringListUpdate::is_empty")]
pub cc: StringListUpdate,
#[serde(skip_serializing_if = "StringListUpdate::is_empty")]
pub groups: StringListUpdate,
#[serde(skip_serializing_if = "StringListUpdate::is_empty")]
pub see_also: StringListUpdate,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<CommentUpdate>,
#[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
pub comment_is_private: std::collections::HashMap<u64, bool>,
}
impl UpdateBugParams {
#[must_use]
pub fn is_empty(&self) -> bool {
serde_json::to_value(self)
.ok()
.and_then(|v| v.as_object().map(serde_json::Map::is_empty))
.unwrap_or(false)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HistoryEntry {
pub who: String,
pub when: String,
pub changes: Vec<FieldChange>,
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FieldChange {
pub field_name: String,
#[serde(default)]
pub removed: String,
#[serde(default)]
pub added: String,
#[serde(default)]
pub attachment_id: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FieldValue {
#[serde(default, deserialize_with = "deserialize_null_string")]
pub name: String,
#[serde(default)]
pub sort_key: u64,
#[serde(default)]
pub is_active: bool,
#[serde(default)]
pub can_change_to: Option<Vec<StatusTransition>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StatusTransition {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum QueryKind {
#[default]
List,
Search,
Url,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub struct SavedQuery {
#[serde(default)]
pub kind: QueryKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub product: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub component: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub status: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assignee: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub creator: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub priority: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub severity: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quicksearch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exclude_fields: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub raw_params: Vec<(String, String)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creation_time: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_change_time: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub whiteboard: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_milestone: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub version: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub op_sys: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub platform: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resolution: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub qa_contact: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub url: Vec<String>,
}
impl SavedQuery {
pub fn to_search_params(&self) -> SearchParams {
self.clone().into_search_params()
}
pub fn into_search_params(self) -> SearchParams {
SearchParams {
product: self.product,
component: self.component,
status: self.status,
assigned_to: self.assignee,
creator: self.creator,
priority: self.priority,
severity: self.severity,
quicksearch: self.quicksearch,
limit: self.limit,
include_fields: self.fields,
exclude_fields: self.exclude_fields,
raw_params: self.raw_params,
creation_time: self.creation_time,
last_change_time: self.last_change_time,
whiteboard: self.whiteboard,
target_milestone: self.target_milestone,
version: self.version,
op_sys: self.op_sys,
platform: self.platform,
resolution: self.resolution,
qa_contact: self.qa_contact,
url: self.url,
..Default::default()
}
}
pub fn get_field_mut(&mut self, name: &str) -> Option<&mut Vec<String>> {
FilterField::from_struct_field(name).map(|field| self.get_filter_field_mut(field))
}
fn get_field(&self, name: &str) -> Option<&[String]> {
FilterField::from_struct_field(name).map(|field| self.get_filter_field(field))
}
fn get_filter_field(&self, field: FilterField) -> &[String] {
filter_field_arm!(self, field, assignee)
}
fn get_filter_field_mut(&mut self, field: FilterField) -> &mut Vec<String> {
filter_field_arm!(self, field, assignee, mut)
}
fn has_mapped_filters(&self) -> bool {
FIELD_MAPPINGS.iter().any(|mapping| {
self.get_field(mapping.struct_field)
.is_some_and(|field| !field.is_empty())
})
}
pub fn has_filters(&self) -> bool {
self.has_mapped_filters()
|| self.quicksearch.is_some()
|| !self.raw_params.is_empty()
|| self.creation_time.is_some()
|| self.last_change_time.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BugTemplate {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub product: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub component: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assignee: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub op_sys: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rep_platform: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[cfg(test)]
#[path = "bug_tests.rs"]
mod tests;