use derive_builder::Builder;
use reqwest::Method;
use serde::Serialize;
use std::borrow::Cow;
use crate::api::issues::RoleFilter;
use crate::api::projects::ProjectEssentials;
use crate::api::roles::RoleEssentials;
use crate::api::trackers::TrackerEssentials;
use crate::api::versions::VersionStatusFilter;
use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CustomizedType {
Issue,
TimeEntry,
Project,
Version,
User,
Group,
Activity,
IssuePriority,
DocumentCategory,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FieldFormat {
User,
Version,
String,
Text,
Link,
Int,
Float,
Date,
List,
Bool,
Enumeration,
Attachment,
Progressbar,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditTagStyle {
DropDown,
CheckBox,
Radio,
}
impl serde::Serialize for EditTagStyle {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match *self {
EditTagStyle::DropDown => serializer.serialize_str(""),
EditTagStyle::CheckBox => serializer.serialize_str("check_box"),
EditTagStyle::Radio => serializer.serialize_str("radio"),
}
}
}
impl<'de> serde::Deserialize<'de> for EditTagStyle {
fn deserialize<D>(deserializer: D) -> Result<EditTagStyle, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"" => Ok(EditTagStyle::DropDown),
"check_box" => Ok(EditTagStyle::CheckBox),
"radio" => Ok(EditTagStyle::Radio),
_ => Err(serde::de::Error::unknown_variant(
&s,
&["", "check_box", "radio"],
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct PossibleValue {
pub label: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CustomFieldDefinition {
pub id: u64,
pub name: String,
pub description: Option<String>,
pub editable: bool,
pub customized_type: CustomizedType,
pub field_format: FieldFormat,
pub regexp: Option<String>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub is_required: Option<bool>,
pub is_filter: Option<bool>,
pub searchable: bool,
pub multiple: bool,
pub default_value: Option<String>,
pub visible: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<RoleEssentials>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub possible_values: Option<Vec<PossibleValue>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trackers: Option<Vec<TrackerEssentials>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub projects: Option<Vec<ProjectEssentials>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_for_all: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub position: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url_pattern: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_formatting: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edit_tag_style: Option<EditTagStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_role: Option<RoleFilter>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_status: Option<VersionStatusFilter>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions_allowed: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub full_width_layout: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thousands_delimiter: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ratio_interval: Option<f32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomFieldEssentialsWithValue {
pub id: u64,
pub name: String,
pub multiple: Option<bool>,
pub value: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct CustomFieldName {
pub id: u64,
pub name: String,
}
impl serde::Serialize for CustomFieldEssentialsWithValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut len = 2;
if self.multiple.is_some() {
len += 1;
};
if self.value.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("CustomFieldEssentialsWithValue", len)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("name", &self.name)?;
if let Some(ref multiple) = self.multiple {
state.serialize_field("multiple", &multiple)?;
if let Some(ref value) = self.value {
state.serialize_field("value", &value)?;
} else {
let s: Option<Vec<String>> = None;
state.serialize_field("value", &s)?;
}
} else if let Some(ref value) = self.value {
match value.as_slice() {
[] => {
let s: Option<String> = None;
state.serialize_field("value", &s)?;
}
[s] => {
state.serialize_field("value", &s)?;
}
values => {
return Err(serde::ser::Error::custom(format!(
"CustomFieldEssentialsWithValue multiple was set to false but value contained more than one value: {values:?}"
)));
}
}
} else {
let s: Option<String> = None;
state.serialize_field("value", &s)?;
}
state.end()
}
}
impl<'de> serde::Deserialize<'de> for CustomFieldEssentialsWithValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Id,
Name,
Multiple,
Value,
}
struct CustomFieldVisitor;
impl<'de> serde::de::Visitor<'de> for CustomFieldVisitor {
type Value = CustomFieldEssentialsWithValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("struct CustomFieldEssentialsWithValue")
}
fn visit_map<V>(self, mut map: V) -> Result<CustomFieldEssentialsWithValue, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut id = None;
let mut name = None;
let mut multiple = None;
let mut string_value: Option<String> = None;
let mut vec_string_value: Option<Vec<String>> = None;
while let Some(key) = map.next_key()? {
match key {
Field::Id => {
if id.is_some() {
return Err(serde::de::Error::duplicate_field("id"));
}
id = Some(map.next_value()?);
}
Field::Name => {
if name.is_some() {
return Err(serde::de::Error::duplicate_field("name"));
}
name = Some(map.next_value()?);
}
Field::Multiple => {
if multiple.is_some() {
return Err(serde::de::Error::duplicate_field("multiple"));
}
multiple = Some(map.next_value()?);
}
Field::Value => {
if string_value.is_some() {
return Err(serde::de::Error::duplicate_field("value"));
}
if vec_string_value.is_some() {
return Err(serde::de::Error::duplicate_field("value"));
}
if let Some(true) = multiple {
vec_string_value = Some(map.next_value()?);
} else {
string_value = map.next_value()?;
}
}
}
}
let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?;
let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
match (multiple, string_value, vec_string_value) {
(None, None, None) => Ok(CustomFieldEssentialsWithValue {
id,
name,
multiple: None,
value: None,
}),
(None, Some(s), None) => Ok(CustomFieldEssentialsWithValue {
id,
name,
multiple: None,
value: Some(vec![s]),
}),
(Some(true), None, Some(v)) => Ok(CustomFieldEssentialsWithValue {
id,
name,
multiple: Some(true),
value: Some(v),
}),
_ => Err(serde::de::Error::custom(
"invalid combination of multiple and value",
)),
}
}
}
const FIELDS: &[&str] = &["id", "name", "multiple", "value"];
deserializer.deserialize_struct(
"CustomFieldEssentialsWithValue",
FIELDS,
CustomFieldVisitor,
)
}
}
#[derive(Debug, Clone, Builder)]
#[builder(setter(strip_option))]
pub struct ListCustomFields {}
impl ReturnsJsonResponse for ListCustomFields {}
impl NoPagination for ListCustomFields {}
impl ListCustomFields {
#[must_use]
pub fn builder() -> ListCustomFieldsBuilder {
ListCustomFieldsBuilder::default()
}
}
impl Endpoint for ListCustomFields {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
"custom_fields.json".into()
}
}
#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct CustomField<'a> {
pub id: u64,
pub name: Option<Cow<'a, str>>,
pub value: Cow<'a, str>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CustomFieldsWrapper<T> {
pub custom_fields: Vec<T>,
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use std::error::Error;
use tracing_test::traced_test;
#[traced_test]
#[test]
fn test_list_custom_fields_no_pagination() -> Result<(), Box<dyn Error>> {
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env(
reqwest::blocking::Client::builder()
.tls_backend_rustls()
.build()?,
)?;
let endpoint = ListCustomFields::builder().build()?;
redmine.json_response_body::<_, CustomFieldsWrapper<CustomFieldDefinition>>(&endpoint)?;
Ok(())
}
#[traced_test]
#[test]
fn test_completeness_custom_fields_type() -> Result<(), Box<dyn Error>> {
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env(
reqwest::blocking::Client::builder()
.tls_backend_rustls()
.build()?,
)?;
let endpoint = ListCustomFields::builder().build()?;
let CustomFieldsWrapper {
custom_fields: values,
} = redmine.json_response_body::<_, CustomFieldsWrapper<serde_json::Value>>(&endpoint)?;
for value in values {
let o: CustomFieldDefinition = serde_json::from_value(value.clone())?;
let reserialized = serde_json::to_value(o)?;
assert_eq!(value, reserialized);
}
Ok(())
}
}