Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,12 @@ pub trait BacklogApi {
fn count_user_stars(&self, _user_id: u64, _params: &[(String, String)]) -> Result<StarCount> {
unimplemented!()
}
fn add_star(&self, _params: &[(String, String)]) -> Result<()> {
unimplemented!()
}
fn delete_star(&self, _star_id: u64) -> Result<()> {
unimplemented!()
}
fn get_notifications(&self, _params: &[(String, String)]) -> Result<Vec<Notification>> {
unimplemented!()
}
Expand Down Expand Up @@ -1084,6 +1090,14 @@ impl BacklogApi for BacklogClient {
self.count_user_stars(user_id, params)
}

fn add_star(&self, params: &[(String, String)]) -> Result<()> {
self.add_star(params)
}

fn delete_star(&self, star_id: u64) -> Result<()> {
self.delete_star(star_id)
}

fn get_notifications(&self, params: &[(String, String)]) -> Result<Vec<Notification>> {
self.get_notifications(params)
}
Expand Down
10 changes: 10 additions & 0 deletions src/api/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ impl BacklogClient {
let value = self.get_with_query(&format!("/users/{user_id}/stars/count"), params)?;
deserialize(value, "star count response")
}

pub fn add_star(&self, params: &[(String, String)]) -> Result<()> {
self.post_form("/stars", params)?;
Ok(())
}

pub fn delete_star(&self, star_id: u64) -> Result<()> {
self.delete_req(&format!("/stars/{star_id}"))?;
Ok(())
}
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod project;
pub mod rate_limit;
pub mod resolution;
pub mod space;
pub mod star;
pub mod team;
pub mod user;
pub mod watch;
Expand Down
195 changes: 195 additions & 0 deletions src/cmd/star/add.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
use anyhow::Result;

use crate::api::{BacklogApi, BacklogClient};

pub struct StarAddArgs {
issue_id: Option<u64>,
comment_id: Option<u64>,
wiki_id: Option<u64>,
pull_request_id: Option<u64>,
pull_request_comment_id: Option<u64>,
}

impl StarAddArgs {
pub fn try_new(
issue_id: Option<u64>,
comment_id: Option<u64>,
wiki_id: Option<u64>,
pull_request_id: Option<u64>,
pull_request_comment_id: Option<u64>,
) -> anyhow::Result<Self> {
let count = [
issue_id.is_some(),
comment_id.is_some(),
wiki_id.is_some(),
pull_request_id.is_some(),
pull_request_comment_id.is_some(),
]
.iter()
.filter(|&&b| b)
.count();
if count != 1 {
anyhow::bail!(
"exactly one of --issue-id, --comment-id, --wiki-id, \
--pull-request-id, --pull-request-comment-id must be specified"
);
}
Ok(Self {
issue_id,
comment_id,
wiki_id,
pull_request_id,
pull_request_comment_id,
})
}
}

pub fn add(args: &StarAddArgs) -> Result<()> {
let client = BacklogClient::from_config()?;
add_with(args, &client)
}

pub fn add_with(args: &StarAddArgs, api: &dyn BacklogApi) -> Result<()> {
let mut params: Vec<(String, String)> = Vec::new();
if let Some(id) = args.issue_id {
params.push(("issueId".to_string(), id.to_string()));
}
if let Some(id) = args.comment_id {
params.push(("commentId".to_string(), id.to_string()));
}
if let Some(id) = args.wiki_id {
params.push(("wikiId".to_string(), id.to_string()));
}
if let Some(id) = args.pull_request_id {
params.push(("pullRequestId".to_string(), id.to_string()));
}
if let Some(id) = args.pull_request_comment_id {
params.push(("pullRequestCommentId".to_string(), id.to_string()));
}
api.add_star(&params)
}

#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
use std::cell::RefCell;

struct MockApi {
ok: bool,
captured_params: RefCell<Vec<(String, String)>>,
}

impl MockApi {
fn new(ok: bool) -> Self {
Self {
ok,
captured_params: RefCell::new(Vec::new()),
}
}
}

impl crate::api::BacklogApi for MockApi {
fn add_star(&self, params: &[(String, String)]) -> anyhow::Result<()> {
*self.captured_params.borrow_mut() = params.to_vec();
if self.ok {
Ok(())
} else {
Err(anyhow!("api error"))
}
}
}

fn args_with_issue() -> StarAddArgs {
StarAddArgs::try_new(Some(1), None, None, None, None).unwrap()
}

#[test]
fn add_with_succeeds() {
let api = MockApi::new(true);
assert!(add_with(&args_with_issue(), &api).is_ok());
}

#[test]
fn add_with_propagates_api_error() {
let api = MockApi::new(false);
let err = add_with(&args_with_issue(), &api).unwrap_err();
assert!(err.to_string().contains("api error"));
}

#[test]
fn try_new_rejects_no_target() {
assert!(StarAddArgs::try_new(None, None, None, None, None).is_err());
}

#[test]
fn try_new_rejects_multiple_targets() {
assert!(StarAddArgs::try_new(Some(1), Some(2), None, None, None).is_err());
}

#[test]
fn try_new_accepts_each_target() {
assert!(StarAddArgs::try_new(Some(1), None, None, None, None).is_ok());
assert!(StarAddArgs::try_new(None, Some(1), None, None, None).is_ok());
assert!(StarAddArgs::try_new(None, None, Some(1), None, None).is_ok());
assert!(StarAddArgs::try_new(None, None, None, Some(1), None).is_ok());
assert!(StarAddArgs::try_new(None, None, None, None, Some(1)).is_ok());
}

#[test]
fn add_with_sends_issue_id_param() {
let api = MockApi::new(true);
let args = StarAddArgs::try_new(Some(42), None, None, None, None).unwrap();
add_with(&args, &api).unwrap();
let params = api.captured_params.borrow();
assert_eq!(
params.as_slice(),
[("issueId".to_string(), "42".to_string())]
);
}

#[test]
fn add_with_sends_comment_id_param() {
let api = MockApi::new(true);
let args = StarAddArgs::try_new(None, Some(10), None, None, None).unwrap();
add_with(&args, &api).unwrap();
let params = api.captured_params.borrow();
assert_eq!(
params.as_slice(),
[("commentId".to_string(), "10".to_string())]
);
}

#[test]
fn add_with_sends_wiki_id_param() {
let api = MockApi::new(true);
let args = StarAddArgs::try_new(None, None, Some(5), None, None).unwrap();
add_with(&args, &api).unwrap();
let params = api.captured_params.borrow();
assert_eq!(params.as_slice(), [("wikiId".to_string(), "5".to_string())]);
}

#[test]
fn add_with_sends_pull_request_id_param() {
let api = MockApi::new(true);
let args = StarAddArgs::try_new(None, None, None, Some(7), None).unwrap();
add_with(&args, &api).unwrap();
let params = api.captured_params.borrow();
assert_eq!(
params.as_slice(),
[("pullRequestId".to_string(), "7".to_string())]
);
}

#[test]
fn add_with_sends_pull_request_comment_id_param() {
let api = MockApi::new(true);
let args = StarAddArgs::try_new(None, None, None, None, Some(3)).unwrap();
add_with(&args, &api).unwrap();
let params = api.captured_params.borrow();
assert_eq!(
params.as_slice(),
[("pullRequestCommentId".to_string(), "3".to_string())]
);
}
}
59 changes: 59 additions & 0 deletions src/cmd/star/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use anyhow::Result;

use crate::api::{BacklogApi, BacklogClient};

pub struct StarDeleteArgs {
star_id: u64,
}

impl StarDeleteArgs {
pub fn new(star_id: u64) -> Self {
Self { star_id }
}
}

pub fn delete(args: &StarDeleteArgs) -> Result<()> {
let client = BacklogClient::from_config()?;
delete_with(args, &client)
}

pub fn delete_with(args: &StarDeleteArgs, api: &dyn BacklogApi) -> Result<()> {
api.delete_star(args.star_id)
}

#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;

struct MockApi {
ok: bool,
}

impl crate::api::BacklogApi for MockApi {
fn delete_star(&self, _star_id: u64) -> anyhow::Result<()> {
if self.ok {
Ok(())
} else {
Err(anyhow!("api error"))
}
}
}

fn args() -> StarDeleteArgs {
StarDeleteArgs::new(42)
}

#[test]
fn delete_with_succeeds() {
let api = MockApi { ok: true };
assert!(delete_with(&args(), &api).is_ok());
}

#[test]
fn delete_with_propagates_api_error() {
let api = MockApi { ok: false };
let err = delete_with(&args(), &api).unwrap_err();
assert!(err.to_string().contains("api error"));
}
}
5 changes: 5 additions & 0 deletions src/cmd/star/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod add;
mod delete;

pub use add::{StarAddArgs, add};
pub use delete::{StarDeleteArgs, delete};
49 changes: 49 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ use cmd::space::{
SpaceActivitiesArgs, SpaceDiskUsageArgs, SpaceLicenceArgs, SpaceNotificationArgs,
SpaceShowArgs, SpaceUpdateNotificationArgs,
};
use cmd::star::{StarAddArgs, StarDeleteArgs};
use cmd::team::{TeamListArgs, TeamShowArgs};
use cmd::user::star::{UserStarCountArgs, UserStarListArgs};
use cmd::user::{
Expand Down Expand Up @@ -176,6 +177,11 @@ enum Commands {
#[arg(long)]
json: bool,
},
/// Manage stars
Star {
#[command(subcommand)]
action: StarCommands,
},
}

#[derive(clap::ValueEnum, Clone)]
Expand Down Expand Up @@ -1868,6 +1874,33 @@ enum ResolutionCommands {
},
}

#[derive(Subcommand)]
enum StarCommands {
/// Add a star to an issue, comment, wiki page, pull request, or pull request comment
Add {
/// ID of the issue to star
#[arg(long)]
issue_id: Option<u64>,
/// ID of the issue comment to star
#[arg(long)]
comment_id: Option<u64>,
/// ID of the wiki page to star
#[arg(long)]
wiki_id: Option<u64>,
/// ID of the pull request to star
#[arg(long)]
pull_request_id: Option<u64>,
/// ID of the pull request comment to star
#[arg(long)]
pull_request_comment_id: Option<u64>,
},
/// Remove a star
Delete {
/// Star ID
id: u64,
},
}

#[derive(Subcommand)]
enum AuthCommands {
/// Login with your API key
Expand Down Expand Up @@ -2873,6 +2906,22 @@ fn run() -> Result<()> {
}
},
Commands::RateLimit { json } => cmd::rate_limit::show(&RateLimitArgs::new(json)),
Commands::Star { action } => match action {
StarCommands::Add {
issue_id,
comment_id,
wiki_id,
pull_request_id,
pull_request_comment_id,
} => cmd::star::add(&StarAddArgs::try_new(
issue_id,
comment_id,
wiki_id,
pull_request_id,
pull_request_comment_id,
)?),
StarCommands::Delete { id } => cmd::star::delete(&StarDeleteArgs::new(id)),
},
Commands::Space { action, json } => match action {
None => cmd::space::show(&SpaceShowArgs::new(json)),
Some(SpaceCommands::Activities {
Expand Down
Loading