redmine_api/api/
queries.rs

1//! Queries Rest API Endpoint definitions
2//!
3//! The Redmine API for queries is read-only. It only supports listing all visible queries.
4//! The API does not expose endpoints for:
5//! - Retrieving a single query by ID (GET /queries/:id)
6//! - Creating a query (POST /queries.json)
7//! - Updating a query (PUT /queries/:id.json)
8//! - Deleting a query (DELETE /queries/:id.json)
9//!
10//! This was confirmed by examining the Redmine source code (config/routes.rb and app/controllers/queries_controller.rb).
11//! Previous analysis indicating inconsistencies due to missing client implementations for these endpoints was a false positive.
12//!
13//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_Queries)
14//!
15//! - [x] all (visible) custom queries endpoint
16
17use derive_builder::Builder;
18use reqwest::Method;
19use std::borrow::Cow;
20
21use crate::api::{Endpoint, Pageable, ReturnsJsonResponse};
22use serde_repr::{Deserialize_repr, Serialize_repr};
23
24/// The visibility of a query
25#[derive(
26    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize_repr, Deserialize_repr,
27)]
28#[repr(u8)]
29pub enum Visibility {
30    /// private
31    Private = 0,
32    /// visible to roles
33    Roles = 1,
34    /// visible to all users
35    Public = 2,
36}
37
38/// a type for query list items to use as an API return type
39///
40/// alternatively you can use your own type limited to the fields you need
41#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
42pub struct QueryListItem {
43    /// numeric id
44    pub id: u64,
45    /// display name
46    pub name: String,
47    /// is the query public
48    pub is_public: bool,
49    /// the project for project-specific queries
50    pub project_id: Option<u64>,
51}
52
53/// a type for a detailed query to use as an API return type
54///
55/// alternatively you can use your own type limited to the fields you need
56#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
57pub struct Query {
58    /// numeric id
59    pub id: u64,
60    /// display name
61    pub name: String,
62    /// is the query public
63    pub is_public: bool,
64    /// the project for project-specific queries
65    pub project_id: Option<u64>,
66    /// the user who created the query
67    pub user_id: u64,
68    /// a description of the query
69    #[serde(default)]
70    pub description: Option<String>,
71    /// the filters for the query
72    pub filters: serde_json::Value,
73    /// the column names for the query
74    pub column_names: Vec<String>,
75    /// the sort criteria for the query
76    pub sort_criteria: serde_json::Value,
77    /// the options for the query
78    pub options: serde_json::Value,
79    /// the type of the query
80    #[serde(rename = "type")]
81    pub query_type: String,
82}
83
84/// The endpoint to retrieve all queries visible to the current user
85///
86/// to actually use them pass the query_id to the ListIssues endpoint
87#[derive(Debug, Clone, Builder)]
88#[builder(setter(strip_option))]
89pub struct ListQueries {}
90
91impl ReturnsJsonResponse for ListQueries {}
92impl Pageable for ListQueries {
93    fn response_wrapper_key(&self) -> String {
94        "queries".to_string()
95    }
96}
97
98impl ListQueries {
99    /// Create a builder for the endpoint.
100    #[must_use]
101    pub fn builder() -> ListQueriesBuilder {
102        ListQueriesBuilder::default()
103    }
104}
105
106impl Endpoint for ListQueries {
107    fn method(&self) -> Method {
108        Method::GET
109    }
110
111    fn endpoint(&self) -> Cow<'static, str> {
112        "queries.json".into()
113    }
114}
115
116/// helper struct for outer layers with a queries field holding the inner data
117#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
118pub struct QueriesWrapper<T> {
119    /// to parse JSON with queries key
120    pub queries: Vec<T>,
121}
122
123#[cfg(test)]
124mod test {
125    use super::*;
126    use pretty_assertions::assert_eq;
127    use std::error::Error;
128    use tracing_test::traced_test;
129
130    #[traced_test]
131    #[test]
132    fn test_list_queries_first_page() -> Result<(), Box<dyn Error>> {
133        dotenvy::dotenv()?;
134        let redmine = crate::api::Redmine::from_env(
135            reqwest::blocking::Client::builder()
136                .use_rustls_tls()
137                .build()?,
138        )?;
139        let endpoint = ListQueries::builder().build()?;
140        redmine.json_response_body_page::<_, QueryListItem>(&endpoint, 0, 25)?;
141        Ok(())
142    }
143
144    #[traced_test]
145    #[test]
146    fn test_list_queries_all_pages() -> Result<(), Box<dyn Error>> {
147        dotenvy::dotenv()?;
148        let redmine = crate::api::Redmine::from_env(
149            reqwest::blocking::Client::builder()
150                .use_rustls_tls()
151                .build()?,
152        )?;
153        let endpoint = ListQueries::builder().build()?;
154        redmine.json_response_body_all_pages::<_, QueryListItem>(&endpoint)?;
155        Ok(())
156    }
157
158    /// this tests if any of the results contain a field we are not deserializing
159    ///
160    /// this will only catch fields we missed if they are part of the response but
161    /// it is better than nothing
162    #[traced_test]
163    #[test]
164    fn test_completeness_query_type() -> Result<(), Box<dyn Error>> {
165        dotenvy::dotenv()?;
166        let redmine = crate::api::Redmine::from_env(
167            reqwest::blocking::Client::builder()
168                .use_rustls_tls()
169                .build()?,
170        )?;
171        let endpoint = ListQueries::builder().build()?;
172        let values: Vec<serde_json::Value> = redmine.json_response_body_all_pages(&endpoint)?;
173        for value in values {
174            let o: QueryListItem = serde_json::from_value(value.clone())?;
175            let reserialized = serde_json::to_value(o)?;
176            assert_eq!(value, reserialized);
177        }
178        Ok(())
179    }
180}