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!("CustomFieldEssentialsWithValue multiple was set to false but value contained more than one value: {values:?}")));
186                }
187            }
188        } else {
189            let s: Option<String> = None;
190            state.serialize_field("value", &s)?;
191        }
192        state.end()
193    }
194}
195
196impl<'de> serde::Deserialize<'de> for CustomFieldEssentialsWithValue {
197    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198    where
199        D: serde::Deserializer<'de>,
200    {
201        /// the fields in the CustomFieldEssentialsWithValue type
202        #[derive(serde::Deserialize)]
203        #[serde(field_identifier, rename_all = "lowercase")]
204        enum Field {
205            /// the id field
206            Id,
207            /// the name field
208            Name,
209            /// the multiple field
210            Multiple,
211            /// the value field
212            Value,
213        }
214
215        /// visitor to deserialize CustomFieldEssentialsWithValue
216        struct CustomFieldVisitor;
217
218        impl<'de> serde::de::Visitor<'de> for CustomFieldVisitor {
219            type Value = CustomFieldEssentialsWithValue;
220
221            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
222                formatter.write_str("struct CustomFieldEssentialsWithValue")
223            }
224
225            fn visit_map<V>(self, mut map: V) -> Result<CustomFieldEssentialsWithValue, V::Error>
226            where
227                V: serde::de::MapAccess<'de>,
228            {
229                let mut id = None;
230                let mut name = None;
231                let mut multiple = None;
232                let mut string_value: Option<String> = None;
233                let mut vec_string_value: Option<Vec<String>> = None;
234                while let Some(key) = map.next_key()? {
235                    match key {
236                        Field::Id => {
237                            if id.is_some() {
238                                return Err(serde::de::Error::duplicate_field("id"));
239                            }
240                            id = Some(map.next_value()?);
241                        }
242                        Field::Name => {
243                            if name.is_some() {
244                                return Err(serde::de::Error::duplicate_field("name"));
245                            }
246                            name = Some(map.next_value()?);
247                        }
248                        Field::Multiple => {
249                            if multiple.is_some() {
250                                return Err(serde::de::Error::duplicate_field("multiple"));
251                            }
252                            multiple = Some(map.next_value()?);
253                        }
254                        Field::Value => {
255                            if string_value.is_some() {
256                                return Err(serde::de::Error::duplicate_field("value"));
257                            }
258                            if vec_string_value.is_some() {
259                                return Err(serde::de::Error::duplicate_field("value"));
260                            }
261                            if let Some(true) = multiple {
262                                vec_string_value = Some(map.next_value()?);
263                            } else {
264                                string_value = map.next_value()?;
265                            }
266                        }
267                    }
268                }
269                let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?;
270                let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
271                match (multiple, string_value, vec_string_value) {
272                    (None, None, None) => Ok(CustomFieldEssentialsWithValue {
273                        id,
274                        name,
275                        multiple: None,
276                        value: None,
277                    }),
278                    (None, Some(s), None) => Ok(CustomFieldEssentialsWithValue {
279                        id,
280                        name,
281                        multiple: None,
282                        value: Some(vec![s]),
283                    }),
284                    (Some(true), None, Some(v)) => Ok(CustomFieldEssentialsWithValue {
285                        id,
286                        name,
287                        multiple: Some(true),
288                        value: Some(v),
289                    }),
290                    _ => Err(serde::de::Error::custom(
291                        "invalid combination of multiple and value",
292                    )),
293                }
294            }
295        }
296
297        /// list of fields of CustomFieldEssentialsWithValue to pass to deserialize_struct
298        const FIELDS: &[&str] = &["id", "name", "multiple", "value"];
299        deserializer.deserialize_struct(
300            "CustomFieldEssentialsWithValue",
301            FIELDS,
302            CustomFieldVisitor,
303        )
304    }
305}
306
307/// The endpoint for all custom fields
308#[derive(Debug, Clone, Builder)]
309#[builder(setter(strip_option))]
310pub struct ListCustomFields {}
311
312impl ReturnsJsonResponse for ListCustomFields {}
313impl NoPagination for ListCustomFields {}
314
315impl ListCustomFields {
316    /// Create a builder for the endpoint.
317    #[must_use]
318    pub fn builder() -> ListCustomFieldsBuilder {
319        ListCustomFieldsBuilder::default()
320    }
321}
322
323impl Endpoint for ListCustomFields {
324    fn method(&self) -> Method {
325        Method::GET
326    }
327
328    fn endpoint(&self) -> Cow<'static, str> {
329        "custom_fields.json".into()
330    }
331}
332
333/// helper struct for outer layers with a custom_fields field holding the inner data
334#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
335pub struct CustomFieldsWrapper<T> {
336    /// to parse JSON with custom_fields key
337    pub custom_fields: Vec<T>,
338}
339
340#[cfg(test)]
341mod test {
342    use super::*;
343    use pretty_assertions::assert_eq;
344    use std::error::Error;
345    use tracing_test::traced_test;
346
347    #[traced_test]
348    #[test]
349    fn test_list_custom_fields_no_pagination() -> Result<(), Box<dyn Error>> {
350        dotenvy::dotenv()?;
351        let redmine = crate::api::Redmine::from_env(
352            reqwest::blocking::Client::builder()
353                .use_rustls_tls()
354                .build()?,
355        )?;
356        let endpoint = ListCustomFields::builder().build()?;
357        redmine.json_response_body::<_, CustomFieldsWrapper<CustomField>>(&endpoint)?;
358        Ok(())
359    }
360
361    /// this tests if any of the results contain a field we are not deserializing
362    ///
363    /// this will only catch fields we missed if they are part of the response but
364    /// it is better than nothing
365    #[traced_test]
366    #[test]
367    fn test_completeness_custom_fields_type() -> Result<(), Box<dyn Error>> {
368        dotenvy::dotenv()?;
369        let redmine = crate::api::Redmine::from_env(
370            reqwest::blocking::Client::builder()
371                .use_rustls_tls()
372                .build()?,
373        )?;
374        let endpoint = ListCustomFields::builder().build()?;
375        let CustomFieldsWrapper {
376            custom_fields: values,
377        } = redmine.json_response_body::<_, CustomFieldsWrapper<serde_json::Value>>(&endpoint)?;
378        for value in values {
379            let o: CustomField = serde_json::from_value(value.clone())?;
380            let reserialized = serde_json::to_value(o)?;
381            assert_eq!(value, reserialized);
382        }
383        Ok(())
384    }
385}