use std::fmt;
use indexmap::IndexMap;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use crate::objects::bugzilla::{Bug, Flag};
use crate::service::bugzilla::Bugzilla;
use crate::traits::{InjectAuth, Merge, MergeOption, RequestSend, RequestTemplate, WebService};
use crate::Error;
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct Request {
#[serde(skip)]
service: Bugzilla,
#[serde(flatten)]
pub params: Parameters,
}
impl RequestSend for Request {
type Output = u64;
async fn send(&self) -> crate::Result<Self::Output> {
let url = self.service.config().base.join("rest/bug")?;
let params = self.encode()?;
let request = self
.service
.client()
.post(url)
.json(¶ms)
.auth(&self.service)?;
let response = request.send().await?;
let mut data = self.service.parse_response(response).await?;
serde_json::from_value(data["id"].take())
.map_err(|e| Error::InvalidResponse(format!("failed deserializing id: {e}")))
}
}
impl RequestTemplate for Request {
type Params = Parameters;
type Service = Bugzilla;
const TYPE: &'static str = "create";
fn service(&self) -> &Self::Service {
&self.service
}
fn params(&mut self) -> &mut Self::Params {
&mut self.params
}
}
impl Request {
pub(super) fn new(service: &Bugzilla) -> Self {
Self {
service: service.clone(),
params: Default::default(),
}
}
fn encode(&self) -> crate::Result<RequestParameters> {
let params = RequestParameters {
op_sys: self.params.os.as_deref().unwrap_or("All"),
platform: self.params.platform.as_deref().unwrap_or("All"),
priority: self.params.priority.as_deref().unwrap_or("Normal"),
severity: self.params.severity.as_deref().unwrap_or("normal"),
version: self.params.version.as_deref().unwrap_or("unspecified"),
component: self.params.component.as_deref().unwrap_or_default(),
description: self.params.description.as_deref().unwrap_or_default(),
product: self.params.product.as_deref().unwrap_or_default(),
summary: self.params.summary.as_deref().unwrap_or_default(),
alias: self.params.alias.as_deref(),
assigned_to: self
.params
.assignee
.as_deref()
.map(|x| self.service.replace_user_alias(x)),
blocks: self.params.blocks.as_deref(),
cc: self.params.cc.as_deref(),
depends_on: self.params.depends.as_deref(),
flags: self.params.flags.as_deref(),
groups: self.params.groups.as_deref(),
keywords: self.params.keywords.as_deref(),
qa_contact: self
.params
.qa
.as_deref()
.map(|x| self.service.replace_user_alias(x)),
resolution: self.params.resolution.as_deref(),
see_also: self.params.see_also.as_deref(),
status: self.params.status.as_deref(),
target_milestone: self.params.target.as_deref(),
url: self.params.url.as_deref(),
whiteboard: self.params.whiteboard.as_deref(),
custom_fields: self.params.custom_fields.as_ref(),
};
let mut missing = vec![];
for (value, name) in [
(params.component, "component"),
(params.description, "description"),
(params.op_sys, "os"),
(params.platform, "platform"),
(params.priority, "priority"),
(params.product, "product"),
(params.severity, "severity"),
(params.summary, "summary"),
(params.version, "version"),
] {
if value.is_empty() {
missing.push(name);
}
}
if !missing.is_empty() {
let fields = missing.iter().sorted().join(", ");
return Err(Error::InvalidRequest(format!(
"missing required fields: {fields}"
)));
}
Ok(params)
}
pub fn alias<I, S>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: fmt::Display,
{
self.params.alias = Some(value.into_iter().map(|x| x.to_string()).collect());
self
}
pub fn assignee<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.assignee = Some(value.to_string());
self
}
pub fn blocks<I, S>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: fmt::Display,
{
self.params.blocks = Some(value.into_iter().map(|x| x.to_string()).collect());
self
}
pub fn cc<I, S>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: fmt::Display,
{
self.params.cc = Some(value.into_iter().map(|x| x.to_string()).collect());
self
}
pub fn component<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.component = Some(value.to_string());
self
}
pub fn depends<I, S>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: fmt::Display,
{
self.params.depends = Some(value.into_iter().map(|x| x.to_string()).collect());
self
}
pub fn description<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.description = Some(value.to_string());
self
}
pub fn flags<I>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = Flag>,
{
self.params.flags = Some(value.into_iter().collect());
self
}
pub fn groups<I, S>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: fmt::Display,
{
self.params.groups = Some(value.into_iter().map(|x| x.to_string()).collect());
self
}
pub fn keywords<I, S>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: fmt::Display,
{
self.params.keywords = Some(value.into_iter().map(|x| x.to_string()).collect());
self
}
pub fn os<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.os = Some(value.to_string());
self
}
pub fn platform<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.platform = Some(value.to_string());
self
}
pub fn priority<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.priority = Some(value.to_string());
self
}
pub fn product<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.product = Some(value.to_string());
self
}
pub fn qa<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.qa = Some(value.to_string());
self
}
pub fn resolution<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.resolution = Some(value.to_string());
self
}
pub fn see_also<I, S>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: fmt::Display,
{
self.params.see_also = Some(value.into_iter().map(|x| x.to_string()).collect());
self
}
pub fn severity<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.severity = Some(value.to_string());
self
}
pub fn status<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.status = Some(value.to_string());
self
}
pub fn summary<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.summary = Some(value.to_string());
self
}
pub fn target<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.target = Some(value.to_string());
self
}
pub fn url<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.url = Some(value.to_string());
self
}
pub fn version<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.version = Some(value.to_string());
self
}
pub fn whiteboard<S>(&mut self, value: S) -> &mut Self
where
S: fmt::Display,
{
self.params.whiteboard = Some(value.to_string());
self
}
pub fn custom_fields<I, S1, S2>(&mut self, value: I) -> &mut Self
where
I: IntoIterator<Item = (S1, S2)>,
S1: fmt::Display,
S2: fmt::Display,
{
self.params.custom_fields = Some(
value
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
);
self
}
}
#[skip_serializing_none]
#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
pub struct Parameters {
pub alias: Option<Vec<String>>,
pub assignee: Option<String>,
pub blocks: Option<Vec<String>>,
pub cc: Option<Vec<String>>,
pub component: Option<String>,
pub depends: Option<Vec<String>>,
pub description: Option<String>,
pub flags: Option<Vec<Flag>>,
pub groups: Option<Vec<String>>,
pub keywords: Option<Vec<String>>,
pub os: Option<String>,
pub platform: Option<String>,
pub priority: Option<String>,
pub product: Option<String>,
pub qa: Option<String>,
pub resolution: Option<String>,
pub see_also: Option<Vec<String>>,
pub severity: Option<String>,
pub status: Option<String>,
pub summary: Option<String>,
pub target: Option<String>,
pub url: Option<String>,
pub version: Option<String>,
pub whiteboard: Option<String>,
#[serde(flatten)]
pub custom_fields: Option<IndexMap<String, String>>,
}
impl Merge for Parameters {
fn merge(&mut self, other: Self) {
*self = Self {
alias: self.alias.merge(other.alias),
assignee: self.assignee.merge(other.assignee),
blocks: self.blocks.merge(other.blocks),
cc: self.cc.merge(other.cc),
component: self.component.merge(other.component),
custom_fields: self.custom_fields.merge(other.custom_fields),
depends: self.depends.merge(other.depends),
description: self.description.merge(other.description),
flags: self.flags.merge(other.flags),
groups: self.groups.merge(other.groups),
keywords: self.keywords.merge(other.keywords),
os: self.os.merge(other.os),
platform: self.platform.merge(other.platform),
priority: self.priority.merge(other.priority),
product: self.product.merge(other.product),
qa: self.qa.merge(other.qa),
resolution: self.resolution.merge(other.resolution),
see_also: self.see_also.merge(other.see_also),
status: self.status.merge(other.status),
severity: self.severity.merge(other.severity),
target: self.target.merge(other.target),
summary: self.summary.merge(other.summary),
url: self.url.merge(other.url),
version: self.version.merge(other.version),
whiteboard: self.whiteboard.merge(other.whiteboard),
}
}
}
impl From<Bug> for Parameters {
fn from(value: Bug) -> Self {
Self {
component: value.component,
os: value.op_sys,
platform: value.platform,
priority: value.priority,
product: value.product,
severity: value.severity,
version: value.version,
..Default::default()
}
}
}
#[skip_serializing_none]
#[derive(Serialize)]
struct RequestParameters<'a> {
component: &'a str,
description: &'a str,
op_sys: &'a str,
platform: &'a str,
priority: &'a str,
product: &'a str,
severity: &'a str,
summary: &'a str,
version: &'a str,
alias: Option<&'a [String]>,
assigned_to: Option<&'a str>,
blocks: Option<&'a [String]>,
cc: Option<&'a [String]>,
depends_on: Option<&'a [String]>,
flags: Option<&'a [Flag]>,
groups: Option<&'a [String]>,
keywords: Option<&'a [String]>,
qa_contact: Option<&'a str>,
resolution: Option<&'a str>,
see_also: Option<&'a [String]>,
status: Option<&'a str>,
target_milestone: Option<&'a str>,
url: Option<&'a str>,
whiteboard: Option<&'a str>,
#[serde(flatten)]
custom_fields: Option<&'a IndexMap<String, String>>,
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::test::*;
use super::*;
#[tokio::test]
async fn request() {
let path = TESTDATA_PATH.join("bugzilla");
let server = TestServer::new().await;
let service = Bugzilla::builder(server.uri())
.unwrap()
.user("user")
.password("pass")
.build()
.unwrap();
let err = service.create().send().await.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
assert_err_re!(
err,
"missing required fields: component, description, product, summary"
);
let err = service
.create()
.os("")
.description("a")
.summary("b")
.send()
.await
.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
assert_err_re!(err, "missing required fields: component, os, product");
let request = || {
let mut req = service.create();
req.component("TestComponent");
req.product("TestProduct");
req.description("description");
req.summary("summary");
req
};
server.respond(200, path.join("create/creation.json")).await;
let id = request().send().await.unwrap();
assert_eq!(id, 123);
request().alias(["alias1", "alias2"]).send().await.unwrap();
request().assignee("user").send().await.unwrap();
request().blocks([1]).send().await.unwrap();
request().blocks(["alias1", "alias2"]).send().await.unwrap();
request().cc(["user@email.com"]).send().await.unwrap();
request()
.cc(["user1@email.com", "user2@email.com"])
.send()
.await
.unwrap();
request().component("component").send().await.unwrap();
request().depends([1]).send().await.unwrap();
request()
.depends(["alias1", "alias2"])
.send()
.await
.unwrap();
request().description("description").send().await.unwrap();
let flag1 = Flag::from_str("flag1+").unwrap();
let flag2 = Flag::from_str("flag2-").unwrap();
let flag3 = Flag::from_str("flag3?").unwrap();
request().flags([flag1, flag2, flag3]).send().await.unwrap();
request().groups(["group1", "group2"]).send().await.unwrap();
request().keywords(["kw1", "kw2"]).send().await.unwrap();
request().os("os").send().await.unwrap();
request().platform("platform").send().await.unwrap();
request().priority("normal").send().await.unwrap();
request().product("product").send().await.unwrap();
request().qa("user@email.com").send().await.unwrap();
request().resolution("fixed").send().await.unwrap();
request().see_also([36]).send().await.unwrap();
request().see_also(["36"]).send().await.unwrap();
request()
.see_also(["https://link/to/external/bug"])
.send()
.await
.unwrap();
request().severity("normal").send().await.unwrap();
request().status("closed").send().await.unwrap();
request().summary("summary").send().await.unwrap();
request().target("milestone").send().await.unwrap();
request().url("https://link/to/site").send().await.unwrap();
request().version("1.2.3").send().await.unwrap();
request().whiteboard("note").send().await.unwrap();
request()
.custom_fields([("name", "value")])
.send()
.await
.unwrap();
}
}