feat: migrate runtime worker domains to ServerApi
This commit is contained in:
@@ -88,28 +88,25 @@ impl BackendRuntimeTarget {
|
||||
content: Vec<u8>,
|
||||
) -> Result<protocol::UploadedFileRef, BackendRuntimeClientError> {
|
||||
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
||||
let client = runtime_server_api_client(&self.base_url, &api)?;
|
||||
let grant = client
|
||||
.runtime_worker_attachment_upload_grant(
|
||||
self.workspace_id.clone(),
|
||||
self.runtime_id.clone(),
|
||||
self.worker_id.clone(),
|
||||
server_api::WorkerFileUploadQuery {
|
||||
file_name: file_name.to_string(),
|
||||
media_type: media_type.to_string(),
|
||||
upload_id: Some(upload_id.to_string()),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let worker_path = format!(
|
||||
"/api/w/{}/runtimes/{}/workers/{}",
|
||||
path_segment_encode(&self.workspace_id),
|
||||
path_segment_encode(&self.runtime_id),
|
||||
path_segment_encode(&self.worker_id),
|
||||
);
|
||||
let grant_path = format!(
|
||||
"{worker_path}/attachment-upload-grants?file_name={}&media_type={}&upload_id={}",
|
||||
path_segment_encode(file_name),
|
||||
path_segment_encode(media_type),
|
||||
path_segment_encode(&upload_id),
|
||||
);
|
||||
let grant_response = api
|
||||
.request(HttpMethod::POST, &grant_path)?
|
||||
.send()
|
||||
.await
|
||||
.map_err(BackendRuntimeClientError::Http)?;
|
||||
api.check_status(grant_response.status())?;
|
||||
let grant = grant_response
|
||||
.json::<AttachmentUploadGrantResponse>()
|
||||
.await
|
||||
.map_err(BackendRuntimeClientError::Http)?;
|
||||
let upload_path = format!(
|
||||
"{worker_path}/attachment-uploads/{}",
|
||||
path_segment_encode(&grant.upload_id),
|
||||
@@ -133,19 +130,14 @@ impl BackendRuntimeTarget {
|
||||
upload_id: &str,
|
||||
) -> Result<(), BackendRuntimeClientError> {
|
||||
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
||||
let path = format!(
|
||||
"/api/w/{}/runtimes/{}/workers/{}/attachment-uploads/{}",
|
||||
path_segment_encode(&self.workspace_id),
|
||||
path_segment_encode(&self.runtime_id),
|
||||
path_segment_encode(&self.worker_id),
|
||||
path_segment_encode(upload_id),
|
||||
);
|
||||
let response = api
|
||||
.request(HttpMethod::DELETE, &path)?
|
||||
.send()
|
||||
.await
|
||||
.map_err(BackendRuntimeClientError::Http)?;
|
||||
api.check_status(response.status())?;
|
||||
runtime_server_api_client(&self.base_url, &api)?
|
||||
.runtime_worker_attachment_upload_cancel(
|
||||
self.workspace_id.clone(),
|
||||
self.runtime_id.clone(),
|
||||
self.worker_id.clone(),
|
||||
upload_id.to_string(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -154,30 +146,18 @@ impl BackendRuntimeTarget {
|
||||
artifact_id: &str,
|
||||
) -> Result<(), BackendRuntimeClientError> {
|
||||
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
||||
let path = format!(
|
||||
"/api/w/{}/runtimes/{}/workers/{}/attachments/{}",
|
||||
path_segment_encode(&self.workspace_id),
|
||||
path_segment_encode(&self.runtime_id),
|
||||
path_segment_encode(&self.worker_id),
|
||||
path_segment_encode(artifact_id),
|
||||
);
|
||||
let response = api
|
||||
.request(HttpMethod::DELETE, &path)?
|
||||
.send()
|
||||
.await
|
||||
.map_err(BackendRuntimeClientError::Http)?;
|
||||
api.check_status(response.status())?;
|
||||
runtime_server_api_client(&self.base_url, &api)?
|
||||
.runtime_worker_attachment_delete(
|
||||
self.workspace_id.clone(),
|
||||
self.runtime_id.clone(),
|
||||
self.worker_id.clone(),
|
||||
artifact_id.to_string(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AttachmentUploadGrantResponse {
|
||||
upload_id: String,
|
||||
#[allow(dead_code)]
|
||||
expires_at_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UploadedFileResponse {
|
||||
file: protocol::UploadedFileRef,
|
||||
@@ -280,6 +260,7 @@ pub enum BackendRuntimeClientError {
|
||||
InvalidTarget(String),
|
||||
Api(BackendApiClientError),
|
||||
SessionApi(server_api::ServerApiClientError),
|
||||
ContractApi(server_api::client_support::ClientError<server_api::RepositoryApiError>),
|
||||
Http(reqwest::Error),
|
||||
Protocol(String),
|
||||
}
|
||||
@@ -290,6 +271,7 @@ impl fmt::Display for BackendRuntimeClientError {
|
||||
Self::InvalidTarget(message) => f.write_str(message),
|
||||
Self::Api(error) => write!(f, "{error}"),
|
||||
Self::SessionApi(error) => write!(f, "{error}"),
|
||||
Self::ContractApi(error) => write!(f, "{error}"),
|
||||
Self::Http(error) => write!(f, "{error}"),
|
||||
Self::Protocol(message) => f.write_str(message),
|
||||
}
|
||||
@@ -304,6 +286,16 @@ impl From<server_api::ServerApiClientError> for BackendRuntimeClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<server_api::client_support::ClientError<server_api::RepositoryApiError>>
|
||||
for BackendRuntimeClientError
|
||||
{
|
||||
fn from(
|
||||
value: server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
||||
) -> Self {
|
||||
Self::ContractApi(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BackendApiClientError> for BackendRuntimeClientError {
|
||||
fn from(error: BackendApiClientError) -> Self {
|
||||
Self::Api(error)
|
||||
@@ -318,6 +310,34 @@ impl From<reqwest::Error> for BackendRuntimeClientError {
|
||||
|
||||
const WORKER_SESSION_RESPONSE_LIMIT: usize = 16 * 1024 * 1024;
|
||||
|
||||
fn runtime_server_api_client(
|
||||
base_url: &str,
|
||||
backend: &BackendApiClient,
|
||||
) -> Result<server_api::ServerApiClient<StoredBearerAuthorizer>, BackendRuntimeClientError> {
|
||||
server_api::ServerApiClient::builder(base_url)
|
||||
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))?
|
||||
.client(backend.asynchronous_client())
|
||||
.authorizer(StoredBearerAuthorizer {
|
||||
authorization: backend.authorization_header_value(),
|
||||
})
|
||||
.response_body_limit(WORKER_SESSION_RESPONSE_LIMIT)
|
||||
.build()
|
||||
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))
|
||||
}
|
||||
|
||||
fn server_api_error_is_auth(
|
||||
error: &server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
||||
) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
server_api::client_support::ClientError::Public { status, .. }
|
||||
if matches!(
|
||||
*status,
|
||||
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct StoredBearerAuthorizer {
|
||||
authorization: String,
|
||||
@@ -351,15 +371,7 @@ async fn observe_backend_worker_session_with_client(
|
||||
target: &BackendRuntimeTarget,
|
||||
backend: &BackendApiClient,
|
||||
) -> Result<server_api::WorkspaceWorkerSessionResponse, BackendRuntimeClientError> {
|
||||
let client = server_api::ServerApiClient::builder(&target.base_url)
|
||||
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))?
|
||||
.authorizer(StoredBearerAuthorizer {
|
||||
authorization: backend.authorization_header_value(),
|
||||
})
|
||||
.response_body_limit(WORKER_SESSION_RESPONSE_LIMIT)
|
||||
.build()
|
||||
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))?;
|
||||
client
|
||||
runtime_server_api_client(&target.base_url, backend)?
|
||||
.worker_session(
|
||||
target.workspace_id.clone(),
|
||||
target.runtime_id.clone(),
|
||||
@@ -426,80 +438,50 @@ pub async fn list_backend_workers(
|
||||
) -> Result<BackendRuntimeListResponse<BackendWorkerSummary>, BackendRuntimeClientError> {
|
||||
validate_list_target(target)?;
|
||||
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||
let client = runtime_server_api_client(&target.base_url, &api)?;
|
||||
let workspace_id = target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope");
|
||||
if let Some(runtime_id) = target.runtime_id.as_deref() {
|
||||
let path = backend_runtime_workers_path(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope"),
|
||||
runtime_id,
|
||||
);
|
||||
let response = api.request(HttpMethod::GET, &path)?.send().await?;
|
||||
api.check_status(response.status())?;
|
||||
return Ok(response
|
||||
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
|
||||
.await?);
|
||||
return client
|
||||
.runtime_worker_list(
|
||||
workspace_id.to_string(),
|
||||
runtime_id.to_string(),
|
||||
server_api::RuntimeWorkersQuery::default(),
|
||||
)
|
||||
.await
|
||||
.map(runtime_worker_list_response)
|
||||
.map_err(Into::into);
|
||||
}
|
||||
|
||||
let runtime_path = backend_runtimes_path(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope"),
|
||||
);
|
||||
let response = api.request(HttpMethod::GET, &runtime_path)?.send().await?;
|
||||
api.check_status(response.status())?;
|
||||
let runtimes = response
|
||||
.json::<BackendRuntimeListResponse<BackendRuntimeSummary>>()
|
||||
.await?;
|
||||
|
||||
let runtimes = client.runtime_list(workspace_id.to_string()).await?;
|
||||
let mut items = Vec::new();
|
||||
let mut diagnostics = runtimes.diagnostics;
|
||||
for runtime in runtimes.items {
|
||||
let path = backend_runtime_workers_path(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope"),
|
||||
&runtime.runtime_id,
|
||||
);
|
||||
let response = match api.request(HttpMethod::GET, &path)?.send().await {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
diagnostics.push(BackendDiagnostic {
|
||||
code: "runtime_worker_list_failed".to_string(),
|
||||
severity: BackendDiagnosticSeverity::Error,
|
||||
message: format!(
|
||||
"failed to list workers for runtime {}: {error}",
|
||||
runtime.runtime_id
|
||||
),
|
||||
});
|
||||
continue;
|
||||
for resource in runtimes.items {
|
||||
let runtime = resource.runtime;
|
||||
match client
|
||||
.runtime_worker_list(
|
||||
workspace_id.to_string(),
|
||||
runtime.runtime_id.clone(),
|
||||
server_api::RuntimeWorkersQuery::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
diagnostics.extend(response.diagnostics);
|
||||
items.extend(response.items);
|
||||
}
|
||||
};
|
||||
if matches!(
|
||||
response.status(),
|
||||
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
|
||||
) {
|
||||
api.check_status(response.status())?;
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
diagnostics.push(BackendDiagnostic {
|
||||
Err(error) if server_api_error_is_auth(&error) => return Err(error.into()),
|
||||
Err(error) => diagnostics.push(BackendDiagnostic {
|
||||
code: "runtime_worker_list_failed".to_string(),
|
||||
severity: BackendDiagnosticSeverity::Error,
|
||||
message: format!(
|
||||
"failed to list workers for runtime {}: Backend returned HTTP {}",
|
||||
runtime.runtime_id,
|
||||
response.status().as_u16()
|
||||
"failed to list workers for runtime {}: {error}",
|
||||
runtime.runtime_id
|
||||
),
|
||||
});
|
||||
continue;
|
||||
}),
|
||||
}
|
||||
let response = response
|
||||
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
|
||||
.await?;
|
||||
diagnostics.extend(response.diagnostics);
|
||||
items.extend(response.items);
|
||||
}
|
||||
|
||||
Ok(BackendRuntimeListResponse {
|
||||
@@ -521,21 +503,21 @@ pub async fn list_backend_stopped_workers(
|
||||
));
|
||||
};
|
||||
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||
let path = backend_runtime_workers_path(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope"),
|
||||
runtime_id,
|
||||
);
|
||||
let response = api
|
||||
.request(HttpMethod::GET, &format!("{path}?status=stopped"))?
|
||||
.send()
|
||||
.await?;
|
||||
api.check_status(response.status())?;
|
||||
Ok(response
|
||||
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
|
||||
.await?)
|
||||
runtime_server_api_client(&target.base_url, &api)?
|
||||
.runtime_worker_list(
|
||||
target
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.expect("validated Backend Workspace scope")
|
||||
.to_string(),
|
||||
runtime_id.to_string(),
|
||||
server_api::RuntimeWorkersQuery {
|
||||
status: Some(server_api::RuntimeWorkersStatusFilter::Stopped),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(runtime_worker_list_response)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn restore_backend_worker(
|
||||
@@ -543,18 +525,18 @@ pub async fn restore_backend_worker(
|
||||
) -> Result<BackendWorkerRestoreResponse, BackendRuntimeClientError> {
|
||||
validate_target(target)?;
|
||||
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||
let path = backend_runtime_worker_restore_path(
|
||||
&target.workspace_id,
|
||||
&target.runtime_id,
|
||||
&target.worker_id,
|
||||
);
|
||||
let response = api
|
||||
.request(HttpMethod::POST, &path)?
|
||||
.json(&serde_json::json!({}))
|
||||
.send()
|
||||
.await?;
|
||||
let response = api.require_success(response).await?;
|
||||
Ok(response.json::<BackendWorkerRestoreResponse>().await?)
|
||||
runtime_server_api_client(&target.base_url, &api)?
|
||||
.runtime_worker_restore(
|
||||
target.workspace_id.clone(),
|
||||
target.runtime_id.clone(),
|
||||
target.worker_id.clone(),
|
||||
server_api::RestoreTicketAssignmentQuery {
|
||||
ticket_id: None,
|
||||
assignment_operation_id: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn connect_backend_runtime(
|
||||
@@ -599,6 +581,18 @@ fn protocol_connect_error_message(
|
||||
)
|
||||
}
|
||||
|
||||
fn runtime_worker_list_response(
|
||||
response: server_api::RuntimeWorkerListResponse,
|
||||
) -> BackendRuntimeListResponse<BackendWorkerSummary> {
|
||||
BackendRuntimeListResponse {
|
||||
workspace_id: response.workspace_id,
|
||||
limit: response.limit,
|
||||
items: response.items,
|
||||
source: response.source,
|
||||
diagnostics: response.diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_target(target: &BackendRuntimeTarget) -> Result<(), BackendRuntimeClientError> {
|
||||
if target.base_url.trim().is_empty() {
|
||||
return Err(BackendRuntimeClientError::InvalidTarget(
|
||||
@@ -697,31 +691,6 @@ fn backend_workspace_workers_launch_options_path(workspace_id: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn backend_runtimes_path(workspace_id: &str) -> String {
|
||||
format!("/api/w/{}/runtimes", path_segment_encode(workspace_id))
|
||||
}
|
||||
|
||||
fn backend_runtime_workers_path(workspace_id: &str, runtime_id: &str) -> String {
|
||||
format!(
|
||||
"/api/w/{}/runtimes/{}/workers",
|
||||
path_segment_encode(workspace_id),
|
||||
path_segment_encode(runtime_id)
|
||||
)
|
||||
}
|
||||
|
||||
fn backend_runtime_worker_restore_path(
|
||||
workspace_id: &str,
|
||||
runtime_id: &str,
|
||||
worker_id: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
"/api/w/{}/runtimes/{}/workers/{}/restore",
|
||||
path_segment_encode(workspace_id),
|
||||
path_segment_encode(runtime_id),
|
||||
path_segment_encode(worker_id)
|
||||
)
|
||||
}
|
||||
|
||||
fn protocol_ws_request(
|
||||
target: &BackendRuntimeTarget,
|
||||
api: &BackendApiClient,
|
||||
@@ -1051,21 +1020,4 @@ mod tests {
|
||||
serde_json::json!(64);
|
||||
assert!(serde_json::from_value::<BackendWorkerSummary>(stale).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workers_path_requires_workspace_scope_for_status_queries() {
|
||||
let path = backend_runtime_workers_path("team main", "runtime/one");
|
||||
assert_eq!(
|
||||
format!("{path}?status=stopped"),
|
||||
"/api/w/team%20main/runtimes/runtime%2Fone/workers?status=stopped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_worker_path_requires_workspace_scope() {
|
||||
assert_eq!(
|
||||
backend_runtime_worker_restore_path("team main", "runtime/one", "worker one"),
|
||||
"/api/w/team%20main/runtimes/runtime%2Fone/workers/worker%20one/restore"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ pub enum BackendWorkspaceClientError {
|
||||
Api(BackendApiClientError),
|
||||
Http(reqwest::Error),
|
||||
ServerApi(server_api::client_support::ClientError<server_api::RepositoryApiError>),
|
||||
RuntimeManagementApi(
|
||||
server_api::client_support::ClientError<server_api::RuntimeManagementApiError>,
|
||||
),
|
||||
}
|
||||
|
||||
impl fmt::Display for BackendWorkspaceClientError {
|
||||
@@ -40,6 +43,7 @@ impl fmt::Display for BackendWorkspaceClientError {
|
||||
Self::Api(error) => write!(f, "{error}"),
|
||||
Self::Http(error) => write!(f, "{error}"),
|
||||
Self::ServerApi(error) => write!(f, "{error}"),
|
||||
Self::RuntimeManagementApi(error) => write!(f, "{error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +95,29 @@ pub(crate) fn server_client_error(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_management_client_error(
|
||||
backend: &BackendApiClient,
|
||||
error: server_api::client_support::ClientError<server_api::RuntimeManagementApiError>,
|
||||
) -> BackendWorkspaceClientError {
|
||||
let status = match &error {
|
||||
server_api::client_support::ClientError::Public { status, .. } => Some(*status),
|
||||
server_api::client_support::ClientError::Failure(_) => None,
|
||||
};
|
||||
match status {
|
||||
Some(reqwest::StatusCode::UNAUTHORIZED) => {
|
||||
BackendWorkspaceClientError::Api(BackendApiClientError::Unauthorized {
|
||||
origin: backend.origin().clone(),
|
||||
})
|
||||
}
|
||||
Some(reqwest::StatusCode::FORBIDDEN) => {
|
||||
BackendWorkspaceClientError::Api(BackendApiClientError::Forbidden {
|
||||
origin: backend.origin().clone(),
|
||||
})
|
||||
}
|
||||
_ => BackendWorkspaceClientError::RuntimeManagementApi(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ServerBearerAuthorizer {
|
||||
authorization: String,
|
||||
|
||||
@@ -345,24 +345,40 @@ impl BackendWorkspaceProductClient {
|
||||
pub fn list_runtimes(
|
||||
&self,
|
||||
) -> Result<ListResponse<WorkspaceRuntimeResource>, BackendWorkspaceClientError> {
|
||||
self.get_json("/runtimes")
|
||||
let workspace_id = self.workspace_id.clone();
|
||||
let response =
|
||||
self.generated(move |client| async move { client.runtime_list(workspace_id).await })?;
|
||||
Ok(ListResponse {
|
||||
workspace_id: response.workspace_id,
|
||||
limit: response.limit,
|
||||
items: response.items,
|
||||
source: response.source,
|
||||
diagnostics: response.diagnostics,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn runtime_detail(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
|
||||
self.get_json(&format!("/runtimes/{}", encode_path_segment(runtime_id)))
|
||||
let workspace_id = self.workspace_id.clone();
|
||||
let runtime_id = runtime_id.to_string();
|
||||
self.generated(move |client| async move {
|
||||
client.runtime_detail(workspace_id, runtime_id).await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reveal_runtime_trust_key(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
) -> Result<RuntimeTrustKeyRevealResponse, BackendWorkspaceClientError> {
|
||||
self.get_json(&format!(
|
||||
"/runtimes/{}/trust-key",
|
||||
encode_path_segment(runtime_id)
|
||||
))
|
||||
let workspace_id = self.workspace_id.clone();
|
||||
let runtime_id = runtime_id.to_string();
|
||||
self.generated(move |client| async move {
|
||||
client
|
||||
.runtime_trust_key_reveal(workspace_id, runtime_id)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn revoke_runtime_trust_key(
|
||||
@@ -370,11 +386,20 @@ impl BackendWorkspaceProductClient {
|
||||
runtime_id: &str,
|
||||
request: &RevokeRuntimeTrustKeyRequest,
|
||||
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
|
||||
self.send_json(
|
||||
Method::DELETE,
|
||||
&format!("/runtimes/{}/trust-key", encode_path_segment(runtime_id)),
|
||||
Some(request),
|
||||
)
|
||||
let client = crate::backend_workspace::server_api_client(&self.api)?;
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?;
|
||||
runtime
|
||||
.block_on(client.runtime_trust_key_revoke(
|
||||
self.workspace_id.clone(),
|
||||
runtime_id.to_string(),
|
||||
request.clone(),
|
||||
))
|
||||
.map_err(|error| {
|
||||
crate::backend_workspace::runtime_management_client_error(&self.api, error)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn memory_document(&self) -> Result<MemoryDocumentResponse, BackendWorkspaceClientError> {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use server_api::repository_openapi_typescript::{
|
||||
INPUT_PATH, RUNTIME_OUTPUT_PATH, generate_runtime_typescript,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = run() {
|
||||
eprintln!("{error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let mut check = false;
|
||||
for argument in std::env::args().skip(1) {
|
||||
match argument.as_str() {
|
||||
"--check" => check = true,
|
||||
"--help" | "-h" => {
|
||||
println!(
|
||||
"Generate the bounded Runtime API TypeScript artifact.\n\n\
|
||||
Usage: cargo run -q -p server-api --example generate_runtime_api_types -- [--check]"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
other => return Err(format!("unknown argument `{other}`")),
|
||||
}
|
||||
}
|
||||
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let input_path = root.join(INPUT_PATH);
|
||||
let output_path = root.join(RUNTIME_OUTPUT_PATH);
|
||||
let input = fs::read_to_string(&input_path)
|
||||
.map_err(|error| format!("failed to read {}: {error}", input_path.display()))?;
|
||||
let generated = generate_runtime_typescript(&input)
|
||||
.map_err(|error| format!("failed to generate Runtime API types: {error}"))?;
|
||||
|
||||
if check {
|
||||
let checked_in = fs::read_to_string(&output_path)
|
||||
.map_err(|error| format!("failed to read {}: {error}", output_path.display()))?;
|
||||
if checked_in != generated {
|
||||
return Err(format!(
|
||||
"{} is stale; regenerate it without --check",
|
||||
output_path.display()
|
||||
));
|
||||
}
|
||||
println!("{} is current", output_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let temporary_path = output_path.with_extension("ts.tmp");
|
||||
fs::write(&temporary_path, generated)
|
||||
.map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?;
|
||||
fs::rename(&temporary_path, &output_path)
|
||||
.map_err(|error| format!("failed to replace {}: {error}", output_path.display()))?;
|
||||
println!("generated {}", output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
+1132
-124
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,11 @@ pub const WORKDIR_GENERATOR_VERSION: &str = "1";
|
||||
pub const WORKDIR_GENERATOR_OPTIONS: &str =
|
||||
"workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
||||
pub const WORKDIR_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/workdir-api.ts";
|
||||
pub const RUNTIME_GENERATOR_NAME: &str = "yoi-runtime-openapi-typescript";
|
||||
pub const RUNTIME_GENERATOR_VERSION: &str = "1";
|
||||
pub const RUNTIME_GENERATOR_OPTIONS: &str =
|
||||
"runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
||||
pub const RUNTIME_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/runtime-api.ts";
|
||||
|
||||
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
|
||||
const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0;
|
||||
@@ -119,6 +124,48 @@ pub fn generate_workdir_typescript(input: &str) -> Result<String, GenerationErro
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn generate_runtime_typescript(input: &str) -> Result<String, GenerationError> {
|
||||
let document: Value = serde_json::from_str(input)
|
||||
.map_err(|error| GenerationError::invalid(format!("invalid OpenAPI JSON: {error}")))?;
|
||||
let root = object(&document, "OpenAPI document")?;
|
||||
if root.get("openapi").and_then(Value::as_str) != Some("3.1.0") {
|
||||
return Err(GenerationError::invalid(
|
||||
"Runtime TypeScript generation requires OpenAPI 3.1.0",
|
||||
));
|
||||
}
|
||||
|
||||
let info = object_field(root, "info", "OpenAPI document")?;
|
||||
let contract_version = required_string(info, "version", "OpenAPI info")?;
|
||||
let contract_digest = required_string(info, "x-yoi-source-digest", "OpenAPI info")?;
|
||||
validate_contract_digest(contract_digest)?;
|
||||
let schemas = object_field(
|
||||
object_field(root, "components", "OpenAPI document")?,
|
||||
"schemas",
|
||||
"OpenAPI components",
|
||||
)?;
|
||||
let paths = object_field(root, "paths", "OpenAPI document")?;
|
||||
let roots = validate_runtime_operations(paths)?;
|
||||
let closure = schema_closure(schemas, &roots)?;
|
||||
|
||||
let mut output = format!(
|
||||
"// @generated by {RUNTIME_GENERATOR_NAME}@{RUNTIME_GENERATOR_VERSION}. Do not edit.\n\
|
||||
// Input: {INPUT_PATH}\n\
|
||||
// Input contract: {contract_version} ({contract_digest})\n\
|
||||
// Options: {RUNTIME_GENERATOR_OPTIONS}\n\
|
||||
// Output: {RUNTIME_OUTPUT_PATH}\n\
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check\n\n"
|
||||
);
|
||||
for name in closure {
|
||||
let schema = schemas
|
||||
.get(&name)
|
||||
.ok_or_else(|| GenerationError::invalid(format!("missing schema `{name}`")))?;
|
||||
let declaration = render_schema(schema, &format!("schema `{name}`"))?;
|
||||
output.push_str(&format!("export type {name} = {declaration};\n\n"));
|
||||
}
|
||||
output.pop();
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> {
|
||||
let Some(hex) = digest.strip_prefix("sha256:") else {
|
||||
return Err(GenerationError::invalid(
|
||||
@@ -347,6 +394,364 @@ fn validate_workdir_operations(
|
||||
validate_operation_contracts(paths, &operations)
|
||||
}
|
||||
|
||||
fn validate_runtime_operations(
|
||||
paths: &Map<String, Value>,
|
||||
) -> Result<BTreeSet<String>, GenerationError> {
|
||||
let operations = [
|
||||
runtime_operation(
|
||||
"/api/runtimes",
|
||||
"get",
|
||||
"runtime_list_alias",
|
||||
None,
|
||||
&[("200", "RuntimeListResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes",
|
||||
"get",
|
||||
"runtime_list",
|
||||
None,
|
||||
&[("200", "WorkspaceRuntimeListResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes",
|
||||
"post",
|
||||
"runtime_create",
|
||||
Some("CreateRemoteRuntimeRequest"),
|
||||
&[
|
||||
("200", "WorkspaceRuntimeResource"),
|
||||
("201", "WorkspaceRuntimeResource"),
|
||||
],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}",
|
||||
"get",
|
||||
"runtime_detail",
|
||||
None,
|
||||
&[("200", "WorkspaceRuntimeDetail")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}",
|
||||
"post",
|
||||
"runtime_update",
|
||||
Some("UpdateRemoteRuntimeRequest"),
|
||||
&[("200", "WorkspaceRuntimeDetail")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}",
|
||||
"delete",
|
||||
"runtime_remove",
|
||||
Some("RemoveRuntimeRequest"),
|
||||
&[("200", "RuntimeRemovalOperationResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/trust-key",
|
||||
"get",
|
||||
"runtime_trust_key_reveal",
|
||||
None,
|
||||
&[("200", "RuntimeTrustKeyRevealResponse")],
|
||||
),
|
||||
RuntimeOperationContract {
|
||||
error: "RuntimeManagementApiError",
|
||||
..runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/trust-key",
|
||||
"delete",
|
||||
"runtime_trust_key_revoke",
|
||||
Some("RevokeRuntimeTrustKeyRequest"),
|
||||
&[("200", "WorkspaceRuntimeDetail")],
|
||||
)
|
||||
},
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/connection-tests",
|
||||
"post",
|
||||
"runtime_connection_test",
|
||||
None,
|
||||
&[("200", "RuntimeConnectionTestResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/workers",
|
||||
"get",
|
||||
"runtime_worker_list_alias",
|
||||
None,
|
||||
&[("200", "RuntimeWorkerListResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/workers",
|
||||
"post",
|
||||
"runtime_worker_create_alias",
|
||||
Some("RuntimeWorkerSpawnRequest"),
|
||||
&[("200", "RuntimeWorkerSpawnResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers",
|
||||
"get",
|
||||
"runtime_worker_list",
|
||||
None,
|
||||
&[("200", "RuntimeWorkerListResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers",
|
||||
"post",
|
||||
"runtime_worker_create",
|
||||
Some("RuntimeWorkerSpawnRequest"),
|
||||
&[("200", "RuntimeWorkerSpawnResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/config-bundles",
|
||||
"post",
|
||||
"runtime_config_bundle_sync_alias",
|
||||
Some("RuntimeConfigBundleSyncRequest"),
|
||||
&[("200", "RuntimeConfigBundleResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/config-bundles",
|
||||
"post",
|
||||
"runtime_config_bundle_sync",
|
||||
Some("RuntimeConfigBundleSyncRequest"),
|
||||
&[("200", "RuntimeConfigBundleResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/config-bundles/{bundle_id}/availability",
|
||||
"get",
|
||||
"runtime_config_bundle_availability_alias",
|
||||
None,
|
||||
&[("200", "RuntimeConfigBundleResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/config-bundles/{bundle_id}/availability",
|
||||
"get",
|
||||
"runtime_config_bundle_availability",
|
||||
None,
|
||||
&[("200", "RuntimeConfigBundleResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}",
|
||||
"get",
|
||||
"runtime_worker_detail_alias",
|
||||
None,
|
||||
&[("200", "RuntimeWorkerShowResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}",
|
||||
"get",
|
||||
"runtime_worker_detail",
|
||||
None,
|
||||
&[("200", "RuntimeWorkerShowResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/restore",
|
||||
"post",
|
||||
"runtime_worker_restore_alias",
|
||||
None,
|
||||
&[("200", "WorkerRestoreResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/restore",
|
||||
"post",
|
||||
"runtime_worker_restore",
|
||||
None,
|
||||
&[("200", "WorkerRestoreResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/pin",
|
||||
"put",
|
||||
"runtime_worker_pin",
|
||||
None,
|
||||
&[("200", "WorkerRetentionResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/pin",
|
||||
"delete",
|
||||
"runtime_worker_unpin",
|
||||
None,
|
||||
&[("200", "WorkerRetentionResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/cleanup-plan",
|
||||
"get",
|
||||
"runtime_cleanup_plan",
|
||||
None,
|
||||
&[("200", "RuntimeCleanupPlanResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/cleanup-executions",
|
||||
"post",
|
||||
"runtime_cleanup_execute",
|
||||
Some("ExecuteRuntimeCleanupRequest"),
|
||||
&[("200", "RuntimeCleanupExecutionResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/input",
|
||||
"post",
|
||||
"runtime_worker_input_alias",
|
||||
Some("RuntimeWorkerInputRequest"),
|
||||
&[("200", "RuntimeWorkerInputResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/input",
|
||||
"post",
|
||||
"runtime_worker_input",
|
||||
Some("RuntimeWorkerInputRequest"),
|
||||
&[("200", "RuntimeWorkerInputResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachment-upload-grants",
|
||||
"post",
|
||||
"runtime_worker_attachment_upload_grant",
|
||||
None,
|
||||
&[("200", "AttachmentUploadGrantResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachment-uploads/{upload_id}",
|
||||
"delete",
|
||||
"runtime_worker_attachment_upload_cancel",
|
||||
None,
|
||||
&[("200", "AttachmentUploadCancelResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachments/{artifact_id}",
|
||||
"delete",
|
||||
"runtime_worker_attachment_delete",
|
||||
None,
|
||||
&[("200", "WorkerFileDeleteResponse")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/completions",
|
||||
"post",
|
||||
"runtime_worker_completions_alias",
|
||||
Some("RuntimeWorkerCompletionsRequest"),
|
||||
&[("200", "RuntimeWorkerCompletionsResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/completions",
|
||||
"post",
|
||||
"runtime_worker_completions",
|
||||
Some("RuntimeWorkerCompletionsRequest"),
|
||||
&[("200", "RuntimeWorkerCompletionsResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/stop",
|
||||
"post",
|
||||
"runtime_worker_stop_alias",
|
||||
Some("RuntimeWorkerLifecycleRequest"),
|
||||
&[("200", "RuntimeWorkerLifecycleResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/stop",
|
||||
"post",
|
||||
"runtime_worker_stop",
|
||||
Some("RuntimeWorkerLifecycleRequest"),
|
||||
&[("200", "RuntimeWorkerLifecycleResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/cancel",
|
||||
"post",
|
||||
"runtime_worker_cancel_alias",
|
||||
Some("RuntimeWorkerLifecycleRequest"),
|
||||
&[("200", "RuntimeWorkerLifecycleResult")],
|
||||
),
|
||||
runtime_operation(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/cancel",
|
||||
"post",
|
||||
"runtime_worker_cancel",
|
||||
Some("RuntimeWorkerLifecycleRequest"),
|
||||
&[("200", "RuntimeWorkerLifecycleResult")],
|
||||
),
|
||||
];
|
||||
validate_runtime_operation_contracts(paths, &operations)
|
||||
}
|
||||
|
||||
const fn runtime_operation(
|
||||
path: &'static str,
|
||||
method: &'static str,
|
||||
operation_id: &'static str,
|
||||
request: Option<&'static str>,
|
||||
successes: &'static [(&'static str, &'static str)],
|
||||
) -> RuntimeOperationContract {
|
||||
RuntimeOperationContract {
|
||||
path,
|
||||
method,
|
||||
operation_id,
|
||||
request,
|
||||
successes,
|
||||
error: "RepositoryApiError",
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_runtime_operation_contracts(
|
||||
paths: &Map<String, Value>,
|
||||
operations: &[RuntimeOperationContract],
|
||||
) -> Result<BTreeSet<String>, GenerationError> {
|
||||
let mut roots = BTreeSet::new();
|
||||
for operation in operations {
|
||||
let path = object_field(paths, operation.path, "OpenAPI paths")?;
|
||||
let value = object_field(path, operation.method, operation.path)?;
|
||||
if value.get("operationId").and_then(Value::as_str) != Some(operation.operation_id) {
|
||||
return Err(GenerationError::invalid(format!(
|
||||
"{} {} must retain operationId `{}`",
|
||||
operation.method, operation.path, operation.operation_id
|
||||
)));
|
||||
}
|
||||
match operation.request {
|
||||
Some(expected) => {
|
||||
let request = object_field(value, "requestBody", operation.operation_id)?;
|
||||
require_schema_ref(
|
||||
json_content_schema(request, operation.operation_id)?,
|
||||
expected,
|
||||
operation.operation_id,
|
||||
)?;
|
||||
roots.insert(expected.to_owned());
|
||||
}
|
||||
None if value.contains_key("requestBody") => {
|
||||
return Err(GenerationError::invalid(format!(
|
||||
"{} unexpectedly gained a request body",
|
||||
operation.operation_id
|
||||
)));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
let responses = object_field(value, "responses", operation.operation_id)?;
|
||||
let successes = operation
|
||||
.successes
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for (status, response) in responses {
|
||||
let response = object(
|
||||
response,
|
||||
&format!("{} response {status}", operation.operation_id),
|
||||
)?;
|
||||
let expected = successes
|
||||
.get(status.as_str())
|
||||
.copied()
|
||||
.unwrap_or(operation.error);
|
||||
require_schema_ref(
|
||||
json_content_schema(response, operation.operation_id)?,
|
||||
expected,
|
||||
operation.operation_id,
|
||||
)?;
|
||||
roots.insert(expected.to_owned());
|
||||
}
|
||||
for (status, expected) in successes {
|
||||
if !responses.contains_key(status) {
|
||||
return Err(GenerationError::invalid(format!(
|
||||
"{} is missing success response {status} ({expected})",
|
||||
operation.operation_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(roots)
|
||||
}
|
||||
|
||||
struct RuntimeOperationContract {
|
||||
path: &'static str,
|
||||
method: &'static str,
|
||||
operation_id: &'static str,
|
||||
request: Option<&'static str>,
|
||||
successes: &'static [(&'static str, &'static str)],
|
||||
error: &'static str,
|
||||
}
|
||||
|
||||
fn validate_operation_contracts(
|
||||
paths: &Map<String, Value>,
|
||||
operations: &[OperationContract],
|
||||
@@ -686,6 +1091,21 @@ fn render_schema(value: &Value, context: &str) -> Result<String, GenerationError
|
||||
.map(|branches| branches.join(" | "));
|
||||
}
|
||||
|
||||
let disjoint_types = branches
|
||||
.iter()
|
||||
.map(|branch| branch.as_object()?.get("type")?.as_str())
|
||||
.collect::<Option<Vec<_>>>();
|
||||
if let Some(types) = disjoint_types {
|
||||
let unique = types.iter().copied().collect::<BTreeSet<_>>();
|
||||
if outer_type.is_none() && unique.len() == branches.len() {
|
||||
return branches
|
||||
.iter()
|
||||
.map(|branch| render_schema(branch, context))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(|branches| branches.join(" | "));
|
||||
}
|
||||
}
|
||||
|
||||
let mut rendered = Vec::with_capacity(branches.len());
|
||||
let mut seen_literals = BTreeSet::new();
|
||||
for branch in branches {
|
||||
@@ -1007,6 +1427,44 @@ mod tests {
|
||||
const OPENAPI: &str = include_str!("../../../openapi/server-api.json");
|
||||
const TYPESCRIPT: &str =
|
||||
include_str!("../../../web/workspace/src/lib/generated/repository-api.ts");
|
||||
const RUNTIME_TYPESCRIPT: &str =
|
||||
include_str!("../../../web/workspace/src/lib/generated/runtime-api.ts");
|
||||
|
||||
#[test]
|
||||
fn checked_in_runtime_typescript_is_current() {
|
||||
assert_eq!(
|
||||
generate_runtime_typescript(OPENAPI).unwrap(),
|
||||
RUNTIME_TYPESCRIPT,
|
||||
"regenerate with `cargo run -q -p server-api --example generate_runtime_api_types`",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_generator_validates_operation_ownership_and_schema_closure() {
|
||||
let output = generate_runtime_typescript(OPENAPI).unwrap();
|
||||
assert!(output.contains(&format!(
|
||||
"@generated by {RUNTIME_GENERATOR_NAME}@{RUNTIME_GENERATOR_VERSION}"
|
||||
)));
|
||||
assert!(output.contains("export type RuntimeCleanupPlanResponse ="));
|
||||
assert!(output.contains("export type RuntimeWorkerLifecycleResult ="));
|
||||
assert!(output.contains("export type RuntimeWorkerSpawnAcceptanceRequirement = \"socket_ready\" | { run_accepted:"));
|
||||
assert!(!output.contains("export type UnmigratedDomain ="));
|
||||
|
||||
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||
document["paths"]["/api/w/{workspace_id}/runtimes/{runtime_id}/cleanup-plan"]["get"]["operationId"] =
|
||||
serde_json::json!("manual_cleanup_plan");
|
||||
let error = generate_runtime_typescript(&document.to_string()).unwrap_err();
|
||||
assert!(error.to_string().contains("runtime_cleanup_plan"));
|
||||
|
||||
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||
document["components"]["schemas"]["RuntimeWorkerSpawnAcceptanceRequirement"]
|
||||
["oneOf"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(serde_json::json!({"type": "object", "properties": {}, "additionalProperties": false}));
|
||||
let error = generate_runtime_typescript(&document.to_string()).unwrap_err();
|
||||
assert!(error.to_string().contains("ambiguous oneOf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_in_repository_typescript_is_current() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user