use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::error::Result;
use crate::pagination::Paginated;
use super::ListParams;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum AllowedInferenceGeos {
Unrestricted(UnrestrictedSentinel),
List(Vec<String>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum UnrestrictedSentinel {
#[serde(rename = "unrestricted")]
Unrestricted,
}
impl AllowedInferenceGeos {
#[must_use]
pub fn unrestricted() -> Self {
Self::Unrestricted(UnrestrictedSentinel::Unrestricted)
}
#[must_use]
pub fn list<I, S>(geos: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self::List(geos.into_iter().map(Into::into).collect())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DataResidency {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_inference_geos: Option<AllowedInferenceGeos>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_inference_geo: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_geo: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Workspace {
pub id: String,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub ty: Option<String>,
pub name: String,
pub display_color: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data_residency: Option<DataResidency>,
pub created_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived_at: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CreateWorkspaceRequest {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_residency: Option<DataResidency>,
}
impl CreateWorkspaceRequest {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
data_residency: None,
}
}
#[must_use]
pub fn with_data_residency(mut self, residency: DataResidency) -> Self {
self.data_residency = Some(residency);
self
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct UpdateWorkspaceRequest {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_residency: Option<DataResidency>,
}
impl UpdateWorkspaceRequest {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
data_residency: None,
}
}
#[must_use]
pub fn with_data_residency(mut self, residency: DataResidency) -> Self {
self.data_residency = Some(residency);
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ListWorkspacesParams {
pub paging: ListParams,
pub include_archived: Option<bool>,
}
impl ListWorkspacesParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = self.paging.to_query();
if let Some(b) = self.include_archived {
q.push(("include_archived", b.to_string()));
}
q
}
}
pub struct Workspaces<'a> {
client: &'a Client,
}
impl<'a> Workspaces<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn create(&self, request: CreateWorkspaceRequest) -> Result<Workspace> {
let body = &request;
self.client
.execute_with_retry(
|| {
self.client
.request_builder(reqwest::Method::POST, "/v1/organizations/workspaces")
.json(body)
},
&[],
)
.await
}
pub async fn retrieve(&self, workspace_id: &str) -> Result<Workspace> {
let path = format!("/v1/organizations/workspaces/{workspace_id}");
self.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::GET, &path),
&[],
)
.await
}
pub async fn list(&self, params: ListWorkspacesParams) -> Result<Paginated<Workspace>> {
let query = params.to_query();
self.client
.execute_with_retry(
|| {
let mut req = self
.client
.request_builder(reqwest::Method::GET, "/v1/organizations/workspaces");
for (k, v) in &query {
req = req.query(&[(k, v)]);
}
req
},
&[],
)
.await
}
pub async fn update(
&self,
workspace_id: &str,
request: UpdateWorkspaceRequest,
) -> Result<Workspace> {
let path = format!("/v1/organizations/workspaces/{workspace_id}");
let body = &request;
self.client
.execute_with_retry(
|| {
self.client
.request_builder(reqwest::Method::POST, &path)
.json(body)
},
&[],
)
.await
}
pub async fn archive(&self, workspace_id: &str) -> Result<Workspace> {
let path = format!("/v1/organizations/workspaces/{workspace_id}/archive");
self.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::POST, &path),
&[],
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use wiremock::matchers::{body_partial_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn client_for(mock: &MockServer) -> Client {
Client::builder()
.api_key("sk-ant-admin-test")
.base_url(mock.uri())
.build()
.unwrap()
}
fn fake_workspace() -> serde_json::Value {
json!({
"id": "ws_01",
"type": "workspace",
"name": "Default",
"display_color": "#0a84ff",
"created_at": "2026-05-01T00:00:00Z"
})
}
#[test]
fn allowed_inference_geos_serializes_unrestricted_as_string() {
let v = serde_json::to_value(AllowedInferenceGeos::unrestricted()).unwrap();
assert_eq!(v, json!("unrestricted"));
}
#[test]
fn allowed_inference_geos_serializes_list_form() {
let v = serde_json::to_value(AllowedInferenceGeos::list(["us", "eu"])).unwrap();
assert_eq!(v, json!(["us", "eu"]));
}
#[test]
fn allowed_inference_geos_round_trips_both_forms() {
let s: AllowedInferenceGeos = serde_json::from_value(json!("unrestricted")).unwrap();
assert_eq!(s, AllowedInferenceGeos::unrestricted());
let l: AllowedInferenceGeos = serde_json::from_value(json!(["us"])).unwrap();
assert_eq!(l, AllowedInferenceGeos::list(["us"]));
}
#[tokio::test]
async fn create_workspace_minimal_body() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/organizations/workspaces"))
.and(body_partial_json(json!({"name": "Default"})))
.respond_with(ResponseTemplate::new(200).set_body_json(fake_workspace()))
.mount(&mock)
.await;
let client = client_for(&mock);
let w = client
.admin()
.workspaces()
.create(CreateWorkspaceRequest::new("Default"))
.await
.unwrap();
assert_eq!(w.id, "ws_01");
}
#[tokio::test]
async fn list_workspaces_passes_include_archived() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/organizations/workspaces"))
.and(wiremock::matchers::query_param("include_archived", "true"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [fake_workspace()],
"has_more": false,
"first_id": "ws_01",
"last_id": "ws_01"
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let page = client
.admin()
.workspaces()
.list(ListWorkspacesParams {
include_archived: Some(true),
..Default::default()
})
.await
.unwrap();
assert_eq!(page.data.len(), 1);
}
#[tokio::test]
async fn update_workspace_round_trips() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/organizations/workspaces/ws_01"))
.and(body_partial_json(json!({"name": "Renamed"})))
.respond_with(ResponseTemplate::new(200).set_body_json(fake_workspace()))
.mount(&mock)
.await;
let client = client_for(&mock);
client
.admin()
.workspaces()
.update("ws_01", UpdateWorkspaceRequest::new("Renamed"))
.await
.unwrap();
}
#[tokio::test]
async fn archive_workspace_posts_to_archive_subpath() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/organizations/workspaces/ws_01/archive"))
.respond_with(ResponseTemplate::new(200).set_body_json({
let mut w = fake_workspace();
w["archived_at"] = json!("2026-05-01T12:00:00Z");
w
}))
.mount(&mock)
.await;
let client = client_for(&mock);
let w = client.admin().workspaces().archive("ws_01").await.unwrap();
assert!(w.archived_at.is_some());
}
#[tokio::test]
async fn retrieve_workspace_returns_typed_record() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/organizations/workspaces/ws_R1"))
.respond_with(ResponseTemplate::new(200).set_body_json(fake_workspace()))
.mount(&mock)
.await;
let client = client_for(&mock);
let w = client.admin().workspaces().retrieve("ws_R1").await.unwrap();
assert_eq!(w.id, "ws_01");
}
}