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            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/// 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)]
132pub struct CustomFieldDefinition {
133    /// numeric id
134    pub id: u64,
135    /// display name
136    pub name: String,
137    /// description
138    pub description: Option<String>,
139    /// is the field editable
140    pub editable: bool,
141    /// type of Redmine object this field is customizing
142    pub customized_type: CustomizedType,
143    /// data type of the field
144    pub field_format: FieldFormat,
145    /// a regular expression to constrain possible string values
146    pub regexp: Option<String>,
147    /// a minimum length for the field
148    pub min_length: Option<usize>,
149    /// a maximum length for the field
150    pub max_length: Option<usize>,
151    /// is this field required when creating/updating an object of the customized type
152    pub is_required: Option<bool>,
153    /// can this field be used as a filter
154    pub is_filter: Option<bool>,
155    /// will this field be indexed for the search
156    pub searchable: bool,
157    /// can this field be added more than once
158    pub multiple: bool,
159    /// default value for the field
160    pub default_value: Option<String>,
161    /// visibility of the custom field
162    pub visible: bool,
163    /// which roles can see the custom field
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub roles: Option<Vec<RoleEssentials>>,
166    /// limit possible values to an explicit list of values
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub possible_values: Option<Vec<PossibleValue>>,
169    /// this field is useable in these trackers
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub trackers: Option<Vec<TrackerEssentials>>,
172    /// this field is useable in these projects (None means all projects)
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub projects: Option<Vec<ProjectEssentials>>,
175    /// is the custom field for all projects
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub is_for_all: Option<bool>,
178    /// position of the custom field in the list
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub position: Option<u64>,
181    /// url pattern for the custom field
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub url_pattern: Option<String>,
184    /// text formatting for the custom field
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub text_formatting: Option<String>,
187    /// edit tag style for the custom field
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub edit_tag_style: Option<EditTagStyle>,
190    /// user role for the custom field
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub user_role: Option<RoleFilter>,
193    /// version status for the custom field
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub version_status: Option<VersionStatusFilter>,
196    /// extensions allowed for the custom field
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub extensions_allowed: Option<String>,
199    /// full width layout for the custom field
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub full_width_layout: Option<bool>,
202    /// thousands delimiter for the custom field
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub thousands_delimiter: Option<bool>,
205    /// ratio interval for the custom field
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub ratio_interval: Option<f32>,
208}
209
210/// a type for custom field essentials with values used in other Redmine
211/// objects (e.g. issues)
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct CustomFieldEssentialsWithValue {
214    /// numeric id
215    pub id: u64,
216    /// display name
217    pub name: String,
218    /// if this is true the value is serialized as an array
219    pub multiple: Option<bool>,
220    /// value
221    pub value: Option<Vec<String>>,
222}
223
224/// a type used to list all the custom field ids and names
225#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
226pub struct CustomFieldName {
227    /// numeric id
228    pub id: u64,
229    /// display name
230    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        /// the fields in the CustomFieldEssentialsWithValue type
286        #[derive(serde::Deserialize)]
287        #[serde(field_identifier, rename_all = "lowercase")]
288        enum Field {
289            /// the id field
290            Id,
291            /// the name field
292            Name,
293            /// the multiple field
294            Multiple,
295            /// the value field
296            Value,
297        }
298
299        /// visitor to deserialize CustomFieldEssentialsWithValue
300        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        /// list of fields of CustomFieldEssentialsWithValue to pass to deserialize_struct
382        const FIELDS: &[&str] = &["id", "name", "multiple", "value"];
383        deserializer.deserialize_struct(
384            "CustomFieldEssentialsWithValue",
385            FIELDS,
386            CustomFieldVisitor,
387        )
388    }
389}
390
391/// The endpoint for all custom fields
392#[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    /// Create a builder for the endpoint.
401    #[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/// a custom field
418#[derive(Debug, Clone, Serialize, serde::Deserialize)]
419pub struct CustomField<'a> {
420    /// the custom field's id
421    pub id: u64,
422    /// is usually present in contexts where it is returned by Redmine but can be omitted when it is sent by the client
423    pub name: Option<Cow<'a, str>>,
424    /// the custom field's value
425    pub value: Cow<'a, str>,
426}
427
428/// helper struct for outer layers with a custom_fields field holding the inner data
429#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
430pub struct CustomFieldsWrapper<T> {
431    /// to parse JSON with custom_fields key
432    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    /// this tests if any of the results contain a field we are not deserializing
457    ///
458    /// this will only catch fields we missed if they are part of the response but
459    /// it is better than nothing
460    #[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}