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, 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    /// type of Redmine object this field is customizing
93    pub customized_type: CustomizedType,
94    /// data type of the field
95    pub field_format: FieldFormat,
96    /// a regular expression to constrain possible string values
97    pub regexp: Option<String>,
98    /// a minimum length for the field
99    pub min_length: Option<usize>,
100    /// a maximum length for the field
101    pub max_length: Option<usize>,
102    /// is this field required when creating/updating an object of the customized type
103    pub is_required: Option<bool>,
104    /// can this field be used as a filter
105    pub is_filter: Option<bool>,
106    /// will this field be indexed for the search
107    pub searchable: bool,
108    /// can this field be added more than once
109    pub multiple: bool,
110    /// default value for the field
111    pub default_value: Option<String>,
112    /// visibility of the custom field
113    pub visible: bool,
114    /// which roles can see the custom field
115    pub roles: Vec<RoleEssentials>,
116    /// limit possible values to an explicit list of values
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub possible_values: Option<Vec<PossibleValue>>,
119    /// this field is useable in these trackers
120    pub trackers: Vec<TrackerEssentials>,
121    /// this field is useable in these projects (None means all projects)
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub projects: Option<Vec<ProjectEssentials>>,
124}
125
126/// a type for custom field essentials with values used in other Redmine
127/// objects (e.g. issues)
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct CustomFieldEssentialsWithValue {
130    /// numeric id
131    pub id: u64,
132    /// display name
133    pub name: String,
134    /// if this is true the value is serialized as an array
135    pub multiple: Option<bool>,
136    /// value
137    pub value: Option<Vec<String>>,
138}
139
140/// a type used to list all the custom field ids and names
141#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
142pub struct CustomFieldName {
143    /// numeric id
144    pub id: u64,
145    /// display name
146    pub name: String,
147}
148
149impl serde::Serialize for CustomFieldEssentialsWithValue {
150    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
151    where
152        S: serde::Serializer,
153    {
154        use serde::ser::SerializeStruct;
155        let mut len = 2;
156        if self.multiple.is_some() {
157            len += 1;
158        };
159        if self.value.is_some() {
160            len += 1;
161        }
162        let mut state = serializer.serialize_struct("CustomFieldEssentialsWithValue", len)?;
163        state.serialize_field("id", &self.id)?;
164        state.serialize_field("name", &self.name)?;
165        if let Some(ref multiple) = self.multiple {
166            state.serialize_field("multiple", &multiple)?;
167            if let Some(ref value) = self.value {
168                state.serialize_field("value", &value)?;
169            } else {
170                let s: Option<Vec<String>> = None;
171                state.serialize_field("value", &s)?;
172            }
173        } else if let Some(ref value) = self.value {
174            match value.as_slice() {
175                [] => {
176                    let s: Option<String> = None;
177                    state.serialize_field("value", &s)?;
178                }
179                [s] => {
180                    state.serialize_field("value", &s)?;
181                }
182                values => {
183                    return Err(serde::ser::Error::custom(format!("CustomFieldEssentialsWithValue multiple was set to false but value contained more than one value: {:?}", values)));
184                }
185            }
186        } else {
187            let s: Option<String> = None;
188            state.serialize_field("value", &s)?;
189        }
190        state.end()
191    }
192}
193
194impl<'de> serde::Deserialize<'de> for CustomFieldEssentialsWithValue {
195    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196    where
197        D: serde::Deserializer<'de>,
198    {
199        /// the fields in the CustomFieldEssentialsWithValue type
200        #[derive(serde::Deserialize)]
201        #[serde(field_identifier, rename_all = "lowercase")]
202        enum Field {
203            /// the id field
204            Id,
205            /// the name field
206            Name,
207            /// the multiple field
208            Multiple,
209            /// the value field
210            Value,
211        }
212
213        /// visitor to deserialize CustomFieldEssentialsWithValue
214        struct CustomFieldVisitor;
215
216        impl<'de> serde::de::Visitor<'de> for CustomFieldVisitor {
217            type Value = CustomFieldEssentialsWithValue;
218
219            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
220                formatter.write_str("struct CustomFieldEssentialsWithValue")
221            }
222
223            fn visit_map<V>(self, mut map: V) -> Result<CustomFieldEssentialsWithValue, V::Error>
224            where
225                V: serde::de::MapAccess<'de>,
226            {
227                let mut id = None;
228                let mut name = None;
229                let mut multiple = None;
230                let mut string_value: Option<String> = None;
231                let mut vec_string_value: Option<Vec<String>> = None;
232                while let Some(key) = map.next_key()? {
233                    match key {
234                        Field::Id => {
235                            if id.is_some() {
236                                return Err(serde::de::Error::duplicate_field("id"));
237                            }
238                            id = Some(map.next_value()?);
239                        }
240                        Field::Name => {
241                            if name.is_some() {
242                                return Err(serde::de::Error::duplicate_field("name"));
243                            }
244                            name = Some(map.next_value()?);
245                        }
246                        Field::Multiple => {
247                            if multiple.is_some() {
248                                return Err(serde::de::Error::duplicate_field("multiple"));
249                            }
250                            multiple = Some(map.next_value()?);
251                        }
252                        Field::Value => {
253                            if string_value.is_some() {
254                                return Err(serde::de::Error::duplicate_field("value"));
255                            }
256                            if vec_string_value.is_some() {
257                                return Err(serde::de::Error::duplicate_field("value"));
258                            }
259                            if let Some(true) = multiple {
260                                vec_string_value = Some(map.next_value()?);
261                            } else {
262                                string_value = map.next_value()?;
263                            }
264                        }
265                    }
266                }
267                let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?;
268                let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
269                match (multiple, string_value, vec_string_value) {
270                    (None, None, None) => Ok(CustomFieldEssentialsWithValue {
271                        id,
272                        name,
273                        multiple: None,
274                        value: None,
275                    }),
276                    (None, Some(s), None) => Ok(CustomFieldEssentialsWithValue {
277                        id,
278                        name,
279                        multiple: None,
280                        value: Some(vec![s]),
281                    }),
282                    (Some(true), None, Some(v)) => Ok(CustomFieldEssentialsWithValue {
283                        id,
284                        name,
285                        multiple: Some(true),
286                        value: Some(v),
287                    }),
288                    _ => Err(serde::de::Error::custom(
289                        "invalid combination of multiple and value",
290                    )),
291                }
292            }
293        }
294
295        /// list of fields of CustomFieldEssentialsWithValue to pass to deserialize_struct
296        const FIELDS: &[&str] = &["id", "name", "multiple", "value"];
297        deserializer.deserialize_struct(
298            "CustomFieldEssentialsWithValue",
299            FIELDS,
300            CustomFieldVisitor,
301        )
302    }
303}
304
305/// The endpoint for all custom fields
306#[derive(Debug, Clone, Builder)]
307#[builder(setter(strip_option))]
308pub struct ListCustomFields {}
309
310impl ReturnsJsonResponse for ListCustomFields {}
311
312impl ListCustomFields {
313    /// Create a builder for the endpoint.
314    #[must_use]
315    pub fn builder() -> ListCustomFieldsBuilder {
316        ListCustomFieldsBuilder::default()
317    }
318}
319
320impl Endpoint for ListCustomFields {
321    fn method(&self) -> Method {
322        Method::GET
323    }
324
325    fn endpoint(&self) -> Cow<'static, str> {
326        "custom_fields.json".into()
327    }
328}
329
330/// helper struct for outer layers with a custom_fields field holding the inner data
331#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
332pub struct CustomFieldsWrapper<T> {
333    /// to parse JSON with custom_fields key
334    pub custom_fields: Vec<T>,
335}
336
337#[cfg(test)]
338mod test {
339    use super::*;
340    use pretty_assertions::assert_eq;
341    use std::error::Error;
342    use tracing_test::traced_test;
343
344    #[traced_test]
345    #[test]
346    fn test_list_custom_fields_no_pagination() -> Result<(), Box<dyn Error>> {
347        dotenvy::dotenv()?;
348        let redmine = crate::api::Redmine::from_env()?;
349        let endpoint = ListCustomFields::builder().build()?;
350        redmine.json_response_body::<_, CustomFieldsWrapper<CustomField>>(&endpoint)?;
351        Ok(())
352    }
353
354    /// this tests if any of the results contain a field we are not deserializing
355    ///
356    /// this will only catch fields we missed if they are part of the response but
357    /// it is better than nothing
358    #[traced_test]
359    #[test]
360    fn test_completeness_custom_fields_type() -> Result<(), Box<dyn Error>> {
361        dotenvy::dotenv()?;
362        let redmine = crate::api::Redmine::from_env()?;
363        let endpoint = ListCustomFields::builder().build()?;
364        let CustomFieldsWrapper {
365            custom_fields: values,
366        } = redmine.json_response_body::<_, CustomFieldsWrapper<serde_json::Value>>(&endpoint)?;
367        for value in values {
368            let o: CustomField = serde_json::from_value(value.clone())?;
369            let reserialized = serde_json::to_value(o)?;
370            assert_eq!(value, reserialized);
371        }
372        Ok(())
373    }
374}