Skip to main content

raps_cli/commands/issue/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Issue management commands
5//!
6//! Commands for managing ACC (Autodesk Construction Cloud) issues.
7//! Uses the Construction Issues API: /construction/issues/v1
8
9mod attachments;
10mod comments;
11mod crud;
12mod transitions;
13
14use std::path::PathBuf;
15
16use anyhow::Result;
17use clap::Subcommand;
18
19use crate::output::OutputFormat;
20use raps_acc::IssuesClient;
21
22#[derive(Debug, Subcommand)]
23pub enum IssueCommands {
24    /// List issues in a project
25    List {
26        /// Project ID (without "b." prefix used by Data Management API)
27        project_id: String,
28
29        /// Filter by status (open, closed, etc.)
30        #[arg(short, long)]
31        status: Option<String>,
32
33        /// Only show issues created after this date (YYYY-MM-DD)
34        #[arg(long)]
35        since: Option<String>,
36    },
37
38    /// Create a new issue
39    Create {
40        /// Project ID (without "b." prefix)
41        project_id: String,
42
43        /// Issue title
44        #[arg(short, long)]
45        title: Option<String>,
46
47        /// Issue description
48        #[arg(short, long)]
49        description: Option<String>,
50
51        /// Create issues from CSV file or stdin (columns: title, description, status; use `-` for stdin)
52        #[arg(long, value_name = "FILE")]
53        from_csv: Option<PathBuf>,
54    },
55
56    /// Update an issue
57    Update {
58        /// Project ID (without "b." prefix)
59        project_id: String,
60        /// Issue ID
61        issue_id: String,
62
63        /// New status
64        #[arg(short, long)]
65        status: Option<String>,
66
67        /// New title
68        #[arg(short, long)]
69        title: Option<String>,
70    },
71
72    /// List issue types (categories) for a project
73    Types {
74        /// Project ID (without "b." prefix)
75        project_id: String,
76    },
77
78    /// Manage issue comments
79    #[command(subcommand)]
80    Comment(CommentCommands),
81
82    /// List attachments for an issue
83    Attachments {
84        /// Project ID (without "b." prefix)
85        project_id: String,
86        /// Issue ID
87        issue_id: String,
88    },
89
90    /// Transition an issue to a new status
91    Transition {
92        /// Project ID (without "b." prefix)
93        project_id: String,
94        /// Issue ID
95        issue_id: String,
96        /// Target status (open, answered, closed, etc.)
97        #[arg(short, long)]
98        to: Option<String>,
99    },
100
101    /// Delete an issue
102    Delete {
103        /// Project ID (without "b." prefix)
104        project_id: String,
105        /// Issue ID
106        issue_id: String,
107    },
108}
109
110#[derive(Debug, Subcommand)]
111pub enum CommentCommands {
112    /// List comments on an issue
113    List {
114        /// Project ID (without "b." prefix)
115        project_id: String,
116        /// Issue ID
117        issue_id: String,
118    },
119
120    /// Add a comment to an issue
121    Add {
122        /// Project ID (without "b." prefix)
123        project_id: String,
124        /// Issue ID
125        issue_id: String,
126        /// Comment body
127        #[arg(short, long)]
128        body: String,
129    },
130
131    /// Delete a comment from an issue
132    Delete {
133        /// Project ID (without "b." prefix)
134        project_id: String,
135        /// Issue ID
136        issue_id: String,
137        /// Comment ID to delete
138        comment_id: String,
139    },
140}
141
142impl IssueCommands {
143    pub async fn execute(self, client: &IssuesClient, output_format: OutputFormat) -> Result<()> {
144        match self {
145            IssueCommands::List {
146                project_id,
147                status,
148                since,
149            } => crud::list_issues(client, &project_id, status, since, output_format).await,
150            IssueCommands::Create {
151                project_id,
152                title,
153                description,
154                from_csv,
155            } => {
156                crud::create_issue(
157                    client,
158                    &project_id,
159                    title,
160                    description,
161                    from_csv,
162                    output_format,
163                )
164                .await
165            }
166            IssueCommands::Update {
167                project_id,
168                issue_id,
169                status,
170                title,
171            } => {
172                crud::update_issue(client, &project_id, &issue_id, status, title, output_format)
173                    .await
174            }
175            IssueCommands::Types { project_id } => {
176                crud::list_issue_types(client, &project_id, output_format).await
177            }
178            IssueCommands::Comment(cmd) => cmd.execute(client, output_format).await,
179            IssueCommands::Attachments {
180                project_id,
181                issue_id,
182            } => attachments::list_attachments(client, &project_id, &issue_id, output_format).await,
183            IssueCommands::Transition {
184                project_id,
185                issue_id,
186                to,
187            } => {
188                transitions::transition_issue(client, &project_id, &issue_id, to, output_format)
189                    .await
190            }
191            IssueCommands::Delete {
192                project_id,
193                issue_id,
194            } => crud::delete_issue(client, &project_id, &issue_id, output_format).await,
195        }
196    }
197}
198
199impl CommentCommands {
200    pub async fn execute(self, client: &IssuesClient, output_format: OutputFormat) -> Result<()> {
201        match self {
202            CommentCommands::List {
203                project_id,
204                issue_id,
205            } => comments::list_comments(client, &project_id, &issue_id, output_format).await,
206            CommentCommands::Add {
207                project_id,
208                issue_id,
209                body,
210            } => comments::add_comment(client, &project_id, &issue_id, &body, output_format).await,
211            CommentCommands::Delete {
212                project_id,
213                issue_id,
214                comment_id,
215            } => {
216                comments::delete_comment(client, &project_id, &issue_id, &comment_id, output_format)
217                    .await
218            }
219        }
220    }
221}
222
223/// Truncate string with ellipsis
224pub(super) fn truncate_str(s: &str, max_len: usize) -> String {
225    if s.len() <= max_len {
226        s.to_string()
227    } else {
228        format!("{}...", &s[..max_len - 3])
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_truncate_str_short() {
238        assert_eq!(truncate_str("hello", 10), "hello");
239    }
240
241    #[test]
242    fn test_truncate_str_exact() {
243        assert_eq!(truncate_str("hello", 5), "hello");
244    }
245
246    #[test]
247    fn test_truncate_str_long() {
248        let result = truncate_str("hello world", 8);
249        assert_eq!(result, "hello...");
250        assert_eq!(result.len(), 8);
251    }
252}