1use derive_builder::Builder;
8use reqwest::Method;
9use serde::Serialize;
10use std::borrow::Cow;
11
12use crate::api::issues::RoleFilter;
13use crate::api::projects::ProjectEssentials;
14use crate::api::roles::RoleEssentials;
15use crate::api::trackers::TrackerEssentials;
16use crate::api::versions::VersionStatusFilter;
17use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
18
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum CustomizedType {
24 Issue,
26 TimeEntry,
28 Project,
30 Version,
32 User,
34 Group,
36 Activity,
38 IssuePriority,
40 DocumentCategory,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum FieldFormat {
48 User,
50 Version,
52 String,
54 Text,
56 Link,
58 Int,
60 Float,
62 Date,
64 List,
66 Bool,
68 Enumeration,
70 Attachment,
72 Progressbar,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum EditTagStyle {
79 DropDown,
81 CheckBox,
83 Radio,
85}
86
87impl serde::Serialize for EditTagStyle {
88 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89 where
90 S: serde::Serializer,
91 {
92 match *self {
93 EditTagStyle::DropDown => serializer.serialize_str(""),
94 EditTagStyle::CheckBox => serializer.serialize_str("check_box"),
95 EditTagStyle::Radio => serializer.serialize_str("radio"),
96 }
97 }
98}
99
100impl<'de> serde::Deserialize<'de> for EditTagStyle {
101 fn deserialize<D>(deserializer: D) -> Result<EditTagStyle, D::Error>
102 where
103 D: serde::Deserializer<'de>,
104 {
105 let s = String::deserialize(deserializer)?;
106 match s.as_str() {
107 "" => Ok(EditTagStyle::DropDown),
108 "check_box" => Ok(EditTagStyle::CheckBox),
109 "radio" => Ok(EditTagStyle::Radio),
110 _ => Err(serde::de::Error::unknown_variant(
111 &s,
112 &["", "check_box", "radio"],
113 )),
114 }
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub struct PossibleValue {
122 pub label: String,
124 pub value: String,
126}
127
128#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
132pub struct CustomFieldDefinition {
133 pub id: u64,
135 pub name: String,
137 pub description: Option<String>,
139 pub editable: bool,
141 pub customized_type: CustomizedType,
143 pub field_format: FieldFormat,
145 pub regexp: Option<String>,
147 pub min_length: Option<usize>,
149 pub max_length: Option<usize>,
151 pub is_required: Option<bool>,
153 pub is_filter: Option<bool>,
155 pub searchable: bool,
157 pub multiple: bool,
159 pub default_value: Option<String>,
161 pub visible: bool,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub roles: Option<Vec<RoleEssentials>>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub possible_values: Option<Vec<PossibleValue>>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub trackers: Option<Vec<TrackerEssentials>>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub projects: Option<Vec<ProjectEssentials>>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub is_for_all: Option<bool>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub position: Option<u64>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub url_pattern: Option<String>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub text_formatting: Option<String>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub edit_tag_style: Option<EditTagStyle>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub user_role: Option<RoleFilter>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub version_status: Option<VersionStatusFilter>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub extensions_allowed: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub full_width_layout: Option<bool>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub thousands_delimiter: Option<bool>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub ratio_interval: Option<f32>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct CustomFieldEssentialsWithValue {
214 pub id: u64,
216 pub name: String,
218 pub multiple: Option<bool>,
220 pub value: Option<Vec<String>>,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
226pub struct CustomFieldName {
227 pub id: u64,
229 pub name: String,
231}
232
233impl serde::Serialize for CustomFieldEssentialsWithValue {
234 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
235 where
236 S: serde::Serializer,
237 {
238 use serde::ser::SerializeStruct;
239 let mut len = 2;
240 if self.multiple.is_some() {
241 len += 1;
242 };
243 if self.value.is_some() {
244 len += 1;
245 }
246 let mut state = serializer.serialize_struct("CustomFieldEssentialsWithValue", len)?;
247 state.serialize_field("id", &self.id)?;
248 state.serialize_field("name", &self.name)?;
249 if let Some(ref multiple) = self.multiple {
250 state.serialize_field("multiple", &multiple)?;
251 if let Some(ref value) = self.value {
252 state.serialize_field("value", &value)?;
253 } else {
254 let s: Option<Vec<String>> = None;
255 state.serialize_field("value", &s)?;
256 }
257 } else if let Some(ref value) = self.value {
258 match value.as_slice() {
259 [] => {
260 let s: Option<String> = None;
261 state.serialize_field("value", &s)?;
262 }
263 [s] => {
264 state.serialize_field("value", &s)?;
265 }
266 values => {
267 return Err(serde::ser::Error::custom(format!(
268 "CustomFieldEssentialsWithValue multiple was set to false but value contained more than one value: {values:?}"
269 )));
270 }
271 }
272 } else {
273 let s: Option<String> = None;
274 state.serialize_field("value", &s)?;
275 }
276 state.end()
277 }
278}
279
280impl<'de> serde::Deserialize<'de> for CustomFieldEssentialsWithValue {
281 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
282 where
283 D: serde::Deserializer<'de>,
284 {
285 #[derive(serde::Deserialize)]
287 #[serde(field_identifier, rename_all = "lowercase")]
288 enum Field {
289 Id,
291 Name,
293 Multiple,
295 Value,
297 }
298
299 struct CustomFieldVisitor;
301
302 impl<'de> serde::de::Visitor<'de> for CustomFieldVisitor {
303 type Value = CustomFieldEssentialsWithValue;
304
305 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
306 formatter.write_str("struct CustomFieldEssentialsWithValue")
307 }
308
309 fn visit_map<V>(self, mut map: V) -> Result<CustomFieldEssentialsWithValue, V::Error>
310 where
311 V: serde::de::MapAccess<'de>,
312 {
313 let mut id = None;
314 let mut name = None;
315 let mut multiple = None;
316 let mut string_value: Option<String> = None;
317 let mut vec_string_value: Option<Vec<String>> = None;
318 while let Some(key) = map.next_key()? {
319 match key {
320 Field::Id => {
321 if id.is_some() {
322 return Err(serde::de::Error::duplicate_field("id"));
323 }
324 id = Some(map.next_value()?);
325 }
326 Field::Name => {
327 if name.is_some() {
328 return Err(serde::de::Error::duplicate_field("name"));
329 }
330 name = Some(map.next_value()?);
331 }
332 Field::Multiple => {
333 if multiple.is_some() {
334 return Err(serde::de::Error::duplicate_field("multiple"));
335 }
336 multiple = Some(map.next_value()?);
337 }
338 Field::Value => {
339 if string_value.is_some() {
340 return Err(serde::de::Error::duplicate_field("value"));
341 }
342 if vec_string_value.is_some() {
343 return Err(serde::de::Error::duplicate_field("value"));
344 }
345 if let Some(true) = multiple {
346 vec_string_value = Some(map.next_value()?);
347 } else {
348 string_value = map.next_value()?;
349 }
350 }
351 }
352 }
353 let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?;
354 let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
355 match (multiple, string_value, vec_string_value) {
356 (None, None, None) => Ok(CustomFieldEssentialsWithValue {
357 id,
358 name,
359 multiple: None,
360 value: None,
361 }),
362 (None, Some(s), None) => Ok(CustomFieldEssentialsWithValue {
363 id,
364 name,
365 multiple: None,
366 value: Some(vec![s]),
367 }),
368 (Some(true), None, Some(v)) => Ok(CustomFieldEssentialsWithValue {
369 id,
370 name,
371 multiple: Some(true),
372 value: Some(v),
373 }),
374 _ => Err(serde::de::Error::custom(
375 "invalid combination of multiple and value",
376 )),
377 }
378 }
379 }
380
381 const FIELDS: &[&str] = &["id", "name", "multiple", "value"];
383 deserializer.deserialize_struct(
384 "CustomFieldEssentialsWithValue",
385 FIELDS,
386 CustomFieldVisitor,
387 )
388 }
389}
390
391#[derive(Debug, Clone, Builder)]
393#[builder(setter(strip_option))]
394pub struct ListCustomFields {}
395
396impl ReturnsJsonResponse for ListCustomFields {}
397impl NoPagination for ListCustomFields {}
398
399impl ListCustomFields {
400 #[must_use]
402 pub fn builder() -> ListCustomFieldsBuilder {
403 ListCustomFieldsBuilder::default()
404 }
405}
406
407impl Endpoint for ListCustomFields {
408 fn method(&self) -> Method {
409 Method::GET
410 }
411
412 fn endpoint(&self) -> Cow<'static, str> {
413 "custom_fields.json".into()
414 }
415}
416
417#[derive(Debug, Clone, Serialize, serde::Deserialize)]
419pub struct CustomField<'a> {
420 pub id: u64,
422 pub name: Option<Cow<'a, str>>,
424 pub value: Cow<'a, str>,
426}
427
428#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
430pub struct CustomFieldsWrapper<T> {
431 pub custom_fields: Vec<T>,
433}
434
435#[cfg(test)]
436mod test {
437 use super::*;
438 use pretty_assertions::assert_eq;
439 use std::error::Error;
440 use tracing_test::traced_test;
441
442 #[traced_test]
443 #[test]
444 fn test_list_custom_fields_no_pagination() -> Result<(), Box<dyn Error>> {
445 dotenvy::dotenv()?;
446 let redmine = crate::api::Redmine::from_env(
447 reqwest::blocking::Client::builder()
448 .use_rustls_tls()
449 .build()?,
450 )?;
451 let endpoint = ListCustomFields::builder().build()?;
452 redmine.json_response_body::<_, CustomFieldsWrapper<CustomFieldDefinition>>(&endpoint)?;
453 Ok(())
454 }
455
456 #[traced_test]
461 #[test]
462 fn test_completeness_custom_fields_type() -> Result<(), Box<dyn Error>> {
463 dotenvy::dotenv()?;
464 let redmine = crate::api::Redmine::from_env(
465 reqwest::blocking::Client::builder()
466 .use_rustls_tls()
467 .build()?,
468 )?;
469 let endpoint = ListCustomFields::builder().build()?;
470 let CustomFieldsWrapper {
471 custom_fields: values,
472 } = redmine.json_response_body::<_, CustomFieldsWrapper<serde_json::Value>>(&endpoint)?;
473 for value in values {
474 let o: CustomFieldDefinition = serde_json::from_value(value.clone())?;
475 let reserialized = serde_json::to_value(o)?;
476 assert_eq!(value, reserialized);
477 }
478 Ok(())
479 }
480}