Skip to content

Commit 4d7c36f

Browse files
authored
Merge pull request #29 from 23prime/feature/user-commands
feat: add bl user list and bl user show commands
2 parents bab6c53 + b527edb commit 4d7c36f

42 files changed

Lines changed: 1018 additions & 25 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/developing/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ git switch -c feature/<name>
3030

3131
## Step 3 — Pick a feature (feature tasks only)
3232

33-
Read `docs/user-guide.md` and pick the first "Planned" entry from the command coverage table.
33+
Read `website/docs/commands.md` and pick the first "Planned" entry from the command coverage table.
3434
Confirm the selection with the user before proceeding.
3535

3636
For feature tasks, also read `references/patterns.md` for code patterns and known gotchas, and check the official API docs before writing structs:
@@ -47,7 +47,7 @@ Follow `AGENTS.md` conventions. For feature tasks, the typical file order is:
4747
3. `src/cmd/<resource>/<subcommand>.rs``<cmd>()` + `<cmd>_with()` + tests
4848
4. `src/cmd/<resource>/mod.rs` — re-export
4949
5. `src/main.rs` — clap wiring
50-
6. `docs/user-guide.md` — mark as implemented
50+
6. `website/docs/commands.md` and `website/i18n/ja/docusaurus-plugin-content-docs/current/commands.md`add command docs and mark as implemented in the coverage table
5151

5252
## Step 5 — Auto-fix and check
5353

src/api/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ use wiki::{Wiki, WikiAttachment, WikiHistory, WikiListItem};
3030
pub trait BacklogApi {
3131
fn get_space(&self) -> Result<Space>;
3232
fn get_myself(&self) -> Result<User>;
33+
fn get_users(&self) -> Result<Vec<User>>;
34+
fn get_user(&self, user_id: u64) -> Result<User>;
3335
fn get_space_activities(&self) -> Result<Vec<Activity>>;
3436
fn get_space_disk_usage(&self) -> Result<DiskUsage>;
3537
fn get_space_notification(&self) -> Result<SpaceNotification>;
@@ -76,6 +78,14 @@ impl BacklogApi for BacklogClient {
7678
self.get_myself()
7779
}
7880

81+
fn get_users(&self) -> Result<Vec<User>> {
82+
self.get_users()
83+
}
84+
85+
fn get_user(&self, user_id: u64) -> Result<User> {
86+
self.get_user(user_id)
87+
}
88+
7989
fn get_space_activities(&self) -> Result<Vec<Activity>> {
8090
self.get_space_activities()
8191
}

src/api/user.rs

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
use anyhow::Result;
22
use serde::{Deserialize, Serialize};
3+
use std::collections::BTreeMap;
34

45
use super::BacklogClient;
56

67
#[derive(Debug, Clone, Serialize, Deserialize)]
78
#[serde(rename_all = "camelCase")]
89
pub struct User {
910
pub id: u64,
10-
pub user_id: String,
11+
/// `null` for bot accounts (e.g. automation bots have no userId in Backlog API).
12+
pub user_id: Option<String>,
1113
pub name: String,
12-
pub mail_address: String,
14+
/// `null` for bot accounts.
15+
pub mail_address: Option<String>,
1316
pub role_type: u8,
17+
#[serde(default)]
18+
pub lang: Option<String>,
19+
#[serde(default)]
20+
pub last_login_time: Option<String>,
21+
#[serde(flatten)]
22+
pub extra: BTreeMap<String, serde_json::Value>,
1423
}
1524

1625
impl BacklogClient {
@@ -19,6 +28,18 @@ impl BacklogClient {
1928
serde_json::from_value(value)
2029
.map_err(|e| anyhow::anyhow!("Failed to deserialize user response: {}", e))
2130
}
31+
32+
pub fn get_users(&self) -> Result<Vec<User>> {
33+
let value = self.get("/users")?;
34+
serde_json::from_value(value)
35+
.map_err(|e| anyhow::anyhow!("Failed to deserialize users response: {}", e))
36+
}
37+
38+
pub fn get_user(&self, user_id: u64) -> Result<User> {
39+
let value = self.get(&format!("/users/{user_id}"))?;
40+
serde_json::from_value(value)
41+
.map_err(|e| anyhow::anyhow!("Failed to deserialize user response: {}", e))
42+
}
2243
}
2344

2445
#[cfg(test)]
@@ -33,7 +54,9 @@ mod tests {
3354
"userId": "john",
3455
"name": "John Doe",
3556
"mailAddress": "john@example.com",
36-
"roleType": 1
57+
"roleType": 1,
58+
"lang": "ja",
59+
"lastLoginTime": "2024-01-01T00:00:00Z"
3760
})
3861
}
3962

@@ -48,7 +71,7 @@ mod tests {
4871
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
4972
let user = client.get_myself().unwrap();
5073
assert_eq!(user.id, 123);
51-
assert_eq!(user.user_id, "john");
74+
assert_eq!(user.user_id.as_deref(), Some("john"));
5275
assert_eq!(user.name, "John Doe");
5376
}
5477

@@ -67,25 +90,83 @@ mod tests {
6790
}
6891

6992
#[test]
70-
fn deserialize_user() {
71-
let v = json!({
72-
"id": 123,
73-
"userId": "john",
74-
"name": "John Doe",
75-
"mailAddress": "john@example.com",
76-
"roleType": 1
93+
fn get_users_returns_list() {
94+
let server = MockServer::start();
95+
server.mock(|when, then| {
96+
when.method(GET).path("/users");
97+
then.status(200).json_body(json!([user_json()]));
98+
});
99+
100+
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
101+
let users = client.get_users().unwrap();
102+
assert_eq!(users.len(), 1);
103+
assert_eq!(users[0].id, 123);
104+
}
105+
106+
#[test]
107+
fn get_users_returns_error_on_api_failure() {
108+
let server = MockServer::start();
109+
server.mock(|when, then| {
110+
when.method(GET).path("/users");
111+
then.status(403)
112+
.json_body(json!({"errors": [{"message": "Forbidden"}]}));
113+
});
114+
115+
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
116+
let err = client.get_users().unwrap_err();
117+
assert!(err.to_string().contains("Forbidden"));
118+
}
119+
120+
#[test]
121+
fn get_user_returns_parsed_struct() {
122+
let server = MockServer::start();
123+
server.mock(|when, then| {
124+
when.method(GET).path("/users/123");
125+
then.status(200).json_body(user_json());
77126
});
127+
128+
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
129+
let user = client.get_user(123).unwrap();
130+
assert_eq!(user.id, 123);
131+
assert_eq!(user.name, "John Doe");
132+
}
133+
134+
#[test]
135+
fn get_user_returns_error_on_not_found() {
136+
let server = MockServer::start();
137+
server.mock(|when, then| {
138+
when.method(GET).path("/users/999");
139+
then.status(404)
140+
.json_body(json!({"errors": [{"message": "No user"}]}));
141+
});
142+
143+
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
144+
let err = client.get_user(999).unwrap_err();
145+
assert!(err.to_string().contains("No user"));
146+
}
147+
148+
#[test]
149+
fn deserialize_user() {
150+
let v = user_json();
78151
let user: User = serde_json::from_value(v).unwrap();
79152
assert_eq!(user.id, 123);
80-
assert_eq!(user.user_id, "john");
153+
assert_eq!(user.user_id.as_deref(), Some("john"));
81154
assert_eq!(user.name, "John Doe");
82-
assert_eq!(user.mail_address, "john@example.com");
155+
assert_eq!(user.mail_address.as_deref(), Some("john@example.com"));
83156
assert_eq!(user.role_type, 1);
84157
}
85158

86159
#[test]
87-
fn deserialize_user_fails_on_missing_required_field() {
88-
let v = json!({"id": 123, "userId": "john"});
89-
assert!(serde_json::from_value::<User>(v).is_err());
160+
fn deserialize_user_with_null_user_id() {
161+
let v = json!({
162+
"id": 1,
163+
"userId": null,
164+
"name": "Bot",
165+
"mailAddress": null,
166+
"roleType": 2
167+
});
168+
let user: User = serde_json::from_value(v).unwrap();
169+
assert_eq!(user.user_id, None);
170+
assert_eq!(user.mail_address, None);
90171
}
91172
}

src/cmd/auth.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,11 @@ pub fn status_with(
276276
}
277277

278278
match api.get_myself() {
279-
Ok(user) => println!(" - Logged in as {} ({})", user.name.green(), user.user_id),
279+
Ok(user) => println!(
280+
" - Logged in as {} ({})",
281+
user.name.green(),
282+
user.user_id.as_deref().unwrap_or("-")
283+
),
280284
Err(e) => println!(" {} Token invalid: {}", "!".red(), e),
281285
}
282286

@@ -376,6 +380,14 @@ mod tests {
376380
.ok_or_else(|| anyhow!("invalid credentials"))
377381
}
378382

383+
fn get_users(&self) -> anyhow::Result<Vec<User>> {
384+
unimplemented!()
385+
}
386+
387+
fn get_user(&self, _user_id: u64) -> anyhow::Result<User> {
388+
unimplemented!()
389+
}
390+
379391
fn get_space_activities(&self) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
380392
unimplemented!()
381393
}
@@ -550,10 +562,13 @@ mod tests {
550562
fn sample_user() -> User {
551563
User {
552564
id: 1,
553-
user_id: "john".to_string(),
565+
user_id: Some("john".to_string()),
554566
name: "John Doe".to_string(),
555-
mail_address: "john@example.com".to_string(),
567+
mail_address: Some("john@example.com".to_string()),
556568
role_type: 1,
569+
lang: None,
570+
last_login_time: None,
571+
extra: std::collections::BTreeMap::new(),
557572
}
558573
}
559574

src/cmd/issue/attachment/list.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ mod tests {
8080
fn get_myself(&self) -> anyhow::Result<crate::api::user::User> {
8181
unimplemented!()
8282
}
83+
fn get_users(&self) -> anyhow::Result<Vec<crate::api::user::User>> {
84+
unimplemented!()
85+
}
86+
fn get_user(&self, _user_id: u64) -> anyhow::Result<crate::api::user::User> {
87+
unimplemented!()
88+
}
8389
fn get_space_activities(&self) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
8490
unimplemented!()
8591
}

src/cmd/issue/comment/add.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ mod tests {
5353
fn get_myself(&self) -> anyhow::Result<crate::api::user::User> {
5454
unimplemented!()
5555
}
56+
fn get_users(&self) -> anyhow::Result<Vec<crate::api::user::User>> {
57+
unimplemented!()
58+
}
59+
fn get_user(&self, _user_id: u64) -> anyhow::Result<crate::api::user::User> {
60+
unimplemented!()
61+
}
5662
fn get_space_activities(&self) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
5763
unimplemented!()
5864
}

src/cmd/issue/comment/delete.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ mod tests {
5555
fn get_myself(&self) -> anyhow::Result<crate::api::user::User> {
5656
unimplemented!()
5757
}
58+
fn get_users(&self) -> anyhow::Result<Vec<crate::api::user::User>> {
59+
unimplemented!()
60+
}
61+
fn get_user(&self, _user_id: u64) -> anyhow::Result<crate::api::user::User> {
62+
unimplemented!()
63+
}
5864
fn get_space_activities(&self) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
5965
unimplemented!()
6066
}

src/cmd/issue/comment/list.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ mod tests {
9494
fn get_myself(&self) -> anyhow::Result<crate::api::user::User> {
9595
unimplemented!()
9696
}
97+
fn get_users(&self) -> anyhow::Result<Vec<crate::api::user::User>> {
98+
unimplemented!()
99+
}
100+
fn get_user(&self, _user_id: u64) -> anyhow::Result<crate::api::user::User> {
101+
unimplemented!()
102+
}
97103
fn get_space_activities(&self) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
98104
unimplemented!()
99105
}

src/cmd/issue/comment/update.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ mod tests {
5959
fn get_myself(&self) -> anyhow::Result<crate::api::user::User> {
6060
unimplemented!()
6161
}
62+
fn get_users(&self) -> anyhow::Result<Vec<crate::api::user::User>> {
63+
unimplemented!()
64+
}
65+
fn get_user(&self, _user_id: u64) -> anyhow::Result<crate::api::user::User> {
66+
unimplemented!()
67+
}
6268
fn get_space_activities(&self) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
6369
unimplemented!()
6470
}

src/cmd/issue/count.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ mod tests {
108108
fn get_myself(&self) -> anyhow::Result<crate::api::user::User> {
109109
unimplemented!()
110110
}
111+
fn get_users(&self) -> anyhow::Result<Vec<crate::api::user::User>> {
112+
unimplemented!()
113+
}
114+
fn get_user(&self, _user_id: u64) -> anyhow::Result<crate::api::user::User> {
115+
unimplemented!()
116+
}
111117
fn get_space_activities(&self) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
112118
unimplemented!()
113119
}

0 commit comments

Comments
 (0)