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 std::borrow::Cow;
10
11use crate::api::projects::ProjectEssentials;
12use crate::api::roles::RoleEssentials;
13use crate::api::trackers::TrackerEssentials;
14use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
15
16/// Represents the types of objects that can be customized with customized types
17/// in Redmine
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum CustomizedType {
21    /// Redmine Issues
22    Issue,
23    /// Redmine Time Entries
24    TimeEntry,
25    /// Redmine Projects
26    Project,
27    /// Redmine Target Versions
28    Version,
29    /// Redmine Users
30    User,
31    /// Redmine Groups
32    Group,
33    /// Redmine Activities (in time tracking)
34    Activity,
35    /// Redmine Issue Priorities
36    IssuePriority,
37    /// Redmine Document Categories
38    DocumentCategory,
39}
40
41/// Describes the format (data type) of a field
42#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum FieldFormat {
45    /// true or false
46    Bool,
47    /// a calendar date
48    Date,
49    /// an uploaded file
50    File,
51    /// a floating point number
52    Float,
53    /// a whole number
54    Integer,
55    /// a list of key/value pairs
56    KeyValueList,
57    /// a hyperlink
58    Link,
59    /// a list of strings
60    List,
61    /// a long text (multi-line)
62    Text,
63    /// a short text
64    String,
65    /// a Redmine user
66    User,
67    /// a Target version
68    Version,
69}
70
71/// Possible values contain a value and a label
72#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub struct PossibleValue {
75    /// label for the value in a select box
76    pub label: String,
77    /// actual value
78    pub value: String,
79}
80
81/// a type for custom fields to use as an API return type
82///
83/// alternatively you can use your own type limited to the fields you need
84#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
85pub struct CustomField {
86    /// numeric id
87    pub id: u64,
88    /// display name
89    pub name: String,
90    /// description
91    pub description: Option<String>,
92    /// is the field editable
93    pub editable: bool,
94    /// type of Redmine object this field is customizing
95    pub customized_type: CustomizedType,
96    /// data type of the field
97    pub field_format: FieldFormat,
98    /// a regular expression to constrain possible string values
99    pub regexp: Option<String>,
100    /// a minimum length for the field
101    pub min_length: Option<usize>,
102    /// a maximum length for the field
103    pub max_length: Option<usize>,
104    /// is this field required when creating/updating an object of the customized type
105    pub is_required: Option<bool>,
106    /// can this field be used as a filter
107    pub is_filter: Option<bool>,
108    /// will this field be indexed for the search
109    pub searchable: bool,
110    /// can this field be added more than once
111    pub multiple: bool,
112    /// default value for the field
113    pub default_value: Option<String>,
114    /// visibility of the custom field
115    pub visible: bool,
116    /// which roles can see the custom field
117    pub roles: Vec<RoleEssentials>,
118    /// limit possible values to an explicit list of values
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub possible_values: Option<Vec<PossibleValue>>,
121    /// this field is useable in these trackers
122    pub trackers: Vec<TrackerEssentials>,
123    /// this field is useable in these projects (None means all projects)
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub projects: Option<Vec<ProjectEssentials>>,
126}
127
128/// a type for custom field essentials with values used in other Redmine
129/// objects (e.g. issues)
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct CustomFieldEssentialsWithValue {
132    /// numeric id
133    pub id: u64,
134    /// display name
135    pub name: String,
136    /// if this is true the value is serialized as an array
137    pub multiple: Option<bool>,
138    /// value
139    pub value: Option<Vec<String>>,
140}
141
142/// a type used to list all the custom field ids and names
143#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
144pub struct CustomFieldName {
145    /// numeric id
146    pub id: u64,
147    /// display name
148    pub name: String,
149}
150
151impl serde::Serialize for CustomFieldEssentialsWithValue {
152    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
153    where
154        S: serde::Serializer,
155    {
156        use serde::ser::SerializeStruct;
157        let mut len = 2;
158        if self.multiple.is_some() {
159            len += 1;
160        };
161        if self.value.is_some() {
162            len += 1;
163        }
164        let mut state = serializer.serialize_struct("CustomFieldEssentialsWithValue", len)?;
165        state.serialize_field("id", &self.id)?;
166        state.serialize_field("name", &self.name)?;
167        if let Some(ref multiple) = self.multiple {
168            state.serialize_field("multiple", &multiple)?;
169            if let Some(ref value) = self.value {
170                state.serialize_field("value", &value)?;
171            } else {
172                let s: Option<Vec<String>> = None;
173                state.serialize_field("value", &s)?;
174            }
175        } else if let Some(ref value) = self.value {
176            match value.as_slice() {
177                [] => {
178                    let s: Option<String> = None;
179                    state.serialize_field("value", &s)?;
180                }
181                [s] => {
182                    state.serialize_field("value", &s)?;
183                }
184                values => {
185                    return Err(serde::ser::Error::custom(format!(
186                        "CustomFieldEssentialsWithValue multiple was set to false but value contained more than one value: {values:?}"
187                    )));
188                }
189            }
190        } else {
191            let s: Option<String> = None;
192            state.serialize_field("value", &s)?;
193        }
194        state.end()
195    }
196}
197
198impl<'de> serde::Deserialize<'de> for CustomFieldEssentialsWithValue {
199    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
200    where
201        D: serde::Deserializer<'de>,
202    {
203        /// the fields in the CustomFieldEssentialsWithValue type
204        #[derive(serde::Deserialize)]
205        #[serde(field_identifier, rename_all = "lowercase")]
206        enum Field {
207            /// the id field
208            Id,
209            /// the name field
210            Name,
211            /// the multiple field
212            Multiple,
213            /// the value field
214            Value,
215        }
216
217        /// visitor to deserialize CustomFieldEssentialsWithValue
218        struct CustomFieldVisitor;
219
220        impl<'de> serde::de::Visitor<'de> for CustomFieldVisitor {
221            type Value = CustomFieldEssentialsWithValue;
222
223            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
224                formatter.write_str("struct CustomFieldEssentialsWithValue")
225            }
226
227            fn visit_map<V>(self, mut map: V) -> Result<CustomFieldEssentialsWithValue, V::Error>
228            where
229                V: serde::de::MapAccess<'de>,
230            {
231                let mut id = None;
232                let mut name = None;
233                let mut multiple = None;
234                let mut string_value: Option<String> = None;
235                let mut vec_string_value: Option<Vec<String>> = None;
236                while let Some(key) = map.next_key()? {
237                    match key {
238                        Field::Id => {
239                            if id.is_some() {
240                                return Err(serde::de::Error::duplicate_field("id"));
241                            }
242                            id = Some(map.next_value()?);
243                        }
244                        Field::Name => {
245                            if name.is_some() {
246                                return Err(serde::de::Error::duplicate_field("name"));
247                            }
248                            name = Some(map.next_value()?);
249                        }
250                        Field::Multiple => {
251                            if multiple.is_some() {
252                                return Err(serde::de::Error::duplicate_field("multiple"));
253                            }
254                            multiple = Some(map.next_value()?);
255                        }
256                        Field::Value => {
257                            if string_value.is_some() {
258                                return Err(serde::de::Error::duplicate_field("value"));
259                            }
260                            if vec_string_value.is_some() {
261                                return Err(serde::de::Error::duplicate_field("value"));
262                            }
263                            if let Some(true) = multiple {
264                                vec_string_value = Some(map.next_value()?);
265                            } else {
266                                string_value = map.next_value()?;
267                            }
268                        }
269                    }
270                }
271                let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?;
272                let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
273                match (multiple, string_value, vec_string_value) {
274                    (None, None, None) => Ok(CustomFieldEssentialsWithValue {
275                        id,
276                        name,
277                        multiple: None,
278                        value: None,
279                    }),
280                    (None, Some(s), None) => Ok(CustomFieldEssentialsWithValue {
281                        id,
282                        name,
283                        multiple: None,
284                        value: Some(vec![s]),
285                    }),
286                    (Some(true), None, Some(v)) => Ok(CustomFieldEssentialsWithValue {
287                        id,
288                        name,
289                        multiple: Some(true),
290                        value: Some(v),
291                    }),
292                    _ => Err(serde::de::Error::custom(
293                        "invalid combination of multiple and value",
294                    )),
295                }
296            }
297        }
298
299        /// list of fields of CustomFieldEssentialsWithValue to pass to deserialize_struct
300        const FIELDS: &[&str] = &["id", "name", "multiple", "value"];
301        deserializer.deserialize_struct(
302            "CustomFieldEssentialsWithValue",
303            FIELDS,
304            CustomFieldVisitor,
305        )
306    }
307}
308
309/// The endpoint for all custom fields
310#[derive(Debug, Clone, Builder)]
311#[builder(setter(strip_option))]
312pub struct ListCustomFields {}
313
314impl ReturnsJsonResponse for ListCustomFields {}
315impl NoPagination for ListCustomFields {}
316
317impl ListCustomFields {
318    /// Create a builder for the endpoint.
319    #[must_use]
320    pub fn builder() -> ListCustomFieldsBuilder {
321        ListCustomFieldsBuilder::default()
322    }
323}
324
325impl Endpoint for ListCustomFields {
326    fn method(&self) -> Method {
327        Method::GET
328    }
329
330    fn endpoint(&self) -> Cow<'static, str> {
331        "custom_fields.json".into()
332    }
333}
334
335/// helper struct for outer layers with a custom_fields field holding the inner data
336#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
337pub struct CustomFieldsWrapper<T> {
338    /// to parse JSON with custom_fields key
339    pub custom_fields: Vec<T>,
340}
341
342#[cfg(test)]
343mod test {
344    use super::*;
345    use pretty_assertions::assert_eq;
346    use std::error::Error;
347    use tracing_test::traced_test;
348
349    #[traced_test]
350    #[test]
351    fn test_list_custom_fields_no_pagination() -> Result<(), Box<dyn Error>> {
352        dotenvy::dotenv()?;
353        let redmine = crate::api::Redmine::from_env(
354            reqwest::blocking::Client::builder()
355                .use_rustls_tls()
356                .build()?,
357        )?;
358        let endpoint = ListCustomFields::builder().build()?;
359        redmine.json_response_body::<_, CustomFieldsWrapper<CustomField>>(&endpoint)?;
360        Ok(())
361    }
362
363    /// this tests if any of the results contain a field we are not deserializing
364    ///
365    /// this will only catch fields we missed if they are part of the response but
366    /// it is better than nothing
367    #[traced_test]
368    #[test]
369    fn test_completeness_custom_fields_type() -> Result<(), Box<dyn Error>> {
370        dotenvy::dotenv()?;
371        let redmine = crate::api::Redmine::from_env(
372            reqwest::blocking::Client::builder()
373                .use_rustls_tls()
374                .build()?,
375        )?;
376        let endpoint = ListCustomFields::builder().build()?;
377        let CustomFieldsWrapper {
378            custom_fields: values,
379        } = redmine.json_response_body::<_, CustomFieldsWrapper<serde_json::Value>>(&endpoint)?;
380        for value in values {
381            let o: CustomField = serde_json::from_value(value.clone())?;
382            let reserialized = serde_json::to_value(o)?;
383            assert_eq!(value, reserialized);
384        }
385        Ok(())
386    }
387}