From 95d1283c50c70f20a5c6aec694e04b3ac2aee9f4 Mon Sep 17 00:00:00 2001 From: 23prime <23.prime.37@gmail.com> Date: Tue, 17 Mar 2026 12:24:53 +0900 Subject: [PATCH] feat: bl priority list + bl resolution list --- src/api/mod.rs | 16 +++ src/api/priority.rs | 49 ++++++++++ src/api/resolution.rs | 49 ++++++++++ src/cmd/mod.rs | 2 + src/cmd/priority/list.rs | 97 +++++++++++++++++++ src/cmd/priority/mod.rs | 3 + src/cmd/resolution/list.rs | 97 +++++++++++++++++++ src/cmd/resolution/mod.rs | 3 + src/main.rs | 40 ++++++++ website/docs/commands.md | 38 +++++++- .../current/commands.md | 38 +++++++- 11 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 src/api/priority.rs create mode 100644 src/api/resolution.rs create mode 100644 src/cmd/priority/list.rs create mode 100644 src/cmd/priority/mod.rs create mode 100644 src/cmd/resolution/list.rs create mode 100644 src/cmd/resolution/mod.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index e99db93..a77b3a5 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -11,7 +11,9 @@ pub mod disk_usage; pub mod issue; pub mod licence; pub mod notification; +pub mod priority; pub mod project; +pub mod resolution; pub mod space; pub mod space_notification; pub mod team; @@ -27,10 +29,12 @@ use issue::{ }; use licence::Licence; use notification::{Notification, NotificationCount}; +use priority::Priority; use project::{ Project, ProjectCategory, ProjectDiskUsage, ProjectIssueType, ProjectStatus, ProjectUser, ProjectVersion, }; +use resolution::Resolution; use space::Space; use space_notification::SpaceNotification; use team::Team; @@ -289,6 +293,12 @@ pub trait BacklogApi { fn put_space_notification(&self, _content: &str) -> Result { unimplemented!() } + fn get_priorities(&self) -> Result> { + unimplemented!() + } + fn get_resolutions(&self) -> Result> { + unimplemented!() + } fn get_watchings(&self, _user_id: u64, _params: &[(String, String)]) -> Result> { unimplemented!() } @@ -611,6 +621,12 @@ impl BacklogApi for BacklogClient { fn put_space_notification(&self, content: &str) -> Result { self.put_space_notification(content) } + fn get_priorities(&self) -> Result> { + self.get_priorities() + } + fn get_resolutions(&self) -> Result> { + self.get_resolutions() + } fn get_watchings(&self, user_id: u64, params: &[(String, String)]) -> Result> { self.get_watchings(user_id, params) } diff --git a/src/api/priority.rs b/src/api/priority.rs new file mode 100644 index 0000000..6fb9bda --- /dev/null +++ b/src/api/priority.rs @@ -0,0 +1,49 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +use super::BacklogClient; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Priority { + pub id: u64, + pub name: String, +} + +impl BacklogClient { + pub fn get_priorities(&self) -> Result> { + let value = self.get_with_query("/priorities", &[])?; + serde_json::from_value(value.clone()).map_err(|e| { + anyhow::anyhow!( + "Failed to deserialize response: {}\nRaw JSON:\n{}", + e, + serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string()) + ) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use httpmock::prelude::*; + + #[test] + fn get_priorities_parses_response() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/priorities"); + then.status(200) + .header("content-type", "application/json") + .json_body(serde_json::json!([ + {"id": 2, "name": "High"}, + {"id": 3, "name": "Normal"}, + {"id": 4, "name": "Low"} + ])); + }); + let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap(); + let result = client.get_priorities().unwrap(); + assert_eq!(result.len(), 3); + assert_eq!(result[0].id, 2); + assert_eq!(result[0].name, "High"); + } +} diff --git a/src/api/resolution.rs b/src/api/resolution.rs new file mode 100644 index 0000000..e0fd850 --- /dev/null +++ b/src/api/resolution.rs @@ -0,0 +1,49 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +use super::BacklogClient; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Resolution { + pub id: u64, + pub name: String, +} + +impl BacklogClient { + pub fn get_resolutions(&self) -> Result> { + let value = self.get_with_query("/resolutions", &[])?; + serde_json::from_value(value.clone()).map_err(|e| { + anyhow::anyhow!( + "Failed to deserialize response: {}\nRaw JSON:\n{}", + e, + serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string()) + ) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use httpmock::prelude::*; + + #[test] + fn get_resolutions_parses_response() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/resolutions"); + then.status(200) + .header("content-type", "application/json") + .json_body(serde_json::json!([ + {"id": 0, "name": "Fixed"}, + {"id": 1, "name": "Won't Fix"}, + {"id": 2, "name": "Invalid"} + ])); + }); + let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap(); + let result = client.get_resolutions().unwrap(); + assert_eq!(result.len(), 3); + assert_eq!(result[0].id, 0); + assert_eq!(result[0].name, "Fixed"); + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index fe95744..264625b 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -2,7 +2,9 @@ pub mod auth; pub mod banner; pub mod issue; pub mod notification; +pub mod priority; pub mod project; +pub mod resolution; pub mod space; pub mod team; pub mod user; diff --git a/src/cmd/priority/list.rs b/src/cmd/priority/list.rs new file mode 100644 index 0000000..3aee8a8 --- /dev/null +++ b/src/cmd/priority/list.rs @@ -0,0 +1,97 @@ +use anstream::println; +use anyhow::{Context, Result}; + +use crate::api::{BacklogApi, BacklogClient}; + +pub struct PriorityListArgs { + json: bool, +} + +impl PriorityListArgs { + pub fn new(json: bool) -> Self { + Self { json } + } +} + +pub fn list(args: &PriorityListArgs) -> Result<()> { + let client = BacklogClient::from_config()?; + list_with(args, &client) +} + +pub fn list_with(args: &PriorityListArgs, api: &dyn BacklogApi) -> Result<()> { + let priorities = api.get_priorities()?; + if args.json { + println!( + "{}", + serde_json::to_string_pretty(&priorities).context("Failed to serialize JSON")? + ); + } else { + for p in &priorities { + println!("[{}] {}", p.id, p.name); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::priority::Priority; + use anyhow::anyhow; + + struct MockApi { + priorities: Option>, + } + + impl crate::api::BacklogApi for MockApi { + fn get_priorities(&self) -> anyhow::Result> { + self.priorities + .clone() + .ok_or_else(|| anyhow!("no priorities")) + } + } + + fn sample_priorities() -> Vec { + vec![ + Priority { + id: 2, + name: "High".to_string(), + }, + Priority { + id: 3, + name: "Normal".to_string(), + }, + Priority { + id: 4, + name: "Low".to_string(), + }, + ] + } + + fn args(json: bool) -> PriorityListArgs { + PriorityListArgs::new(json) + } + + #[test] + fn list_with_text_output_succeeds() { + let api = MockApi { + priorities: Some(sample_priorities()), + }; + assert!(list_with(&args(false), &api).is_ok()); + } + + #[test] + fn list_with_json_output_succeeds() { + let api = MockApi { + priorities: Some(sample_priorities()), + }; + assert!(list_with(&args(true), &api).is_ok()); + } + + #[test] + fn list_with_propagates_api_error() { + let api = MockApi { priorities: None }; + let err = list_with(&args(false), &api).unwrap_err(); + assert!(err.to_string().contains("no priorities")); + } +} diff --git a/src/cmd/priority/mod.rs b/src/cmd/priority/mod.rs new file mode 100644 index 0000000..466e27c --- /dev/null +++ b/src/cmd/priority/mod.rs @@ -0,0 +1,3 @@ +mod list; + +pub use list::{PriorityListArgs, list}; diff --git a/src/cmd/resolution/list.rs b/src/cmd/resolution/list.rs new file mode 100644 index 0000000..e46d97f --- /dev/null +++ b/src/cmd/resolution/list.rs @@ -0,0 +1,97 @@ +use anstream::println; +use anyhow::{Context, Result}; + +use crate::api::{BacklogApi, BacklogClient}; + +pub struct ResolutionListArgs { + json: bool, +} + +impl ResolutionListArgs { + pub fn new(json: bool) -> Self { + Self { json } + } +} + +pub fn list(args: &ResolutionListArgs) -> Result<()> { + let client = BacklogClient::from_config()?; + list_with(args, &client) +} + +pub fn list_with(args: &ResolutionListArgs, api: &dyn BacklogApi) -> Result<()> { + let resolutions = api.get_resolutions()?; + if args.json { + println!( + "{}", + serde_json::to_string_pretty(&resolutions).context("Failed to serialize JSON")? + ); + } else { + for r in &resolutions { + println!("[{}] {}", r.id, r.name); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::resolution::Resolution; + use anyhow::anyhow; + + struct MockApi { + resolutions: Option>, + } + + impl crate::api::BacklogApi for MockApi { + fn get_resolutions(&self) -> anyhow::Result> { + self.resolutions + .clone() + .ok_or_else(|| anyhow!("no resolutions")) + } + } + + fn sample_resolutions() -> Vec { + vec![ + Resolution { + id: 0, + name: "Fixed".to_string(), + }, + Resolution { + id: 1, + name: "Won't Fix".to_string(), + }, + Resolution { + id: 2, + name: "Invalid".to_string(), + }, + ] + } + + fn args(json: bool) -> ResolutionListArgs { + ResolutionListArgs::new(json) + } + + #[test] + fn list_with_text_output_succeeds() { + let api = MockApi { + resolutions: Some(sample_resolutions()), + }; + assert!(list_with(&args(false), &api).is_ok()); + } + + #[test] + fn list_with_json_output_succeeds() { + let api = MockApi { + resolutions: Some(sample_resolutions()), + }; + assert!(list_with(&args(true), &api).is_ok()); + } + + #[test] + fn list_with_propagates_api_error() { + let api = MockApi { resolutions: None }; + let err = list_with(&args(false), &api).unwrap_err(); + assert!(err.to_string().contains("no resolutions")); + } +} diff --git a/src/cmd/resolution/mod.rs b/src/cmd/resolution/mod.rs new file mode 100644 index 0000000..654edc7 --- /dev/null +++ b/src/cmd/resolution/mod.rs @@ -0,0 +1,3 @@ +mod list; + +pub use list::{ResolutionListArgs, list}; diff --git a/src/main.rs b/src/main.rs index 8b21d03..dc5051d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,7 @@ use cmd::issue::{ IssueUpdateArgs, ParentChild, }; use cmd::notification::{NotificationCountArgs, NotificationListArgs, NotificationReadArgs}; +use cmd::priority::PriorityListArgs; use cmd::project::category::ProjectCategoryListArgs; use cmd::project::issue_type::ProjectIssueTypeListArgs; use cmd::project::status::ProjectStatusListArgs; @@ -37,6 +38,7 @@ use cmd::project::{ ProjectActivitiesArgs, ProjectCreateArgs, ProjectDeleteArgs, ProjectDiskUsageArgs, ProjectListArgs, ProjectShowArgs, ProjectUpdateArgs, }; +use cmd::resolution::ResolutionListArgs; use cmd::space::{ SpaceActivitiesArgs, SpaceDiskUsageArgs, SpaceLicenceArgs, SpaceNotificationArgs, SpaceShowArgs, SpaceUpdateNotificationArgs, @@ -125,6 +127,16 @@ enum Commands { #[command(subcommand)] action: WatchCommands, }, + /// List priorities + Priority { + #[command(subcommand)] + action: PriorityCommands, + }, + /// List resolutions + Resolution { + #[command(subcommand)] + action: ResolutionCommands, + }, } #[derive(clap::ValueEnum, Clone)] @@ -1201,6 +1213,26 @@ enum WatchCommands { }, } +#[derive(Subcommand)] +enum PriorityCommands { + /// List priorities + List { + /// Output as JSON + #[arg(long)] + json: bool, + }, +} + +#[derive(Subcommand)] +enum ResolutionCommands { + /// List resolutions + List { + /// Output as JSON + #[arg(long)] + json: bool, + }, +} + #[derive(Subcommand)] enum AuthCommands { /// Login with your API key @@ -1823,6 +1855,14 @@ fn run() -> Result<()> { } WatchCommands::Read { id } => cmd::watch::read(&WatchReadArgs::new(id)), }, + Commands::Priority { action } => match action { + PriorityCommands::List { json } => cmd::priority::list(&PriorityListArgs::new(json)), + }, + Commands::Resolution { action } => match action { + ResolutionCommands::List { json } => { + cmd::resolution::list(&ResolutionListArgs::new(json)) + } + }, Commands::Space { action, json } => match action { None => cmd::space::show(&SpaceShowArgs::new(json)), Some(SpaceCommands::Activities { diff --git a/website/docs/commands.md b/website/docs/commands.md index fb3d3a0..16e7d75 100644 --- a/website/docs/commands.md +++ b/website/docs/commands.md @@ -1109,6 +1109,40 @@ bl watch read On success, this command prints no output (HTTP 204 No Content). +## `bl priority list` + +List issue priorities. + +```bash +bl priority list [--json] +``` + +Example output: + +```text +[2] High +[3] Normal +[4] Low +``` + +## `bl resolution list` + +List issue resolutions. + +```bash +bl resolution list [--json] +``` + +Example output: + +```text +[0] Fixed +[1] Won't Fix +[2] Invalid +[3] Duplication +[4] Cannot Reproduce +``` + ## Command coverage The table below maps Backlog API v2 endpoints to `bl` commands. @@ -1181,8 +1215,8 @@ The table below maps Backlog API v2 endpoints to `bl` commands. | Command | API endpoint | Status | | --- | --- | --- | -| `bl priority list` | `GET /api/v2/priorities` | Planned | -| `bl resolution list` | `GET /api/v2/resolutions` | Planned | +| `bl priority list` | `GET /api/v2/priorities` | ✅ Implemented | +| `bl resolution list` | `GET /api/v2/resolutions` | ✅ Implemented | ### Issues diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md b/website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md index 928f669..b6ae306 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md @@ -1113,6 +1113,40 @@ bl watch read 成功時は出力されません(HTTP 204 No Content)。 +## `bl priority list` + +課題優先度の一覧を表示します。 + +```bash +bl priority list [--json] +``` + +出力例: + +```text +[2] 高 +[3] 中 +[4] 低 +``` + +## `bl resolution list` + +課題解決理由の一覧を表示します。 + +```bash +bl resolution list [--json] +``` + +出力例: + +```text +[0] 対応済み +[1] 対応しない +[2] 無効 +[3] 重複 +[4] 再現しない +``` + ## コマンドカバレッジ Backlog API v2 エンドポイントと `bl` コマンドの対応表です。 @@ -1185,8 +1219,8 @@ Backlog API v2 エンドポイントと `bl` コマンドの対応表です。 | コマンド | API エンドポイント | 状態 | | --- | --- | --- | -| `bl priority list` | `GET /api/v2/priorities` | 計画中 | -| `bl resolution list` | `GET /api/v2/resolutions` | 計画中 | +| `bl priority list` | `GET /api/v2/priorities` | ✅ 実装済み | +| `bl resolution list` | `GET /api/v2/resolutions` | ✅ 実装済み | ### Issues