From c08152d069c1e40eb619b174b674fd899008c0cb Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 14 Sep 2026 19:45:18 +0900 Subject: [PATCH] fix: share workspace creation wire contract --- crates/client/src/backend_workspace.rs | 32 ++++--------- crates/client/tests/backend_workspace.rs | 16 +++---- crates/tui/src/backend_workspace_picker.rs | 4 +- crates/workspace-api/src/lib.rs | 48 +++++++++++++++++++ .../workspace-server/src/workspace_catalog.rs | 19 +------- crates/yoi/src/workspace_bootstrap.rs | 4 +- 6 files changed, 70 insertions(+), 53 deletions(-) diff --git a/crates/client/src/backend_workspace.rs b/crates/client/src/backend_workspace.rs index 149dc04e..60edbc7a 100644 --- a/crates/client/src/backend_workspace.rs +++ b/crates/client/src/backend_workspace.rs @@ -1,32 +1,18 @@ use crate::{BackendApiClient, BackendApiClientError}; use reqwest::Method; -use serde::{Deserialize, Serialize}; use std::fmt; use workspace_api::{ - RepositoryListResponse, RepositorySummary, WorkspaceCatalogListResponse, - WorkspaceCreateResponse, WorkspaceSummary, + InitialRepositoryIntent, RepositoryListResponse, RepositorySummary, + WorkspaceCatalogListResponse, WorkspaceCreateRequest, WorkspaceCreateResponse, + WorkspaceSummary, }; const DEFAULT_WORKSPACE_LIMIT: usize = 200; pub type BackendWorkspace = WorkspaceSummary; pub type CreateBackendWorkspaceResponse = WorkspaceCreateResponse; - -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct CreateBackendWorkspaceRequest { - pub operation_id: String, - pub display_name: String, - pub repository: CreateBackendWorkspaceRepository, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct CreateBackendWorkspaceRepository { - pub repository_key: String, - pub source: String, - pub default_ref: Option, -} +pub type CreateBackendWorkspaceRequest = WorkspaceCreateRequest; +pub type CreateBackendWorkspaceRepository = InitialRepositoryIntent; #[derive(Debug, Clone, PartialEq, Eq)] pub struct BackendWorkspaceCatalogTarget { @@ -185,19 +171,19 @@ mod tests { } #[test] - fn create_request_keeps_operation_id_for_exact_retry() { + fn create_request_keeps_operation_key_for_exact_retry() { let request = CreateBackendWorkspaceRequest { - operation_id: "workspace-create-1".to_string(), + operation_key: "workspace-create-1".to_string(), display_name: "Alpha".to_string(), repository: CreateBackendWorkspaceRepository { repository_key: "main".to_string(), - source: "/srv/repos/alpha".to_string(), + uri: "/srv/repos/alpha".to_string(), default_ref: Some("develop".to_string()), }, }; let retry = request.clone(); - assert_eq!(retry.operation_id, "workspace-create-1"); + assert_eq!(retry.operation_key, "workspace-create-1"); assert_eq!(retry, request); } } diff --git a/crates/client/tests/backend_workspace.rs b/crates/client/tests/backend_workspace.rs index e7a9a122..accd61fc 100644 --- a/crates/client/tests/backend_workspace.rs +++ b/crates/client/tests/backend_workspace.rs @@ -4,26 +4,26 @@ use client::{ }; #[test] -fn workspace_creation_request_preserves_operation_id_for_retry() { +fn workspace_creation_request_preserves_operation_key_for_retry() { let request = CreateBackendWorkspaceRequest { - operation_id: "workspace-create-1".to_string(), + operation_key: "workspace-create-1".to_string(), display_name: "Alpha".to_string(), repository: CreateBackendWorkspaceRepository { repository_key: "main".to_string(), - source: "/srv/repos/alpha".to_string(), + uri: "/srv/repos/alpha".to_string(), default_ref: Some("develop".to_string()), }, }; assert_eq!(request.clone(), request); - assert_eq!(request.operation_id, "workspace-create-1"); + assert_eq!(request.operation_key, "workspace-create-1"); let json = serde_json::to_value(&request).unwrap(); - assert_eq!(json["operation_id"], "workspace-create-1"); + assert_eq!(json["operation_key"], "workspace-create-1"); assert_eq!(json["repository"]["repository_key"], "main"); - assert_eq!(json["repository"]["source"], "/srv/repos/alpha"); - assert!(json.get("operation_key").is_none()); + assert_eq!(json["repository"]["uri"], "/srv/repos/alpha"); + assert!(json.get("operation_id").is_none()); assert!(json["repository"].get("display_name").is_none()); - assert!(json["repository"].get("uri").is_none()); + assert!(json["repository"].get("source").is_none()); } #[test] diff --git a/crates/tui/src/backend_workspace_picker.rs b/crates/tui/src/backend_workspace_picker.rs index 0db2356f..ace33d7e 100644 --- a/crates/tui/src/backend_workspace_picker.rs +++ b/crates/tui/src/backend_workspace_picker.rs @@ -200,10 +200,10 @@ fn prompt_create_request_inner() -> PickerResult, +} + +/// Request for atomically creating a Workspace and its initial Repository. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorkspaceCreateRequest { + pub operation_key: String, + pub display_name: String, + pub repository: InitialRepositoryIntent, +} + /// Response returned after atomically creating a Workspace and its first Repository. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] @@ -3899,6 +3918,35 @@ mod tests { ); } + #[test] + fn workspace_create_request_has_one_closed_shared_wire_shape() { + let request = WorkspaceCreateRequest { + operation_key: "workspace-create-1".to_string(), + display_name: "Workspace".to_string(), + repository: InitialRepositoryIntent { + repository_key: "main".to_string(), + uri: "/srv/repositories/main".to_string(), + default_ref: Some("develop".to_string()), + }, + }; + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["operation_key"], "workspace-create-1"); + assert_eq!(json["repository"]["uri"], "/srv/repositories/main"); + assert!(json.get("operation_id").is_none()); + assert!(json["repository"].get("source").is_none()); + assert!( + serde_json::from_value::(serde_json::json!({ + "operation_id": "workspace-create-1", + "display_name": "Workspace", + "repository": { + "repository_key": "main", + "source": "/srv/repositories/main" + } + })) + .is_err() + ); + } + #[test] fn workspace_and_repository_response_shapes_round_trip() { let workspace = serde_json::json!({ diff --git a/crates/workspace-server/src/workspace_catalog.rs b/crates/workspace-server/src/workspace_catalog.rs index f0600f72..b62a8b42 100644 --- a/crates/workspace-server/src/workspace_catalog.rs +++ b/crates/workspace-server/src/workspace_catalog.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use chrono::{SecondsFormat, Utc}; -use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use uuid::Uuid; +pub use workspace_api::{InitialRepositoryIntent, WorkspaceCreateRequest}; use workspace_api::{RepositoryObservedStatus, RepositorySource}; use crate::repository_source::{parse_repository_source, repository_source_fingerprint}; @@ -19,23 +19,6 @@ use crate::{Error, Result}; const MAX_DISPLAY_NAME_BYTES: usize = 200; const MAX_OPERATION_KEY_BYTES: usize = 200; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct InitialRepositoryIntent { - pub repository_key: String, - pub uri: String, - #[serde(default)] - pub default_ref: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct WorkspaceCreateRequest { - pub operation_key: String, - pub display_name: String, - pub repository: InitialRepositoryIntent, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkspaceCreateResult { pub workspace: WorkspaceRecord, diff --git a/crates/yoi/src/workspace_bootstrap.rs b/crates/yoi/src/workspace_bootstrap.rs index f62266b0..898494a9 100644 --- a/crates/yoi/src/workspace_bootstrap.rs +++ b/crates/yoi/src/workspace_bootstrap.rs @@ -32,11 +32,11 @@ pub(crate) async fn run_init( let response = create_backend_workspace( &target, &CreateBackendWorkspaceRequest { - operation_id: uuid::Uuid::now_v7().to_string(), + operation_key: uuid::Uuid::now_v7().to_string(), display_name: options.display_name, repository: CreateBackendWorkspaceRepository { repository_key: options.repository_key, - source: repository_uri.to_string(), + uri: repository_uri.to_string(), default_ref: options.default_ref, }, },