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
31 changes: 31 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ pub trait BacklogApi {
fn get_project_categories(&self, _key: &str) -> Result<Vec<ProjectCategory>> {
unimplemented!()
}
fn add_project_category(&self, _key: &str, _name: &str) -> Result<ProjectCategory> {
unimplemented!()
}
fn update_project_category(
&self,
_key: &str,
_category_id: u64,
_name: &str,
) -> Result<ProjectCategory> {
unimplemented!()
}
fn delete_project_category(&self, _key: &str, _category_id: u64) -> Result<ProjectCategory> {
unimplemented!()
}
fn get_project_versions(&self, _key: &str) -> Result<Vec<ProjectVersion>> {
unimplemented!()
}
Expand Down Expand Up @@ -513,6 +527,23 @@ impl BacklogApi for BacklogClient {
self.get_project_categories(key)
}

fn add_project_category(&self, key: &str, name: &str) -> Result<ProjectCategory> {
self.add_project_category(key, name)
}

fn update_project_category(
&self,
key: &str,
category_id: u64,
name: &str,
) -> Result<ProjectCategory> {
self.update_project_category(key, category_id, name)
}

fn delete_project_category(&self, key: &str, category_id: u64) -> Result<ProjectCategory> {
self.delete_project_category(key, category_id)
}

fn get_project_versions(&self, key: &str) -> Result<Vec<ProjectVersion>> {
self.get_project_versions(key)
}
Expand Down
45 changes: 45 additions & 0 deletions src/api/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub struct ProjectIssueType {
#[serde(rename_all = "camelCase")]
pub struct ProjectCategory {
pub id: u64,
pub project_id: u64,
pub name: String,
pub display_order: u32,
}
Expand Down Expand Up @@ -246,6 +247,49 @@ impl BacklogClient {
})
}

pub fn add_project_category(&self, key: &str, name: &str) -> Result<ProjectCategory> {
let params = vec![("name".to_string(), name.to_string())];
let value = self.post_form(&format!("/projects/{}/categories", key), &params)?;
serde_json::from_value(value.clone()).map_err(|e| {
anyhow::anyhow!(
"Failed to deserialize add project category response: {}\nRaw JSON:\n{}",
e,
serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string())
)
})
}

pub fn update_project_category(
&self,
key: &str,
category_id: u64,
name: &str,
) -> Result<ProjectCategory> {
let params = vec![("name".to_string(), name.to_string())];
let value = self.patch_form(
&format!("/projects/{}/categories/{}", key, category_id),
&params,
)?;
serde_json::from_value(value.clone()).map_err(|e| {
anyhow::anyhow!(
"Failed to deserialize update project category response: {}\nRaw JSON:\n{}",
e,
serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string())
)
})
}

pub fn delete_project_category(&self, key: &str, category_id: u64) -> Result<ProjectCategory> {
let value = self.delete_req(&format!("/projects/{}/categories/{}", key, category_id))?;
serde_json::from_value(value.clone()).map_err(|e| {
anyhow::anyhow!(
"Failed to deserialize delete project category response: {}\nRaw JSON:\n{}",
e,
serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string())
)
})
}

pub fn get_project_versions(&self, key: &str) -> Result<Vec<ProjectVersion>> {
let value = self.get(&format!("/projects/{}/versions", key))?;
serde_json::from_value(value.clone()).map_err(|e| {
Expand Down Expand Up @@ -672,6 +716,7 @@ mod tests {
when.method(GET).path("/projects/TEST/categories");
then.status(200).json_body(json!([{
"id": 11,
"projectId": 1,
"name": "Development",
"displayOrder": 0
}]));
Expand Down
213 changes: 201 additions & 12 deletions src/cmd/project/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,52 @@ impl ProjectCategoryListArgs {
}
}

pub struct ProjectCategoryAddArgs {
key: String,
name: String,
json: bool,
}

impl ProjectCategoryAddArgs {
pub fn new(key: String, name: String, json: bool) -> Self {
Self { key, name, json }
}
}

pub struct ProjectCategoryUpdateArgs {
key: String,
category_id: u64,
name: String,
json: bool,
}

impl ProjectCategoryUpdateArgs {
pub fn new(key: String, category_id: u64, name: String, json: bool) -> Self {
Self {
key,
category_id,
name,
json,
}
}
}

pub struct ProjectCategoryDeleteArgs {
key: String,
category_id: u64,
json: bool,
}

impl ProjectCategoryDeleteArgs {
pub fn new(key: String, category_id: u64, json: bool) -> Self {
Self {
key,
category_id,
json,
}
}
}

pub fn list(args: &ProjectCategoryListArgs) -> Result<()> {
let client = BacklogClient::from_config()?;
list_with(args, &client)
Expand All @@ -34,6 +80,60 @@ pub fn list_with(args: &ProjectCategoryListArgs, api: &dyn BacklogApi) -> Result
Ok(())
}

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

pub fn add_with(args: &ProjectCategoryAddArgs, api: &dyn BacklogApi) -> Result<()> {
let category = api.add_project_category(&args.key, &args.name)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&category).context("Failed to serialize JSON")?
);
} else {
println!("Added: {}", format_category_row(&category));
}
Ok(())
}

pub fn update(args: &ProjectCategoryUpdateArgs) -> Result<()> {
let client = BacklogClient::from_config()?;
update_with(args, &client)
}

pub fn update_with(args: &ProjectCategoryUpdateArgs, api: &dyn BacklogApi) -> Result<()> {
let category = api.update_project_category(&args.key, args.category_id, &args.name)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&category).context("Failed to serialize JSON")?
);
} else {
println!("Updated: {}", format_category_row(&category));
}
Ok(())
}

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

pub fn delete_with(args: &ProjectCategoryDeleteArgs, api: &dyn BacklogApi) -> Result<()> {
let category = api.delete_project_category(&args.key, args.category_id)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&category).context("Failed to serialize JSON")?
);
} else {
println!("Deleted: {}", format_category_row(&category));
}
Ok(())
}

fn format_category_row(c: &ProjectCategory) -> String {
format!("[{}] {}", c.id, c.name)
}
Expand All @@ -44,20 +144,45 @@ mod tests {
use anyhow::anyhow;

struct MockApi {
categories: Option<Vec<ProjectCategory>>,
list: Option<Vec<ProjectCategory>>,
single: Option<ProjectCategory>,
}

fn mock(list: Option<Vec<ProjectCategory>>, single: Option<ProjectCategory>) -> MockApi {
MockApi { list, single }
}

impl crate::api::BacklogApi for MockApi {
fn get_project_categories(&self, _key: &str) -> anyhow::Result<Vec<ProjectCategory>> {
self.categories
.clone()
.ok_or_else(|| anyhow!("no categories"))
self.list.clone().ok_or_else(|| anyhow!("list failed"))
}

fn add_project_category(&self, _key: &str, _name: &str) -> anyhow::Result<ProjectCategory> {
self.single.clone().ok_or_else(|| anyhow!("add failed"))
}

fn update_project_category(
&self,
_key: &str,
_category_id: u64,
_name: &str,
) -> anyhow::Result<ProjectCategory> {
self.single.clone().ok_or_else(|| anyhow!("update failed"))
}

fn delete_project_category(
&self,
_key: &str,
_category_id: u64,
) -> anyhow::Result<ProjectCategory> {
self.single.clone().ok_or_else(|| anyhow!("delete failed"))
}
}

fn sample_category() -> ProjectCategory {
ProjectCategory {
id: 11,
project_id: 1,
name: "Development".to_string(),
display_order: 0,
}
Expand All @@ -72,9 +197,7 @@ mod tests {

#[test]
fn list_with_text_output_succeeds() {
let api = MockApi {
categories: Some(vec![sample_category()]),
};
let api = mock(Some(vec![sample_category()]), None);
assert!(
list_with(
&ProjectCategoryListArgs::new("TEST".to_string(), false),
Expand All @@ -86,9 +209,7 @@ mod tests {

#[test]
fn list_with_json_output_succeeds() {
let api = MockApi {
categories: Some(vec![sample_category()]),
};
let api = mock(Some(vec![sample_category()]), None);
assert!(
list_with(
&ProjectCategoryListArgs::new("TEST".to_string(), true),
Expand All @@ -100,12 +221,80 @@ mod tests {

#[test]
fn list_with_propagates_api_error() {
let api = MockApi { categories: None };
let api = mock(None, None);
let err = list_with(
&ProjectCategoryListArgs::new("TEST".to_string(), false),
&api,
)
.unwrap_err();
assert!(err.to_string().contains("no categories"));
assert!(err.to_string().contains("list failed"));
}

#[test]
fn add_with_text_output_succeeds() {
let api = mock(None, Some(sample_category()));
let args = ProjectCategoryAddArgs::new("TEST".to_string(), "Dev".to_string(), false);
assert!(add_with(&args, &api).is_ok());
}

#[test]
fn add_with_json_output_succeeds() {
let api = mock(None, Some(sample_category()));
let args = ProjectCategoryAddArgs::new("TEST".to_string(), "Dev".to_string(), true);
assert!(add_with(&args, &api).is_ok());
}

#[test]
fn add_with_propagates_api_error() {
let api = mock(None, None);
let args = ProjectCategoryAddArgs::new("TEST".to_string(), "Dev".to_string(), false);
let err = add_with(&args, &api).unwrap_err();
assert!(err.to_string().contains("add failed"));
}

#[test]
fn update_with_text_output_succeeds() {
let api = mock(None, Some(sample_category()));
let args =
ProjectCategoryUpdateArgs::new("TEST".to_string(), 11, "Dev2".to_string(), false);
assert!(update_with(&args, &api).is_ok());
}

#[test]
fn update_with_json_output_succeeds() {
let api = mock(None, Some(sample_category()));
let args = ProjectCategoryUpdateArgs::new("TEST".to_string(), 11, "Dev2".to_string(), true);
assert!(update_with(&args, &api).is_ok());
}

#[test]
fn update_with_propagates_api_error() {
let api = mock(None, None);
let args =
ProjectCategoryUpdateArgs::new("TEST".to_string(), 11, "Dev2".to_string(), false);
let err = update_with(&args, &api).unwrap_err();
assert!(err.to_string().contains("update failed"));
}

#[test]
fn delete_with_text_output_succeeds() {
let api = mock(None, Some(sample_category()));
let args = ProjectCategoryDeleteArgs::new("TEST".to_string(), 11, false);
assert!(delete_with(&args, &api).is_ok());
}

#[test]
fn delete_with_json_output_succeeds() {
let api = mock(None, Some(sample_category()));
let args = ProjectCategoryDeleteArgs::new("TEST".to_string(), 11, true);
assert!(delete_with(&args, &api).is_ok());
}

#[test]
fn delete_with_propagates_api_error() {
let api = mock(None, None);
let args = ProjectCategoryDeleteArgs::new("TEST".to_string(), 11, false);
let err = delete_with(&args, &api).unwrap_err();
assert!(err.to_string().contains("delete failed"));
}
}
Loading