Skip to main content

redmine_api/api/
custom_fields.rs

1//! Custom Fields Rest API Endpoint definitions
2//!
3//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_CustomFields)
4//!
5//! - [x] all custom fields endpoint
6
7use 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/// Represents the types of objects that can be customized with customized types
20/// in Redmine
21#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum CustomizedType {
24    /// Redmine Issues
25    Issue,
26    /// Redmine Time Entries
27    TimeEntry,
28    /// Redmine Projects
29    Project,
30    /// Redmine Target Versions
31    Version,
32    /// Redmine Users
33    User,
34    /// Redmine Groups
35    Group,
36    /// Redmine Activities (in time tracking)
37    Activity,
38    /// Redmine Issue Priorities
39    IssuePriority,
40    /// Redmine Document Categories
41    DocumentCategory,
42}
43
44/// The data type or format of a custom field.
45#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum FieldFormat {
48    /// a Redmine user
49    User,
50    /// a Target version
51    Version,
52    /// a string
53    String,
54    /// a text block
55    Text,
56    /// a link
57    Link,
58    /// an integer
59    Int,
60    /// a floating point number
61    Float,
62    /// a date
63    Date,
64    /// a list of values
65    List,
66    /// a boolean
67    Bool,
68    /// an enumeration
69    Enumeration,
70    /// an attachment
71    Attachment,
72    /// a progress bar
73    Progressbar,
74}
75
76/// The style of the edit tag for a custom field.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum EditTagStyle {
79    /// Dropdown list style.
80    DropDown,
81    /// Checkbox style.
82    CheckBox,
83    /// Radio button style.
84    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/// Possible values contain a value and a label
119#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub struct PossibleValue {
122    /// label for the value in a select box
123    pub label: String,
124    /// actual value
125    pub value: String,
126}
127
128/// a type for custom fields to use as an API return type
129///
130/// alternatively you can use your own type limited to the fields you need
131#[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    /// numeric id
138    pub id: u64,
139    /// display name
140    pub name: String,
141    /// description
142    pub description: Option<String>,
143    /// is the field editable
144    pub editable: bool,
145    /// type of Redmine object this field is customizing
146    pub customized_type: CustomizedType,
147    /// data type of the field
148    pub field_format: FieldFormat,
149    /// a regular expression to constrain possible string values
150    pub regexp: Option<String>,
151    /// a minimum length for the field
152    pub min_length: Option<usize>,
153    /// a maximum length for the field
154    pub max_length: Option<usize>,
155    /// is this field required when creating/updating an object of the customized type
156    pub is_required: Option<bool>,
157    /// can this field be used as a filter
158    pub is_filter: Option<bool>,
159    /// will this field be indexed for the search
160    pub searchable: bool,
161    /// can this field be added more than once
162    pub multiple: bool,
163    /// default value for the field
164    pub default_value: Option<String>,
165    /// visibility of the custom field
166    pub visible: bool,
167    /// which roles can see the custom field
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub roles: Option<Vec<RoleEssentials>>,
170    /// limit possible values to an explicit list of values
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub possible_values: Option<Vec<PossibleValue>>,
173    /// this field is useable in these trackers
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub trackers: Option<Vec<TrackerEssentials>>,
176    /// this field is useable in these projects (None means all projects)
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub projects: Option<Vec<ProjectEssentials>>,
179    /// is the custom field for all projects
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub is_for_all: Option<bool>,
182    /// position of the custom field in the list
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub position: Option<u64>,
185    /// url pattern for the custom field
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub url_pattern: Option<String>,
188    /// text formatting for the custom field
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub text_formatting: Option<String>,
191    /// edit tag style for the custom field
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub edit_tag_style: Option<EditTagStyle>,
194    /// user role for the custom field
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub user_role: Option<RoleFilter>,
197    /// version status for the custom field
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub version_status: Option<VersionStatusFilter>,
200    /// extensions allowed for the custom field
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub extensions_allowed: Option<String>,
203    /// full width layout for the custom field
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub full_width_layout: Option<bool>,
206    /// thousands delimiter for the custom field
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub thousands_delimiter: Option<bool>,
209    /// ratio interval for the custom field
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub ratio_interval: Option<f32>,
212}
213
214/// a type for custom field essentials with values used in other Redmine
215/// objects (e.g. issues)
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct CustomFieldEssentialsWithValue {
218    /// numeric id
219    pub id: u64,
220    /// display name
221    pub name: String,
222    /// if this is true the value is serialized as an array
223    pub multiple: Option<bool>,
224    /// value
225    pub value: Option<Vec<String>>,
226}
227
228/// a type used to list all the custom field ids and names
229#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
230pub struct CustomFieldName {
231    /// numeric id
232    pub id: u64,
233    /// display name
234    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        /// the fields in the CustomFieldEssentialsWithValue type
294        #[derive(serde::Deserialize)]
295        #[serde(field_identifier, rename_all = "lowercase")]
296        enum Field {
297            /// the id field
298            Id,
299            /// the name field
300            Name,
301            /// the multiple field
302            Multiple,
303            /// the value field
304            Value,
305        }
306
307        /// visitor to deserialize CustomFieldEssentialsWithValue
308        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        /// list of fields of CustomFieldEssentialsWithValue to pass to deserialize_struct
390        const FIELDS: &[&str] = &["id", "name", "multiple", "value"];
391        deserializer.deserialize_struct(
392            "CustomFieldEssentialsWithValue",
393            FIELDS,
394            CustomFieldVisitor,
395        )
396    }
397}
398
399/// The endpoint for all custom fields
400#[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    /// Create a builder for the endpoint.
413    #[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/// a custom field
430#[derive(Debug, Clone, Serialize, serde::Deserialize)]
431pub struct CustomField<'a> {
432    /// the custom field's id
433    pub id: u64,
434    /// is usually present in contexts where it is returned by Redmine but can be omitted when it is sent by the client
435    pub name: Option<Cow<'a, str>>,
436    /// the custom field's value
437    pub value: Cow<'a, str>,
438}
439
440/// helper struct for outer layers with a custom_fields field holding the inner data
441#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
442pub struct CustomFieldsWrapper<T> {
443    /// to parse JSON with custom_fields key
444    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    /// this tests if any of the results contain a field we are not deserializing
469    ///
470    /// this will only catch fields we missed if they are part of the response but
471    /// it is better than nothing
472    #[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}