1use serde::Deserialize;
2
3use crate::api::issues::UserRef;
4use crate::api::{Client, Projection};
5use crate::error::Result;
6use crate::fields::schema::check_short_name;
7use crate::fields::{choices, closest, same, usage};
8
9#[derive(Clone, Debug, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct Project {
12 pub short_name: String,
13 #[serde(default)]
14 pub name: String,
15 pub leader: Option<UserRef>,
16 pub description: Option<String>,
17 #[serde(default)]
18 pub archived: bool,
19}
20
21impl Projection for Project {
22 const FIELDS: &'static str = "shortName,name,leader(login,fullName),description,archived";
23}
24
25pub async fn list(client: &Client) -> Result<Vec<Project>> {
27 client.get_all("admin/projects").await
28}
29
30pub async fn short_names(client: &Client) -> Result<Vec<String>> {
32 Ok(list(client)
33 .await?
34 .into_iter()
35 .filter(|project| !project.archived)
36 .map(|project| project.short_name)
37 .collect())
38}
39
40pub fn find<'a>(projects: &'a [Project], name: &str) -> Result<&'a Project> {
42 if let Some(project) = projects
43 .iter()
44 .find(|project| same(&project.short_name, name))
45 {
46 return Ok(project);
47 }
48 let names: Vec<String> = projects
49 .iter()
50 .map(|project| project.short_name.clone())
51 .collect();
52 let mut message = format!("there is no project '{name}'");
53 if let Some(close) = closest(name, names.iter().map(String::as_str)) {
54 message.push_str(&format!(" (did you mean '{close}'?)"));
55 } else if !names.is_empty() {
56 message.push_str(&format!("; projects: {}", choices(&names)));
57 }
58 Err(usage(message))
59}
60
61#[derive(Clone, Debug, Deserialize)]
62pub struct WorkItemType {
63 pub id: String,
64 pub name: String,
65}
66
67impl Projection for WorkItemType {
68 const FIELDS: &'static str = "id,name";
69}
70
71#[derive(Clone, Debug, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct TimeTracking {
74 #[serde(default)]
75 pub enabled: bool,
76 #[serde(default)]
77 pub work_item_types: Vec<WorkItemType>,
78}
79
80impl Projection for TimeTracking {
81 const FIELDS: &'static str = "enabled,workItemTypes(id,name)";
82}
83
84pub async fn time_tracking(client: &Client, project: &str) -> Result<TimeTracking> {
85 check_short_name(project)?;
86 client
87 .get(
88 &format!("admin/projects/{project}/timeTrackingSettings"),
89 &[],
90 )
91 .await
92}
93
94#[cfg(test)]
95mod tests {
96 use serde_json::json;
97 use wiremock::matchers::{method, path};
98 use wiremock::{Mock, MockServer, ResponseTemplate};
99
100 use super::*;
101 use crate::api::test_client;
102
103 #[tokio::test]
104 async fn archived_projects_are_left_out() {
105 let server = MockServer::start().await;
106 Mock::given(method("GET"))
107 .and(path("/api/admin/projects"))
108 .respond_with(ResponseTemplate::new(200).set_body_json(json!([
109 {"shortName": "WEB", "archived": false},
110 {"shortName": "OLD", "archived": true}
111 ])))
112 .mount(&server)
113 .await;
114 assert_eq!(
115 short_names(&test_client(&server.uri())).await.unwrap(),
116 ["WEB"]
117 );
118 }
119
120 fn project(short: &str) -> Project {
121 Project {
122 short_name: short.into(),
123 name: format!("{short} project"),
124 leader: None,
125 description: None,
126 archived: false,
127 }
128 }
129
130 #[test]
131 fn projects_are_found_in_any_case_with_a_suggestion_otherwise() {
132 let projects = [project("TEST"), project("APP")];
133 assert_eq!(find(&projects, "test").unwrap().short_name, "TEST");
134 let error = find(&projects, "TES").unwrap_err().to_string();
135 assert!(error.contains("did you mean 'TEST'"), "{error}");
136 let error = find(&projects, "zzz").unwrap_err().to_string();
137 assert!(error.contains("projects: TEST, APP"), "{error}");
138 }
139
140 #[tokio::test]
141 async fn time_tracking_settings_are_read_by_short_name() {
142 let server = MockServer::start().await;
143 Mock::given(method("GET"))
144 .and(path("/api/admin/projects/TEST/timeTrackingSettings"))
145 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
146 "enabled": true,
147 "workItemTypes": [{"id": "85-0", "name": "Development"}]
148 })))
149 .mount(&server)
150 .await;
151 let settings = time_tracking(&test_client(&server.uri()), "TEST")
152 .await
153 .unwrap();
154 assert!(settings.enabled);
155 assert_eq!(settings.work_item_types[0].id, "85-0");
156 assert!(
157 time_tracking(&test_client(&server.uri()), "../x")
158 .await
159 .is_err()
160 );
161 }
162}