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 Self::DropDown => serializer.serialize_str(""),
94 Self::CheckBox => serializer.serialize_str("check_box"),
95 Self::Radio => serializer.serialize_str("radio"),
96 }
97 }
98}
99
100impl<'de> serde::Deserialize<'de> for EditTagStyle {
101 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
102 where
103 D: serde::Deserializer<'de>,
104 {
105 let s = String::deserialize(deserializer)?;
106 match s.as_str() {
107 "" => Ok(Self::DropDown),
108 "check_box" => Ok(Self::CheckBox),
109 "radio" => Ok(Self::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)]
132#[expect(
133 clippy::struct_excessive_bools,
134 reason = "field set mirrors the Redmine REST representation"
135)]
136pub struct CustomFieldDefinition {
137 pub id: u64,
139 pub name: String,
141 pub description: Option<String>,
143 pub editable: bool,
145 pub customized_type: CustomizedType,
147 pub field_format: FieldFormat,
149 pub regexp: Option<String>,
151 pub min_length: Option<usize>,
153 pub max_length: Option<usize>,
155 pub is_required: Option<bool>,
157 pub is_filter: Option<bool>,
159 pub searchable: bool,
161 pub multiple: bool,
163 pub default_value: Option<String>,
165 pub visible: bool,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub roles: Option<Vec<RoleEssentials>>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub possible_values: Option<Vec<PossibleValue>>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub trackers: Option<Vec<TrackerEssentials>>,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub projects: Option<Vec<ProjectEssentials>>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub is_for_all: Option<bool>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub position: Option<u64>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub url_pattern: Option<String>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub text_formatting: Option<String>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub edit_tag_style: Option<EditTagStyle>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub user_role: Option<RoleFilter>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub version_status: Option<VersionStatusFilter>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub extensions_allowed: Option<String>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub full_width_layout: Option<bool>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub thousands_delimiter: Option<bool>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub ratio_interval: Option<f32>,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct CustomFieldEssentialsWithValue {
218 pub id: u64,
220 pub name: String,
222 pub multiple: Option<bool>,
224 pub value: Option<Vec<String>>,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
230pub struct CustomFieldName {
231 pub id: u64,
233 pub name: String,
235}
236
237impl serde::Serialize for CustomFieldEssentialsWithValue {
238 #[expect(
239 clippy::arithmetic_side_effects,
240 reason = "field count is bounded by the four optional fields below"
241 )]
242 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
243 where
244 S: serde::Serializer,
245 {
246 use serde::ser::SerializeStruct as _;
247 let mut len = 2;
248 if self.multiple.is_some() {
249 len += 1;
250 };
251 if self.value.is_some() {
252 len += 1;
253 }
254 let mut state = serializer.serialize_struct("CustomFieldEssentialsWithValue", len)?;
255 state.serialize_field("id", &self.id)?;
256 state.serialize_field("name", &self.name)?;
257 if let Some(ref multiple) = self.multiple {
258 state.serialize_field("multiple", &multiple)?;
259 if let Some(ref value) = self.value {
260 state.serialize_field("value", &value)?;
261 } else {
262 let s: Option<Vec<String>> = None;
263 state.serialize_field("value", &s)?;
264 }
265 } else if let Some(ref value) = self.value {
266 match value.as_slice() {
267 [] => {
268 let s: Option<String> = None;
269 state.serialize_field("value", &s)?;
270 }
271 [s] => {
272 state.serialize_field("value", &s)?;
273 }
274 values => {
275 return Err(serde::ser::Error::custom(format!(
276 "CustomFieldEssentialsWithValue multiple was set to false but value contained more than one value: {values:?}"
277 )));
278 }
279 }
280 } else {
281 let s: Option<String> = None;
282 state.serialize_field("value", &s)?;
283 }
284 state.end()
285 }
286}
287
288impl<'de> serde::Deserialize<'de> for CustomFieldEssentialsWithValue {
289 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290 where
291 D: serde::Deserializer<'de>,
292 {
293 #[derive(serde::Deserialize)]
295 #[serde(field_identifier, rename_all = "lowercase")]
296 enum Field {
297 Id,
299 Name,
301 Multiple,
303 Value,
305 }
306
307 struct CustomFieldVisitor;
309
310 impl<'de> serde::de::Visitor<'de> for CustomFieldVisitor {
311 type Value = CustomFieldEssentialsWithValue;
312
313 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
314 formatter.write_str("struct CustomFieldEssentialsWithValue")
315 }
316
317 fn visit_map<V>(self, mut map: V) -> Result<CustomFieldEssentialsWithValue, V::Error>
318 where
319 V: serde::de::MapAccess<'de>,
320 {
321 let mut id = None;
322 let mut name = None;
323 let mut multiple = None;
324 let mut string_value: Option<String> = None;
325 let mut vec_string_value: Option<Vec<String>> = None;
326 while let Some(key) = map.next_key()? {
327 match key {
328 Field::Id => {
329 if id.is_some() {
330 return Err(serde::de::Error::duplicate_field("id"));
331 }
332 id = Some(map.next_value()?);
333 }
334 Field::Name => {
335 if name.is_some() {
336 return Err(serde::de::Error::duplicate_field("name"));
337 }
338 name = Some(map.next_value()?);
339 }
340 Field::Multiple => {
341 if multiple.is_some() {
342 return Err(serde::de::Error::duplicate_field("multiple"));
343 }
344 multiple = Some(map.next_value()?);
345 }
346 Field::Value => {
347 if string_value.is_some() {
348 return Err(serde::de::Error::duplicate_field("value"));
349 }
350 if vec_string_value.is_some() {
351 return Err(serde::de::Error::duplicate_field("value"));
352 }
353 if multiple == Some(true) {
354 vec_string_value = Some(map.next_value()?);
355 } else {
356 string_value = map.next_value()?;
357 }
358 }
359 }
360 }
361 let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?;
362 let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
363 match (multiple, string_value, vec_string_value) {
364 (None, None, None) => Ok(CustomFieldEssentialsWithValue {
365 id,
366 name,
367 multiple: None,
368 value: None,
369 }),
370 (None, Some(s), None) => Ok(CustomFieldEssentialsWithValue {
371 id,
372 name,
373 multiple: None,
374 value: Some(vec![s]),
375 }),
376 (Some(true), None, Some(v)) => Ok(CustomFieldEssentialsWithValue {
377 id,
378 name,
379 multiple: Some(true),
380 value: Some(v),
381 }),
382 _ => Err(serde::de::Error::custom(
383 "invalid combination of multiple and value",
384 )),
385 }
386 }
387 }
388
389 const FIELDS: &[&str] = &["id", "name", "multiple", "value"];
391 deserializer.deserialize_struct(
392 "CustomFieldEssentialsWithValue",
393 FIELDS,
394 CustomFieldVisitor,
395 )
396 }
397}
398
399#[derive(Debug, Clone, Builder)]
401#[builder(setter(strip_option))]
402#[expect(
403 clippy::empty_structs_with_brackets,
404 reason = "derive_builder requires named-field syntax"
405)]
406pub struct ListCustomFields {}
407
408impl ReturnsJsonResponse for ListCustomFields {}
409impl NoPagination for ListCustomFields {}
410
411impl ListCustomFields {
412 #[must_use]
414 pub fn builder() -> ListCustomFieldsBuilder {
415 ListCustomFieldsBuilder::default()
416 }
417}
418
419impl Endpoint for ListCustomFields {
420 fn method(&self) -> Method {
421 Method::GET
422 }
423
424 fn endpoint(&self) -> Cow<'static, str> {
425 "custom_fields.json".into()
426 }
427}
428
429#[derive(Debug, Clone, Serialize, serde::Deserialize)]
431pub struct CustomField<'a> {
432 pub id: u64,
434 pub name: Option<Cow<'a, str>>,
436 pub value: Cow<'a, str>,
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
442pub struct CustomFieldsWrapper<T> {
443 pub custom_fields: Vec<T>,
445}
446
447#[cfg(test)]
448mod test {
449 use super::*;
450 use pretty_assertions::assert_eq;
451 use std::error::Error;
452 use tracing_test::traced_test;
453
454 #[traced_test]
455 #[test]
456 fn test_list_custom_fields_no_pagination() -> Result<(), Box<dyn Error>> {
457 dotenvy::dotenv()?;
458 let redmine = crate::api::Redmine::from_env(
459 reqwest::blocking::Client::builder()
460 .tls_backend_rustls()
461 .build()?,
462 )?;
463 let endpoint = ListCustomFields::builder().build()?;
464 redmine.json_response_body::<_, CustomFieldsWrapper<CustomFieldDefinition>>(&endpoint)?;
465 Ok(())
466 }
467
468 #[traced_test]
473 #[test]
474 fn test_completeness_custom_fields_type() -> Result<(), Box<dyn Error>> {
475 dotenvy::dotenv()?;
476 let redmine = crate::api::Redmine::from_env(
477 reqwest::blocking::Client::builder()
478 .tls_backend_rustls()
479 .build()?,
480 )?;
481 let endpoint = ListCustomFields::builder().build()?;
482 let CustomFieldsWrapper {
483 custom_fields: values,
484 } = redmine.json_response_body::<_, CustomFieldsWrapper<serde_json::Value>>(&endpoint)?;
485 for value in values {
486 let o: CustomFieldDefinition = serde_json::from_value(value.clone())?;
487 let reserialized = serde_json::to_value(o)?;
488 assert_eq!(value, reserialized);
489 }
490 Ok(())
491 }
492}