| 1 | //! Issues. Served by the MCP tools; see [`crate::mcp`] for why. |
| 2 | |
| 3 | use { |
| 4 | crate::{ |
| 5 | client::Client, |
| 6 | error::Result, |
| 7 | models::{CreatedRef, Issue, IssueSummary}, |
| 8 | }, |
| 9 | serde_json::json, |
| 10 | std::fmt, |
| 11 | }; |
| 12 | |
| 13 | /// Which issues or pull requests to list. |
| 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 15 | pub enum State { |
| 16 | #[default] |
| 17 | Open, |
| 18 | Closed, |
| 19 | Merged, |
| 20 | All, |
| 21 | } |
| 22 | |
| 23 | impl State { |
| 24 | pub fn as_str(&self) -> &'static str { |
| 25 | match self { |
| 26 | State::Open => "open", |
| 27 | State::Closed => "closed", |
| 28 | State::Merged => "merged", |
| 29 | State::All => "all", |
| 30 | } |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | impl fmt::Display for State { |
| 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 36 | f.write_str(self.as_str()) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | impl Client { |
| 41 | /// Issues in `owner/repo`, optionally filtered by a title/body substring. |
| 42 | pub async fn issues( |
| 43 | &self, |
| 44 | repo: &str, |
| 45 | state: State, |
| 46 | query: Option<&str>, |
| 47 | ) -> Result<Vec<IssueSummary>> { |
| 48 | let mut args = json!({ "repo": repo, "state": state.as_str() }); |
| 49 | if let Some(query) = query { |
| 50 | args["query"] = query.into(); |
| 51 | } |
| 52 | self.call_tool_as("list_issues", args).await |
| 53 | } |
| 54 | |
| 55 | /// One issue with its comments. |
| 56 | pub async fn issue(&self, repo: &str, number: i64) -> Result<Issue> { |
| 57 | self.call_tool_as("get_issue", json!({ "repo": repo, "number": number })) |
| 58 | .await |
| 59 | } |
| 60 | |
| 61 | /// Open a new issue. |
| 62 | pub async fn create_issue( |
| 63 | &self, |
| 64 | repo: &str, |
| 65 | title: &str, |
| 66 | body: Option<&str>, |
| 67 | ) -> Result<CreatedRef> { |
| 68 | let mut args = json!({ "repo": repo, "title": title }); |
| 69 | if let Some(body) = body { |
| 70 | args["body"] = body.into(); |
| 71 | } |
| 72 | self.call_tool_as("create_issue", args).await |
| 73 | } |
| 74 | |
| 75 | /// Comment on an issue *or* a pull request — they share one number space. |
| 76 | pub async fn add_comment(&self, repo: &str, number: i64, body: &str) -> Result<CommentRef> { |
| 77 | let args = json!({ "repo": repo, "number": number, "body": body }); |
| 78 | self.call_tool_as("add_issue_comment", args).await |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | /// What `add_issue_comment` returns. |
| 83 | #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] |
| 84 | pub struct CommentRef { |
| 85 | pub id: i64, |
| 86 | pub url: String, |
| 87 | } |
| 88 | |
| 89 | #[cfg(test)] |
| 90 | mod tests { |
| 91 | use super::*; |
| 92 | |
| 93 | #[test] |
| 94 | fn states_use_the_wire_spelling_the_server_expects() { |
| 95 | assert_eq!(State::Open.as_str(), "open"); |
| 96 | assert_eq!(State::Merged.as_str(), "merged"); |
| 97 | assert_eq!(State::default(), State::Open); |
| 98 | } |
| 99 | } |