Skip to main content

claude_api/admin/
workspace_members.rs

1//! Workspace members: create / retrieve / list / update / delete.
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Result;
7use crate::pagination::Paginated;
8
9use super::{ListParams, WorkspaceRole, WriteWorkspaceRole};
10
11/// One workspace-member assignment.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct WorkspaceMember {
15    /// Wire type tag (`"workspace_member"`).
16    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
17    pub ty: Option<String>,
18    /// User ID.
19    pub user_id: String,
20    /// Workspace ID.
21    pub workspace_id: String,
22    /// Member's role in this workspace.
23    pub workspace_role: WorkspaceRole,
24}
25
26/// Response shape for `DELETE
27/// /v1/organizations/workspaces/{ws}/members/{user}`.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[non_exhaustive]
30pub struct WorkspaceMemberDeleted {
31    /// User ID.
32    pub user_id: String,
33    /// Workspace ID.
34    pub workspace_id: String,
35    /// Wire type tag (`"workspace_member_deleted"`).
36    #[serde(rename = "type")]
37    pub ty: String,
38}
39
40/// Request body for `POST /v1/organizations/workspaces/{ws}/members`.
41#[derive(Debug, Clone, Serialize)]
42#[non_exhaustive]
43pub struct CreateWorkspaceMemberRequest {
44    /// User to add.
45    pub user_id: String,
46    /// Role to assign. `workspace_billing` is not a valid value.
47    pub workspace_role: WriteWorkspaceRole,
48}
49
50impl CreateWorkspaceMemberRequest {
51    /// Build a request.
52    #[must_use]
53    pub fn new(user_id: impl Into<String>, role: WriteWorkspaceRole) -> Self {
54        Self {
55            user_id: user_id.into(),
56            workspace_role: role,
57        }
58    }
59}
60
61/// Request body for `POST /v1/organizations/workspaces/{ws}/members/{user}`
62/// (update role).
63#[derive(Debug, Clone, Serialize)]
64#[non_exhaustive]
65pub struct UpdateWorkspaceMemberRequest {
66    /// New role. Per the API the body type allows `workspace_billing`
67    /// here; use [`WorkspaceRole`] directly so callers can pass it.
68    pub workspace_role: WorkspaceRole,
69}
70
71impl UpdateWorkspaceMemberRequest {
72    /// Build with a new role.
73    #[must_use]
74    pub fn new(role: WorkspaceRole) -> Self {
75        Self {
76            workspace_role: role,
77        }
78    }
79}
80
81/// Namespace handle for workspace-member endpoints. Scoped to a single
82/// workspace at construction time.
83pub struct WorkspaceMembers<'a> {
84    client: &'a Client,
85    workspace_id: String,
86}
87
88impl<'a> WorkspaceMembers<'a> {
89    pub(crate) fn new(client: &'a Client, workspace_id: String) -> Self {
90        Self {
91            client,
92            workspace_id,
93        }
94    }
95
96    /// `POST /v1/organizations/workspaces/{ws}/members`.
97    pub async fn create(&self, request: CreateWorkspaceMemberRequest) -> Result<WorkspaceMember> {
98        let path = format!("/v1/organizations/workspaces/{}/members", self.workspace_id);
99        let body = &request;
100        self.client
101            .execute_with_retry(
102                || {
103                    self.client
104                        .request_builder(reqwest::Method::POST, &path)
105                        .json(body)
106                },
107                &[],
108            )
109            .await
110    }
111
112    /// `GET /v1/organizations/workspaces/{ws}/members/{user}`.
113    pub async fn retrieve(&self, user_id: &str) -> Result<WorkspaceMember> {
114        let path = format!(
115            "/v1/organizations/workspaces/{}/members/{user_id}",
116            self.workspace_id
117        );
118        self.client
119            .execute_with_retry(
120                || self.client.request_builder(reqwest::Method::GET, &path),
121                &[],
122            )
123            .await
124    }
125
126    /// `GET /v1/organizations/workspaces/{ws}/members`.
127    pub async fn list(&self, params: ListParams) -> Result<Paginated<WorkspaceMember>> {
128        let path = format!("/v1/organizations/workspaces/{}/members", self.workspace_id);
129        let query = params.to_query();
130        self.client
131            .execute_with_retry(
132                || {
133                    let mut req = self.client.request_builder(reqwest::Method::GET, &path);
134                    for (k, v) in &query {
135                        req = req.query(&[(k, v)]);
136                    }
137                    req
138                },
139                &[],
140            )
141            .await
142    }
143
144    /// `POST /v1/organizations/workspaces/{ws}/members/{user}` (update).
145    pub async fn update(
146        &self,
147        user_id: &str,
148        request: UpdateWorkspaceMemberRequest,
149    ) -> Result<WorkspaceMember> {
150        let path = format!(
151            "/v1/organizations/workspaces/{}/members/{user_id}",
152            self.workspace_id
153        );
154        let body = &request;
155        self.client
156            .execute_with_retry(
157                || {
158                    self.client
159                        .request_builder(reqwest::Method::POST, &path)
160                        .json(body)
161                },
162                &[],
163            )
164            .await
165    }
166
167    /// `DELETE /v1/organizations/workspaces/{ws}/members/{user}`.
168    pub async fn delete(&self, user_id: &str) -> Result<WorkspaceMemberDeleted> {
169        let path = format!(
170            "/v1/organizations/workspaces/{}/members/{user_id}",
171            self.workspace_id
172        );
173        self.client
174            .execute_with_retry(
175                || self.client.request_builder(reqwest::Method::DELETE, &path),
176                &[],
177            )
178            .await
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use serde_json::json;
186    use wiremock::matchers::{body_partial_json, method, path};
187    use wiremock::{Mock, MockServer, ResponseTemplate};
188
189    fn client_for(mock: &MockServer) -> Client {
190        Client::builder()
191            .api_key("sk-ant-admin-test")
192            .base_url(mock.uri())
193            .build()
194            .unwrap()
195    }
196
197    fn fake_member() -> serde_json::Value {
198        json!({
199            "type": "workspace_member",
200            "user_id": "user_01",
201            "workspace_id": "ws_01",
202            "workspace_role": "workspace_user"
203        })
204    }
205
206    #[tokio::test]
207    async fn create_workspace_member_posts_user_id_and_role() {
208        let mock = MockServer::start().await;
209        Mock::given(method("POST"))
210            .and(path("/v1/organizations/workspaces/ws_01/members"))
211            .and(body_partial_json(json!({
212                "user_id": "user_01",
213                "workspace_role": "workspace_admin"
214            })))
215            .respond_with(ResponseTemplate::new(200).set_body_json(fake_member()))
216            .mount(&mock)
217            .await;
218        let client = client_for(&mock);
219        client
220            .admin()
221            .workspace_members("ws_01")
222            .create(CreateWorkspaceMemberRequest::new(
223                "user_01",
224                WriteWorkspaceRole::WorkspaceAdmin,
225            ))
226            .await
227            .unwrap();
228    }
229
230    #[tokio::test]
231    async fn retrieve_workspace_member_returns_role() {
232        let mock = MockServer::start().await;
233        Mock::given(method("GET"))
234            .and(path("/v1/organizations/workspaces/ws_01/members/user_01"))
235            .respond_with(ResponseTemplate::new(200).set_body_json(fake_member()))
236            .mount(&mock)
237            .await;
238        let client = client_for(&mock);
239        let m = client
240            .admin()
241            .workspace_members("ws_01")
242            .retrieve("user_01")
243            .await
244            .unwrap();
245        assert!(matches!(m.workspace_role, WorkspaceRole::User));
246    }
247
248    #[tokio::test]
249    async fn list_workspace_members_paginates() {
250        let mock = MockServer::start().await;
251        Mock::given(method("GET"))
252            .and(path("/v1/organizations/workspaces/ws_01/members"))
253            .and(wiremock::matchers::query_param("limit", "5"))
254            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
255                "data": [fake_member()],
256                "has_more": false,
257                "first_id": "user_01",
258                "last_id": "user_01"
259            })))
260            .mount(&mock)
261            .await;
262        let client = client_for(&mock);
263        let page = client
264            .admin()
265            .workspace_members("ws_01")
266            .list(ListParams {
267                limit: Some(5),
268                ..Default::default()
269            })
270            .await
271            .unwrap();
272        assert_eq!(page.data.len(), 1);
273    }
274
275    #[tokio::test]
276    async fn update_workspace_member_changes_role() {
277        let mock = MockServer::start().await;
278        Mock::given(method("POST"))
279            .and(path("/v1/organizations/workspaces/ws_01/members/user_01"))
280            .and(body_partial_json(json!({
281                "workspace_role": "workspace_developer"
282            })))
283            .respond_with(ResponseTemplate::new(200).set_body_json(fake_member()))
284            .mount(&mock)
285            .await;
286        let client = client_for(&mock);
287        client
288            .admin()
289            .workspace_members("ws_01")
290            .update(
291                "user_01",
292                UpdateWorkspaceMemberRequest::new(WorkspaceRole::Developer),
293            )
294            .await
295            .unwrap();
296    }
297
298    #[tokio::test]
299    async fn delete_workspace_member_returns_deleted_marker() {
300        let mock = MockServer::start().await;
301        Mock::given(method("DELETE"))
302            .and(path("/v1/organizations/workspaces/ws_01/members/user_01"))
303            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
304                "type": "workspace_member_deleted",
305                "user_id": "user_01",
306                "workspace_id": "ws_01"
307            })))
308            .mount(&mock)
309            .await;
310        let client = client_for(&mock);
311        let r = client
312            .admin()
313            .workspace_members("ws_01")
314            .delete("user_01")
315            .await
316            .unwrap();
317        assert_eq!(r.ty, "workspace_member_deleted");
318    }
319}