Migrate repository routes to ServerApi
This commit is contained in:
Generated
+2
@@ -4157,8 +4157,10 @@ dependencies = [
|
|||||||
"api-macros",
|
"api-macros",
|
||||||
"protocol",
|
"protocol",
|
||||||
"runtime-api",
|
"runtime-api",
|
||||||
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2 0.11.0",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"webauthn-rs-proto",
|
"webauthn-rs-proto",
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ struct RouteArgs {
|
|||||||
path: LitStr,
|
path: LitStr,
|
||||||
operation_id: Option<LitStr>,
|
operation_id: Option<LitStr>,
|
||||||
status: Option<LitInt>,
|
status: Option<LitInt>,
|
||||||
|
alternate_status: Option<LitInt>,
|
||||||
error_status: Option<LitInt>,
|
error_status: Option<LitInt>,
|
||||||
|
openapi: Option<syn::LitBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for RouteArgs {
|
impl Parse for RouteArgs {
|
||||||
@@ -114,7 +116,9 @@ impl Parse for RouteArgs {
|
|||||||
path,
|
path,
|
||||||
operation_id: None,
|
operation_id: None,
|
||||||
status: None,
|
status: None,
|
||||||
|
alternate_status: None,
|
||||||
error_status: None,
|
error_status: None,
|
||||||
|
openapi: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
while !input.is_empty() {
|
while !input.is_empty() {
|
||||||
@@ -132,12 +136,24 @@ impl Parse for RouteArgs {
|
|||||||
"operation_id",
|
"operation_id",
|
||||||
)?,
|
)?,
|
||||||
"status" => set_once(&mut result.status, input.parse::<LitInt>()?, &key, "status")?,
|
"status" => set_once(&mut result.status, input.parse::<LitInt>()?, &key, "status")?,
|
||||||
|
"alternate_status" => set_once(
|
||||||
|
&mut result.alternate_status,
|
||||||
|
input.parse::<LitInt>()?,
|
||||||
|
&key,
|
||||||
|
"alternate_status",
|
||||||
|
)?,
|
||||||
"error_status" => set_once(
|
"error_status" => set_once(
|
||||||
&mut result.error_status,
|
&mut result.error_status,
|
||||||
input.parse::<LitInt>()?,
|
input.parse::<LitInt>()?,
|
||||||
&key,
|
&key,
|
||||||
"error_status",
|
"error_status",
|
||||||
)?,
|
)?,
|
||||||
|
"openapi" => set_once(
|
||||||
|
&mut result.openapi,
|
||||||
|
input.parse::<syn::LitBool>()?,
|
||||||
|
&key,
|
||||||
|
"openapi",
|
||||||
|
)?,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(syn::Error::new(
|
return Err(syn::Error::new(
|
||||||
key.span(),
|
key.span(),
|
||||||
@@ -167,6 +183,9 @@ enum Location {
|
|||||||
Query,
|
Query,
|
||||||
Header,
|
Header,
|
||||||
Body,
|
Body,
|
||||||
|
/// Trusted request-local context supplied by the server adapter. Extension
|
||||||
|
/// parameters are not part of the wire contract or generated client signature.
|
||||||
|
Extension,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Location {
|
impl Location {
|
||||||
@@ -176,6 +195,7 @@ impl Location {
|
|||||||
Self::Query => quote!(Query),
|
Self::Query => quote!(Query),
|
||||||
Self::Header => quote!(Header),
|
Self::Header => quote!(Header),
|
||||||
Self::Body => quote!(Body),
|
Self::Body => quote!(Body),
|
||||||
|
Self::Extension => unreachable!("extensions are not wire parameters"),
|
||||||
};
|
};
|
||||||
quote!(#api_crate::ParameterLocation::#variant)
|
quote!(#api_crate::ParameterLocation::#variant)
|
||||||
}
|
}
|
||||||
@@ -201,7 +221,9 @@ struct Operation {
|
|||||||
error_body: Option<Type>,
|
error_body: Option<Type>,
|
||||||
fallible: bool,
|
fallible: bool,
|
||||||
response_status: u16,
|
response_status: u16,
|
||||||
|
alternate_status: Option<u16>,
|
||||||
error_status: Option<u16>,
|
error_status: Option<u16>,
|
||||||
|
openapi_skip: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ApiDefinition {
|
struct ApiDefinition {
|
||||||
@@ -392,6 +414,21 @@ fn normalize_operation(method: &mut TraitItemFn) -> syn::Result<Operation> {
|
|||||||
"status must be a success status between 200 and 299",
|
"status must be a success status between 200 and 299",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let alternate_status = parse_status(route.alternate_status.as_ref(), "alternate_status")?;
|
||||||
|
if let Some(status) = alternate_status
|
||||||
|
&& (!(200..=299).contains(&status) || status == response_status)
|
||||||
|
{
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
route.alternate_status,
|
||||||
|
"alternate_status must be a distinct success status between 200 and 299",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if alternate_status.is_some() && response_body.is_none() {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
route.alternate_status,
|
||||||
|
"alternate_status requires a response body implementing HttpSuccess",
|
||||||
|
));
|
||||||
|
}
|
||||||
let error_status = parse_status(route.error_status.as_ref(), "error_status")?;
|
let error_status = parse_status(route.error_status.as_ref(), "error_status")?;
|
||||||
if error_status.is_some() && error_body.is_none() {
|
if error_status.is_some() && error_body.is_none() {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
@@ -464,11 +501,20 @@ fn normalize_operation(method: &mut TraitItemFn) -> syn::Result<Operation> {
|
|||||||
}
|
}
|
||||||
(Location::Body, rust_name.clone())
|
(Location::Body, rust_name.clone())
|
||||||
}
|
}
|
||||||
|
Some((Location::Extension, wire_name)) => {
|
||||||
|
if wire_name.is_some() {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
ident.span(),
|
||||||
|
"#[extension] does not accept a wire name",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(Location::Extension, rust_name.clone())
|
||||||
|
}
|
||||||
None if placeholders.contains(&rust_name) => (Location::Path, rust_name.clone()),
|
None if placeholders.contains(&rust_name) => (Location::Path, rust_name.clone()),
|
||||||
None => {
|
None => {
|
||||||
return Err(syn::Error::new(
|
return Err(syn::Error::new(
|
||||||
ident.span(),
|
ident.span(),
|
||||||
"API arguments must use #[path], #[query], #[header], or #[body]; path arguments may omit #[path] when their name matches a placeholder",
|
"API arguments must use #[path], #[query], #[header], #[body], or #[extension]; path arguments may omit #[path] when their name matches a placeholder",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -537,7 +583,9 @@ fn normalize_operation(method: &mut TraitItemFn) -> syn::Result<Operation> {
|
|||||||
error_body,
|
error_body,
|
||||||
fallible,
|
fallible,
|
||||||
response_status,
|
response_status,
|
||||||
|
alternate_status,
|
||||||
error_status,
|
error_status,
|
||||||
|
openapi_skip: route.openapi.as_ref().is_some_and(|value| !value.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,6 +636,7 @@ fn take_location(attrs: &mut Vec<Attribute>) -> syn::Result<Option<(Location, Op
|
|||||||
Some("query") => Some(Location::Query),
|
Some("query") => Some(Location::Query),
|
||||||
Some("header") => Some(Location::Header),
|
Some("header") => Some(Location::Header),
|
||||||
Some("body") => Some(Location::Body),
|
Some("body") => Some(Location::Body),
|
||||||
|
Some("extension") => Some(Location::Extension),
|
||||||
Some("stream") | Some("multipart") | Some("binary") => {
|
Some("stream") | Some("multipart") | Some("binary") => {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
attr,
|
attr,
|
||||||
@@ -881,10 +930,13 @@ fn reqwest_adapter_tokens(
|
|||||||
let builder_ident = format_ident!("{}ClientBuilder", ident_text(trait_ident));
|
let builder_ident = format_ident!("{}ClientBuilder", ident_text(trait_ident));
|
||||||
let methods = api.operations.iter().map(|operation| {
|
let methods = api.operations.iter().map(|operation| {
|
||||||
let method_ident = &operation.method_ident;
|
let method_ident = &operation.method_ident;
|
||||||
let arguments = operation.parameters.iter().map(|parameter| {
|
let arguments = operation.parameters.iter().filter_map(|parameter| {
|
||||||
|
if matches!(parameter.location, Location::Extension) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let ident = ¶meter.rust_ident;
|
let ident = ¶meter.rust_ident;
|
||||||
let ty = ¶meter.ty;
|
let ty = ¶meter.ty;
|
||||||
quote!(#ident: #ty)
|
Some(quote!(#ident: #ty))
|
||||||
});
|
});
|
||||||
let path_segments = operation
|
let path_segments = operation
|
||||||
.path
|
.path
|
||||||
@@ -928,6 +980,10 @@ fn reqwest_adapter_tokens(
|
|||||||
.unwrap_or_else(|| quote!(::core::option::Option::None));
|
.unwrap_or_else(|| quote!(::core::option::Option::None));
|
||||||
let method = reqwest_method_tokens(operation.method, api_crate);
|
let method = reqwest_method_tokens(operation.method, api_crate);
|
||||||
let response_status = operation.response_status;
|
let response_status = operation.response_status;
|
||||||
|
let accepted_status = operation
|
||||||
|
.alternate_status
|
||||||
|
.map(|alternate| quote!(__actual == #response_status || __actual == #alternate))
|
||||||
|
.unwrap_or_else(|| quote!(__actual == #response_status));
|
||||||
let response_type = operation
|
let response_type = operation
|
||||||
.response_body
|
.response_body
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -985,7 +1041,7 @@ fn reqwest_adapter_tokens(
|
|||||||
.await
|
.await
|
||||||
.map_err(#api_crate::reqwest::ClientError::from)?;
|
.map_err(#api_crate::reqwest::ClientError::from)?;
|
||||||
let __actual = __response.status().as_u16();
|
let __actual = __response.status().as_u16();
|
||||||
if __actual == #response_status {
|
if #accepted_status {
|
||||||
#success
|
#success
|
||||||
}
|
}
|
||||||
#public_error
|
#public_error
|
||||||
@@ -1181,6 +1237,14 @@ fn axum_adapter_tokens(
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
let extension_extractors = operation.parameters.iter().filter_map(|parameter| {
|
||||||
|
if !matches!(parameter.location, Location::Extension) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ident = ¶meter.rust_ident;
|
||||||
|
let ty = ¶meter.ty;
|
||||||
|
Some(quote!(#api_crate::axum::framework::Extension(#ident): #api_crate::axum::framework::Extension<#ty>,))
|
||||||
|
});
|
||||||
let body_extractor = operation.parameters.iter().find_map(|parameter| {
|
let body_extractor = operation.parameters.iter().find_map(|parameter| {
|
||||||
if !matches!(parameter.location, Location::Body) {
|
if !matches!(parameter.location, Location::Body) {
|
||||||
return None;
|
return None;
|
||||||
@@ -1191,7 +1255,16 @@ fn axum_adapter_tokens(
|
|||||||
});
|
});
|
||||||
let call_arguments = operation.parameters.iter().map(|parameter| ¶meter.rust_ident);
|
let call_arguments = operation.parameters.iter().map(|parameter| ¶meter.rust_ident);
|
||||||
let response_status = operation.response_status;
|
let response_status = operation.response_status;
|
||||||
let success = if operation.response_body.is_some() {
|
let success = if let Some(alternate_status) = operation.alternate_status {
|
||||||
|
quote!({
|
||||||
|
let status = #api_crate::HttpSuccess::status_code(&value);
|
||||||
|
if status == #response_status || status == #alternate_status {
|
||||||
|
#api_crate::axum::json_response(#api_crate::axum::status(status), value)
|
||||||
|
} else {
|
||||||
|
#api_crate::axum::empty_response(#api_crate::axum::status(500))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if operation.response_body.is_some() {
|
||||||
quote!(#api_crate::axum::json_response(#api_crate::axum::status(#response_status), value))
|
quote!(#api_crate::axum::json_response(#api_crate::axum::status(#response_status), value))
|
||||||
} else {
|
} else {
|
||||||
quote!({ let _ = value; #api_crate::axum::empty_response(#api_crate::axum::status(#response_status)) })
|
quote!({ let _ = value; #api_crate::axum::empty_response(#api_crate::axum::status(#response_status)) })
|
||||||
@@ -1232,6 +1305,7 @@ fn axum_adapter_tokens(
|
|||||||
#path_extractor
|
#path_extractor
|
||||||
#(#query_extractors)*
|
#(#query_extractors)*
|
||||||
#header_extractor
|
#header_extractor
|
||||||
|
#(#extension_extractors)*
|
||||||
#body_extractor
|
#body_extractor
|
||||||
) -> #api_crate::axum::framework::Response
|
) -> #api_crate::axum::framework::Response
|
||||||
where
|
where
|
||||||
@@ -1285,12 +1359,16 @@ fn openapi_adapter_tokens(
|
|||||||
to_snake_case(&api.item.ident),
|
to_snake_case(&api.item.ident),
|
||||||
span = api.item.ident.span()
|
span = api.item.ident.span()
|
||||||
);
|
);
|
||||||
let operations = api.operations.iter().map(|operation| {
|
let operations = api
|
||||||
|
.operations
|
||||||
|
.iter()
|
||||||
|
.filter(|operation| !operation.openapi_skip)
|
||||||
|
.map(|operation| {
|
||||||
let method = method_name(operation.method);
|
let method = method_name(operation.method);
|
||||||
let path = &operation.path;
|
let path = &operation.path;
|
||||||
let operation_id = &operation.operation_id;
|
let operation_id = &operation.operation_id;
|
||||||
let parameters = operation.parameters.iter().filter_map(|parameter| {
|
let parameters = operation.parameters.iter().filter_map(|parameter| {
|
||||||
if matches!(parameter.location, Location::Body) {
|
if matches!(parameter.location, Location::Body | Location::Extension) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let ty = ¶meter.ty;
|
let ty = ¶meter.ty;
|
||||||
@@ -1299,7 +1377,7 @@ fn openapi_adapter_tokens(
|
|||||||
Location::Path => "path",
|
Location::Path => "path",
|
||||||
Location::Query => "query",
|
Location::Query => "query",
|
||||||
Location::Header => "header",
|
Location::Header => "header",
|
||||||
Location::Body => unreachable!(),
|
Location::Body | Location::Extension => unreachable!(),
|
||||||
};
|
};
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
operation.parameter::<#ty>(#name, #location)?;
|
operation.parameter::<#ty>(#name, #location)?;
|
||||||
@@ -1319,6 +1397,16 @@ fn openapi_adapter_tokens(
|
|||||||
operation.empty_response(#status, "Successful response")?;
|
operation.empty_response(#status, "Successful response")?;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
let alternate_response = operation.alternate_status.map(|status| {
|
||||||
|
match operation.response_body.as_ref() {
|
||||||
|
Some(ty) => quote! {
|
||||||
|
operation.response::<#ty>(#status, "application/json", "Alternate successful response")?;
|
||||||
|
},
|
||||||
|
None => quote! {
|
||||||
|
operation.empty_response(#status, "Alternate successful response")?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
let error = operation
|
let error = operation
|
||||||
.error_status
|
.error_status
|
||||||
.zip(operation.error_body.as_ref())
|
.zip(operation.error_body.as_ref())
|
||||||
@@ -1334,6 +1422,7 @@ fn openapi_adapter_tokens(
|
|||||||
#(#parameters)*
|
#(#parameters)*
|
||||||
#request
|
#request
|
||||||
#response
|
#response
|
||||||
|
#alternate_response
|
||||||
#error
|
#error
|
||||||
operation.finish()?;
|
operation.finish()?;
|
||||||
}
|
}
|
||||||
@@ -1383,7 +1472,13 @@ fn expand_api(api: ApiDefinition) -> syn::Result<proc_macro2::TokenStream> {
|
|||||||
.map(|operation| {
|
.map(|operation| {
|
||||||
let marker_ident = &operation.marker_ident;
|
let marker_ident = &operation.marker_ident;
|
||||||
let metadata = metadata_tokens(operation, &api_crate);
|
let metadata = metadata_tokens(operation, &api_crate);
|
||||||
let parameter_types = operation.parameters.iter().map(|parameter| ¶meter.ty);
|
let parameter_types = operation.parameters.iter().filter_map(|parameter| {
|
||||||
|
if matches!(parameter.location, Location::Extension) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(¶meter.ty)
|
||||||
|
}
|
||||||
|
});
|
||||||
let request_type = operation
|
let request_type = operation
|
||||||
.request_body
|
.request_body
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1472,17 +1567,20 @@ fn metadata_tokens(
|
|||||||
},
|
},
|
||||||
None => quote!(::core::option::Option::None),
|
None => quote!(::core::option::Option::None),
|
||||||
};
|
};
|
||||||
let parameters = operation.parameters.iter().map(|parameter| {
|
let parameters = operation.parameters.iter().filter_map(|parameter| {
|
||||||
|
if matches!(parameter.location, Location::Extension) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let rust_name = ¶meter.rust_name;
|
let rust_name = ¶meter.rust_name;
|
||||||
let wire_name = ¶meter.wire_name;
|
let wire_name = ¶meter.wire_name;
|
||||||
let location = parameter.location.tokens(api_crate);
|
let location = parameter.location.tokens(api_crate);
|
||||||
quote! {
|
Some(quote! {
|
||||||
#api_crate::ParameterMetadata {
|
#api_crate::ParameterMetadata {
|
||||||
rust_name: #rust_name,
|
rust_name: #rust_name,
|
||||||
wire_name: #wire_name,
|
wire_name: #wire_name,
|
||||||
location: #location,
|
location: #location,
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ pub fn status(code: u16) -> StatusCode {
|
|||||||
pub mod framework {
|
pub mod framework {
|
||||||
pub use axum::{
|
pub use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::{Path, Query, State},
|
extract::{Extension, Path, Query, State},
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
response::Response,
|
response::Response,
|
||||||
routing::{delete, get, head, options, patch, post, put},
|
routing::{delete, get, head, options, patch, post, put},
|
||||||
|
|||||||
@@ -38,13 +38,17 @@
|
|||||||
//! `options`. The first argument is a path template. `operation_id` defaults to the Rust
|
//! `options`. The first argument is a path template. `operation_id` defaults to the Rust
|
||||||
//! method name but should be set explicitly for contracts which must remain stable while Rust
|
//! method name but should be set explicitly for contracts which must remain stable while Rust
|
||||||
//! names evolve. Success status defaults to `200`, except an empty (`()`) response defaults to
|
//! names evolve. Success status defaults to `200`, except an empty (`()`) response defaults to
|
||||||
//! `204`. A public error inferred from `Result<T, E>` defaults to status `400`.
|
//! `204`. `alternate_status` opts a JSON response into [`HttpSuccess`] when two successful HTTP
|
||||||
|
//! outcomes share one schema. A public error inferred from `Result<T, E>` defaults to status `400`.
|
||||||
//!
|
//!
|
||||||
//! Arguments are classified with `#[body]`, `#[query]`, `#[header]`, or `#[path]`. An
|
//! Arguments are classified with `#[body]`, `#[query]`, `#[header]`, `#[path]`, or `#[extension]`.
|
||||||
//! unannotated argument whose Rust name occurs in the route template is inferred as a path
|
//! Extension values are trusted server-local Axum context: generated clients and OpenAPI omit them.
|
||||||
|
//! An unannotated argument whose Rust name occurs in the route template is inferred as a path
|
||||||
//! argument. Header attributes may carry a wire name, as in `#[header("x-request-id")]`.
|
//! argument. Header attributes may carry a wire name, as in `#[header("x-request-id")]`.
|
||||||
//! Exactly one JSON body is allowed. JSON request, response, and error bodies must be named
|
//! Exactly one JSON body is allowed. JSON request, response, and error bodies must be named
|
||||||
//! Rust types; tuples, references, arrays, and other anonymous structural types are rejected.
|
//! Rust types; tuples, references, arrays, and other anonymous structural types are rejected.
|
||||||
|
//! `openapi = false` explicitly excludes an operation whose wire body cannot yet satisfy the
|
||||||
|
//! strict OpenAPI schema boundary; Reqwest and Axum adapters are still generated.
|
||||||
//!
|
//!
|
||||||
//! # Generated names
|
//! # Generated names
|
||||||
//!
|
//!
|
||||||
@@ -200,6 +204,13 @@ pub trait HttpError {
|
|||||||
fn status_code(&self) -> u16;
|
fn status_code(&self) -> u16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Successful JSON response that selects one of an operation's declared success statuses.
|
||||||
|
///
|
||||||
|
/// Operations opt in with `alternate_status`; ordinary one-status operations remain fully static.
|
||||||
|
pub trait HttpSuccess {
|
||||||
|
fn status_code(&self) -> u16;
|
||||||
|
}
|
||||||
|
|
||||||
/// Marker used when an operation has no request, response, or public error body.
|
/// Marker used when an operation has no request, response, or public error body.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum NoBody {}
|
pub enum NoBody {}
|
||||||
|
|||||||
@@ -192,6 +192,10 @@ impl BackendApiClient {
|
|||||||
format!("Bearer {}", self.access_token.0)
|
format!("Bearer {}", self.access_token.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn asynchronous_client(&self) -> reqwest::Client {
|
||||||
|
self.asynchronous.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn require_success(
|
pub async fn require_success(
|
||||||
&self,
|
&self,
|
||||||
response: reqwest::Response,
|
response: reqwest::Response,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use crate::{BackendApiClient, BackendApiClientError};
|
use crate::{BackendApiClient, BackendApiClientError};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use server_api::{
|
use server_api::{
|
||||||
InitialRepositoryIntent, RepositoryListResponse, RepositorySummary,
|
InitialRepositoryIntent, RepositorySummary, WorkspaceCatalogListResponse,
|
||||||
WorkspaceCatalogListResponse, WorkspaceCreateRequest, WorkspaceCreateResponse,
|
WorkspaceCreateRequest, WorkspaceCreateResponse, WorkspaceSummary,
|
||||||
WorkspaceSummary,
|
|
||||||
};
|
};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
@@ -32,6 +31,7 @@ pub enum BackendWorkspaceClientError {
|
|||||||
InvalidTarget(String),
|
InvalidTarget(String),
|
||||||
Api(BackendApiClientError),
|
Api(BackendApiClientError),
|
||||||
Http(reqwest::Error),
|
Http(reqwest::Error),
|
||||||
|
RepositoryApi(server_api::client_support::ClientError<server_api::RepositoryApiError>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for BackendWorkspaceClientError {
|
impl fmt::Display for BackendWorkspaceClientError {
|
||||||
@@ -40,6 +40,7 @@ impl fmt::Display for BackendWorkspaceClientError {
|
|||||||
Self::InvalidTarget(message) => f.write_str(message),
|
Self::InvalidTarget(message) => f.write_str(message),
|
||||||
Self::Api(error) => write!(f, "{error}"),
|
Self::Api(error) => write!(f, "{error}"),
|
||||||
Self::Http(error) => write!(f, "{error}"),
|
Self::Http(error) => write!(f, "{error}"),
|
||||||
|
Self::RepositoryApi(error) => write!(f, "{error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +59,39 @@ impl From<reqwest::Error> for BackendWorkspaceClientError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<server_api::client_support::ClientError<server_api::RepositoryApiError>>
|
||||||
|
for BackendWorkspaceClientError
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
error: server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
||||||
|
) -> Self {
|
||||||
|
Self::RepositoryApi(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RepositoryBearerAuthorizer {
|
||||||
|
authorization: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl server_api::client_support::RequestAuthorizer for RepositoryBearerAuthorizer {
|
||||||
|
fn authorize(
|
||||||
|
&self,
|
||||||
|
_request: server_api::client_support::AuthorizerRequest<'_>,
|
||||||
|
) -> Result<reqwest::header::HeaderMap, server_api::client_support::AuthorizationError> {
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
self.authorization
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| server_api::client_support::AuthorizationError::new())?,
|
||||||
|
);
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPOSITORY_RESPONSE_LIMIT: usize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
pub fn list_backend_workspaces_blocking(
|
pub fn list_backend_workspaces_blocking(
|
||||||
target: &BackendWorkspaceCatalogTarget,
|
target: &BackendWorkspaceCatalogTarget,
|
||||||
) -> Result<Vec<BackendWorkspace>, BackendWorkspaceClientError> {
|
) -> Result<Vec<BackendWorkspace>, BackendWorkspaceClientError> {
|
||||||
@@ -75,6 +109,21 @@ pub fn list_backend_workspaces_blocking(
|
|||||||
pub fn list_backend_workspace_repositories_blocking(
|
pub fn list_backend_workspace_repositories_blocking(
|
||||||
target: &BackendWorkspaceCatalogTarget,
|
target: &BackendWorkspaceCatalogTarget,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
|
) -> Result<Vec<RepositorySummary>, BackendWorkspaceClientError> {
|
||||||
|
let client = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?;
|
||||||
|
runtime.block_on(list_backend_workspace_repositories_with_client(
|
||||||
|
&client,
|
||||||
|
workspace_id,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_backend_workspace_repositories_with_client(
|
||||||
|
backend: &BackendApiClient,
|
||||||
|
workspace_id: &str,
|
||||||
) -> Result<Vec<RepositorySummary>, BackendWorkspaceClientError> {
|
) -> Result<Vec<RepositorySummary>, BackendWorkspaceClientError> {
|
||||||
if workspace_id.is_empty()
|
if workspace_id.is_empty()
|
||||||
|| workspace_id.len() > 200
|
|| workspace_id.len() > 200
|
||||||
@@ -86,12 +135,16 @@ pub fn list_backend_workspace_repositories_blocking(
|
|||||||
"Workspace id returned by Backend is invalid".to_string(),
|
"Workspace id returned by Backend is invalid".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let client = BackendApiClient::from_stored_token(&target.base_url)?;
|
let client = server_api::ServerApiClient::builder(backend.origin().as_str())
|
||||||
let response = client
|
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?
|
||||||
.blocking_request(Method::GET, &format!("/api/w/{workspace_id}/repositories"))?
|
.client(backend.asynchronous_client())
|
||||||
.send()?;
|
.authorizer(RepositoryBearerAuthorizer {
|
||||||
client.check_status(response.status())?;
|
authorization: backend.authorization_header_value(),
|
||||||
let response = response.json::<RepositoryListResponse>()?;
|
})
|
||||||
|
.response_body_limit(REPOSITORY_RESPONSE_LIMIT)
|
||||||
|
.build()
|
||||||
|
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?;
|
||||||
|
let response = client.repository_list(workspace_id.to_owned()).await?;
|
||||||
if response.workspace_id != workspace_id {
|
if response.workspace_id != workspace_id {
|
||||||
return Err(BackendWorkspaceClientError::InvalidTarget(
|
return Err(BackendWorkspaceClientError::InvalidTarget(
|
||||||
"Repository catalog response does not match the requested Workspace".to_string(),
|
"Repository catalog response does not match the requested Workspace".to_string(),
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ default = []
|
|||||||
typescript = ["dep:ts-rs", "protocol/typescript"]
|
typescript = ["dep:ts-rs", "protocol/typescript"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
api-macros = { workspace = true, features = ["reqwest", "axum"] }
|
api-macros = { workspace = true, features = ["reqwest", "axum", "openapi"] }
|
||||||
protocol.workspace = true
|
protocol.workspace = true
|
||||||
runtime-api.workspace = true
|
runtime-api.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
sha2.workspace = true
|
||||||
ts-rs = { version = "12.0.1", optional = true }
|
ts-rs = { version = "12.0.1", optional = true }
|
||||||
webauthn-rs-proto = "0.5.5"
|
webauthn-rs-proto = "0.5.5"
|
||||||
workdir.workspace = true
|
workdir.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use std::{env, fs, path::PathBuf};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let output = env::args_os()
|
||||||
|
.nth(1)
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| PathBuf::from("openapi/server-api.json"));
|
||||||
|
let document = server_api::canonical_openapi_document()?;
|
||||||
|
let json = document.to_json()?;
|
||||||
|
if let Some(parent) = output.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
fs::write(&output, json)?;
|
||||||
|
println!("wrote {}", output.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+183
-15
@@ -8,7 +8,9 @@ use api_macros::api;
|
|||||||
pub use api_macros::axum as server_support;
|
pub use api_macros::axum as server_support;
|
||||||
pub use api_macros::reqwest as client_support;
|
pub use api_macros::reqwest as client_support;
|
||||||
pub use api_macros::{ApiContract, HttpMethod};
|
pub use api_macros::{ApiContract, HttpMethod};
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use webauthn_rs_proto::{
|
use webauthn_rs_proto::{
|
||||||
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
|
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
|
||||||
RequestChallengeResponse,
|
RequestChallengeResponse,
|
||||||
@@ -17,6 +19,57 @@ use webauthn_rs_proto::{
|
|||||||
pub type ServerApiError = runtime_api::RuntimeApiError;
|
pub type ServerApiError = runtime_api::RuntimeApiError;
|
||||||
pub type ServerApiClientError = client_support::ClientError<ServerApiError>;
|
pub type ServerApiClientError = client_support::ClientError<ServerApiError>;
|
||||||
|
|
||||||
|
/// Error body shared by the existing repository routes and their generated adapters.
|
||||||
|
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
|
||||||
|
pub struct RepositoryApiError {
|
||||||
|
pub error: String,
|
||||||
|
pub message: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
#[serde(skip, default = "default_repository_error_status")]
|
||||||
|
status: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_repository_error_status() -> u16 {
|
||||||
|
400
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepositoryApiError {
|
||||||
|
pub fn new(
|
||||||
|
status: u16,
|
||||||
|
error: impl Into<String>,
|
||||||
|
message: impl Into<String>,
|
||||||
|
diagnostics: Vec<Diagnostic>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
error: error.into(),
|
||||||
|
message: message.into(),
|
||||||
|
diagnostics,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl api_macros::HttpError for RepositoryApiError {
|
||||||
|
fn status_code(&self) -> u16 {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_openapi_schema {
|
||||||
|
($($ty:ty),+ $(,)?) => {
|
||||||
|
$(impl api_macros::openapi::OpenApiSchema for $ty {})+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_openapi_schema!(
|
||||||
|
CreateWorkspaceRepositoryRequest,
|
||||||
|
CreateWorkspaceRepositoryResponse,
|
||||||
|
RepositoryApiError,
|
||||||
|
RepositoryListResponse,
|
||||||
|
RepositoryDetailResponse,
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct WorkspaceWorkerSessionResponse {
|
pub struct WorkspaceWorkerSessionResponse {
|
||||||
pub subject: WorkspaceWorkerSubject,
|
pub subject: WorkspaceWorkerSubject,
|
||||||
@@ -24,12 +77,13 @@ pub struct WorkspaceWorkerSessionResponse {
|
|||||||
pub observation: runtime_api::WorkerSessionAvailability,
|
pub observation: runtime_api::WorkerSessionAvailability,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(reqwest, axum)]
|
#[api(reqwest, axum, openapi)]
|
||||||
pub trait ServerApi {
|
pub trait ServerApi {
|
||||||
#[get(
|
#[get(
|
||||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/session",
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/session",
|
||||||
status = 200,
|
status = 200,
|
||||||
error_status = 400
|
error_status = 400,
|
||||||
|
openapi = false
|
||||||
)]
|
)]
|
||||||
async fn worker_session(
|
async fn worker_session(
|
||||||
&self,
|
&self,
|
||||||
@@ -37,6 +91,59 @@ pub trait ServerApi {
|
|||||||
#[path] runtime_id: String,
|
#[path] runtime_id: String,
|
||||||
#[path] worker_id: String,
|
#[path] worker_id: String,
|
||||||
) -> Result<WorkspaceWorkerSessionResponse, ServerApiError>;
|
) -> Result<WorkspaceWorkerSessionResponse, ServerApiError>;
|
||||||
|
|
||||||
|
#[get("/api/w/{workspace_id}/repositories", status = 200, error_status = 400)]
|
||||||
|
async fn repository_list(
|
||||||
|
&self,
|
||||||
|
#[path] workspace_id: String,
|
||||||
|
) -> Result<RepositoryListResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[get(
|
||||||
|
"/api/w/{workspace_id}/repositories/{repository_key}",
|
||||||
|
status = 200,
|
||||||
|
error_status = 404
|
||||||
|
)]
|
||||||
|
async fn repository_detail(
|
||||||
|
&self,
|
||||||
|
#[path] workspace_id: String,
|
||||||
|
#[path] repository_key: String,
|
||||||
|
) -> Result<RepositoryDetailResponse, RepositoryApiError>;
|
||||||
|
|
||||||
|
#[post(
|
||||||
|
"/api/w/{workspace_id}/repositories",
|
||||||
|
status = 201,
|
||||||
|
alternate_status = 200,
|
||||||
|
error_status = 400
|
||||||
|
)]
|
||||||
|
async fn repository_create(
|
||||||
|
&self,
|
||||||
|
#[extension] actor: RequestActor,
|
||||||
|
#[path] workspace_id: String,
|
||||||
|
#[body] request: CreateWorkspaceRepositoryRequest,
|
||||||
|
) -> Result<CreateWorkspaceRepositoryResponse, RepositoryApiError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Digest of the authoritative ServerApi Rust contract source used for artifact provenance.
|
||||||
|
pub fn canonical_openapi_source_digest() -> String {
|
||||||
|
let digest = Sha256::digest(include_bytes!("lib.rs"));
|
||||||
|
let mut encoded = String::with_capacity("sha256:".len() + digest.len() * 2);
|
||||||
|
encoded.push_str("sha256:");
|
||||||
|
for byte in digest {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
write!(&mut encoded, "{byte:02x}").expect("writing to String cannot fail");
|
||||||
|
}
|
||||||
|
encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the deployment-independent canonical ServerApi OpenAPI document.
|
||||||
|
pub fn canonical_openapi_document()
|
||||||
|
-> Result<api_macros::openapi::OpenApiDocument, api_macros::openapi::OpenApiError> {
|
||||||
|
let source_digest = canonical_openapi_source_digest();
|
||||||
|
server_api_openapi(api_macros::openapi::OpenApiInfo {
|
||||||
|
title: "Yoi Server API",
|
||||||
|
version: env!("CARGO_PKG_VERSION"),
|
||||||
|
source_digest: &source_digest,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public browser-authentication configuration.
|
/// Public browser-authentication configuration.
|
||||||
@@ -394,7 +501,7 @@ pub fn validate_repository_key(value: &str) -> Result<(), RepositoryKeyError> {
|
|||||||
///
|
///
|
||||||
/// Local paths remain distinct from network Git transports so callers cannot
|
/// Local paths remain distinct from network Git transports so callers cannot
|
||||||
/// accidentally treat an unmaterialized remote as a server-local filesystem path.
|
/// accidentally treat an unmaterialized remote as a server-local filesystem path.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum RepositorySourceKind {
|
pub enum RepositorySourceKind {
|
||||||
@@ -451,7 +558,7 @@ impl<'de> Deserialize<'de> for RepositorySourceKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stable Repository source identity stored by Workspace authority.
|
/// Stable Repository source identity stored by Workspace authority.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
pub struct RepositorySource {
|
pub struct RepositorySource {
|
||||||
pub kind: RepositorySourceKind,
|
pub kind: RepositorySourceKind,
|
||||||
@@ -464,7 +571,7 @@ pub struct RepositorySource {
|
|||||||
///
|
///
|
||||||
/// The Server parses and canonicalizes `source`; callers cannot assert a
|
/// The Server parses and canonicalizes `source`; callers cannot assert a
|
||||||
/// transport classification or supply credential material through this DTO.
|
/// transport classification or supply credential material through this DTO.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct CreateWorkspaceRepositoryRequest {
|
pub struct CreateWorkspaceRepositoryRequest {
|
||||||
pub repository_key: String,
|
pub repository_key: String,
|
||||||
@@ -473,14 +580,20 @@ pub struct CreateWorkspaceRepositoryRequest {
|
|||||||
pub default_ref: Option<String>,
|
pub default_ref: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
pub struct CreateWorkspaceRepositoryResponse {
|
pub struct CreateWorkspaceRepositoryResponse {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
pub repository_key: String,
|
pub repository_key: String,
|
||||||
pub replayed: bool,
|
pub replayed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
impl api_macros::HttpSuccess for CreateWorkspaceRepositoryResponse {
|
||||||
|
fn status_code(&self) -> u16 {
|
||||||
|
if self.replayed { 200 } else { 201 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum RepositoryObservedStatus {
|
pub enum RepositoryObservedStatus {
|
||||||
@@ -1125,7 +1238,7 @@ pub enum WorkspaceProfileSourceProvenance {
|
|||||||
ProjectProfileSourceTree,
|
ProjectProfileSourceTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct RepositoryDiagnostic {
|
pub struct RepositoryDiagnostic {
|
||||||
@@ -1134,7 +1247,7 @@ pub struct RepositoryDiagnostic {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct GitRemoteSummary {
|
pub struct GitRemoteSummary {
|
||||||
@@ -1142,7 +1255,7 @@ pub struct GitRemoteSummary {
|
|||||||
pub fetch_url: String,
|
pub fetch_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct GitRepositorySummary {
|
pub struct GitRepositorySummary {
|
||||||
@@ -1153,7 +1266,7 @@ pub struct GitRepositorySummary {
|
|||||||
pub remotes: Vec<GitRemoteSummary>,
|
pub remotes: Vec<GitRemoteSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct RepositorySummary {
|
pub struct RepositorySummary {
|
||||||
@@ -1162,6 +1275,7 @@ pub struct RepositorySummary {
|
|||||||
pub provider: String,
|
pub provider: String,
|
||||||
pub source: RepositorySource,
|
pub source: RepositorySource,
|
||||||
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
#[cfg_attr(feature = "typescript", ts(type = "number"))]
|
||||||
|
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
|
||||||
pub source_revision: u64,
|
pub source_revision: u64,
|
||||||
pub source_fingerprint: String,
|
pub source_fingerprint: String,
|
||||||
pub observed_status: RepositoryObservedStatus,
|
pub observed_status: RepositoryObservedStatus,
|
||||||
@@ -1194,7 +1308,7 @@ pub struct GitCommitSummary {
|
|||||||
pub refs: Vec<String>,
|
pub refs: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct RepositoryListResponse {
|
pub struct RepositoryListResponse {
|
||||||
@@ -1204,7 +1318,7 @@ pub struct RepositoryListResponse {
|
|||||||
pub diagnostics: Vec<Diagnostic>,
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct RepositoryDetailResponse {
|
pub struct RepositoryDetailResponse {
|
||||||
@@ -1282,7 +1396,7 @@ pub struct RepositoryLogResponse {
|
|||||||
pub const TICKET_RELATIONS_QUERY_PATH: &str = "/tickets/relations/search";
|
pub const TICKET_RELATIONS_QUERY_PATH: &str = "/tickets/relations/search";
|
||||||
pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search";
|
pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum DiagnosticSeverity {
|
pub enum DiagnosticSeverity {
|
||||||
@@ -1291,7 +1405,7 @@ pub enum DiagnosticSeverity {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Diagnostic {
|
pub struct Diagnostic {
|
||||||
@@ -4985,6 +5099,60 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod openapi_artifact_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonical_openapi_artifact_is_current() {
|
||||||
|
let generated = canonical_openapi_document()
|
||||||
|
.expect("canonical OpenAPI contract must be valid")
|
||||||
|
.to_json()
|
||||||
|
.expect("canonical OpenAPI contract must serialize");
|
||||||
|
let checked_in = include_str!("../../../openapi/server-api.json");
|
||||||
|
assert_eq!(
|
||||||
|
generated, checked_in,
|
||||||
|
"regenerate with `cargo run -p server-api --example export_openapi -- openapi/server-api.json`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repository_operations_are_in_the_canonical_contract() {
|
||||||
|
let document = canonical_openapi_document().expect("canonical OpenAPI contract must build");
|
||||||
|
let value: serde_json::Value =
|
||||||
|
serde_json::from_str(&document.to_json().expect("document must serialize"))
|
||||||
|
.expect("document must be JSON");
|
||||||
|
|
||||||
|
assert_eq!(value["openapi"], "3.1.0");
|
||||||
|
assert!(
|
||||||
|
value["paths"]
|
||||||
|
["/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/session"]
|
||||||
|
.is_null()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value["info"]["x-yoi-source-digest"]
|
||||||
|
.as_str()
|
||||||
|
.is_some_and(|digest| digest.starts_with("sha256:"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
value["paths"]["/api/w/{workspace_id}/repositories"]["get"]["responses"]["200"]["content"]
|
||||||
|
["application/json"]["schema"]["$ref"],
|
||||||
|
"#/components/schemas/RepositoryListResponse"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value["paths"]["/api/w/{workspace_id}/repositories"]["post"]["responses"]["200"]
|
||||||
|
.is_object()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value["paths"]["/api/w/{workspace_id}/repositories"]["post"]["responses"]["201"]
|
||||||
|
.is_object()
|
||||||
|
);
|
||||||
|
assert!(value["paths"]["/api/w/{workspace_id}/repositories/{repository_key}"]
|
||||||
|
["get"]["responses"]["404"]
|
||||||
|
.is_object());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "typescript"))]
|
#[cfg(all(test, feature = "typescript"))]
|
||||||
mod typescript_tests {
|
mod typescript_tests {
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -3074,6 +3074,56 @@ impl server_api::ServerApi for WorkspaceApi {
|
|||||||
observation,
|
observation,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn repository_list(
|
||||||
|
&self,
|
||||||
|
workspace_id: String,
|
||||||
|
) -> std::result::Result<server_api::RepositoryListResponse, server_api::RepositoryApiError>
|
||||||
|
{
|
||||||
|
scoped_list_repositories(
|
||||||
|
State(self.clone()),
|
||||||
|
AxumPath(ScopedWorkspacePath { workspace_id }),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|Json(response)| response)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn repository_detail(
|
||||||
|
&self,
|
||||||
|
workspace_id: String,
|
||||||
|
repository_key: String,
|
||||||
|
) -> std::result::Result<server_api::RepositoryDetailResponse, server_api::RepositoryApiError>
|
||||||
|
{
|
||||||
|
scoped_repository_detail(
|
||||||
|
State(self.clone()),
|
||||||
|
AxumPath(ScopedRepositoryPath {
|
||||||
|
workspace_id,
|
||||||
|
repository_key,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|Json(response)| response)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn repository_create(
|
||||||
|
&self,
|
||||||
|
actor: RequestActor,
|
||||||
|
workspace_id: String,
|
||||||
|
request: CreateWorkspaceRepositoryRequest,
|
||||||
|
) -> std::result::Result<CreateWorkspaceRepositoryResponse, server_api::RepositoryApiError>
|
||||||
|
{
|
||||||
|
scoped_create_repository(
|
||||||
|
State(self.clone()),
|
||||||
|
AxumPath(ScopedWorkspacePath { workspace_id }),
|
||||||
|
Extension(actor),
|
||||||
|
Json(request),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|(_status, Json(response))| response)
|
||||||
|
.map_err(ApiError::into_repository_api_error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_inner_router(api: WorkspaceApi) -> Router {
|
fn build_inner_router(api: WorkspaceApi) -> Router {
|
||||||
@@ -3399,15 +3449,7 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
|
|||||||
delete(scoped_unlink_objective_ticket),
|
delete(scoped_unlink_objective_ticket),
|
||||||
)
|
)
|
||||||
.route("/api/repositories", get(list_repositories))
|
.route("/api/repositories", get(list_repositories))
|
||||||
.route(
|
|
||||||
"/api/w/{workspace_id}/repositories",
|
|
||||||
get(scoped_list_repositories).post(scoped_create_repository),
|
|
||||||
)
|
|
||||||
.route("/api/repositories/{repository_key}", get(repository_detail))
|
.route("/api/repositories/{repository_key}", get(repository_detail))
|
||||||
.route(
|
|
||||||
"/api/w/{workspace_id}/repositories/{repository_key}",
|
|
||||||
get(scoped_repository_detail),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test",
|
"/api/w/{workspace_id}/repositories/{repository_key}/ssh-connection-test",
|
||||||
post(scoped_probe_repository_ssh_connection)
|
post(scoped_probe_repository_ssh_connection)
|
||||||
@@ -18781,6 +18823,47 @@ impl ApiError {
|
|||||||
fn with_diagnostics(error: Error, diagnostics: Vec<RuntimeDiagnostic>) -> Self {
|
fn with_diagnostics(error: Error, diagnostics: Vec<RuntimeDiagnostic>) -> Self {
|
||||||
Self { error, diagnostics }
|
Self { error, diagnostics }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_repository_api_error(self) -> server_api::RepositoryApiError {
|
||||||
|
let status = match &self.error {
|
||||||
|
Error::WorkspacePermissionDenied(_) | Error::BrowserReopenConfirmationRequired => {
|
||||||
|
StatusCode::FORBIDDEN
|
||||||
|
}
|
||||||
|
Error::RepositoryConflict(_) => StatusCode::CONFLICT,
|
||||||
|
Error::InvalidInput(_)
|
||||||
|
| Error::WorkerSourceIdentity(_)
|
||||||
|
| Error::InvalidRuntimeIdentifier { .. }
|
||||||
|
| Error::ReservedWorkerName(_) => StatusCode::BAD_REQUEST,
|
||||||
|
Error::RuntimeOperationFailed { code, .. } if code == "repository_not_configured" => {
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
}
|
||||||
|
Error::RuntimeOperationFailed { code, .. }
|
||||||
|
if code == "repository_provider_unsupported" =>
|
||||||
|
{
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
}
|
||||||
|
Error::InvalidRecordId(_)
|
||||||
|
| Error::MissingFrontmatter(_)
|
||||||
|
| Error::UnknownRepository(_)
|
||||||
|
| Error::UnknownRuntime(_)
|
||||||
|
| Error::UnknownWorker { .. }
|
||||||
|
| Error::RuntimeBindingNotFound { .. }
|
||||||
|
| Error::UnknownHost(_)
|
||||||
|
| Error::WorkspaceIdMismatch => StatusCode::NOT_FOUND,
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
};
|
||||||
|
let message = if status == StatusCode::INTERNAL_SERVER_ERROR {
|
||||||
|
"internal server error".to_owned()
|
||||||
|
} else {
|
||||||
|
self.error.to_string()
|
||||||
|
};
|
||||||
|
server_api::RepositoryApiError::new(
|
||||||
|
status.as_u16(),
|
||||||
|
status.canonical_reason().unwrap_or("error"),
|
||||||
|
message,
|
||||||
|
working_directory_diagnostics(self.diagnostics),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for ApiError {
|
impl IntoResponse for ApiError {
|
||||||
@@ -22846,6 +22929,45 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(String::from_utf8_lossy(&listed_body).contains("documentation"));
|
assert!(String::from_utf8_lossy(&listed_body).contains("documentation"));
|
||||||
|
|
||||||
|
let repository_detail_uri = format!("{repositories_uri}/documentation");
|
||||||
|
let detailed = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri(repository_detail_uri)
|
||||||
|
.header(
|
||||||
|
axum::http::header::COOKIE,
|
||||||
|
"yoi_workspace_session=browser-session-auth",
|
||||||
|
)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(detailed.status(), StatusCode::OK);
|
||||||
|
let missing = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri(format!("{repositories_uri}/missing"))
|
||||||
|
.header(
|
||||||
|
axum::http::header::COOKIE,
|
||||||
|
"yoi_workspace_session=browser-session-auth",
|
||||||
|
)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let missing_status = missing.status();
|
||||||
|
let missing_body = to_bytes(missing.into_body(), usize::MAX).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
missing_status,
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
"{}",
|
||||||
|
String::from_utf8_lossy(&missing_body)
|
||||||
|
);
|
||||||
|
|
||||||
let identity_uri = format!(
|
let identity_uri = format!(
|
||||||
"/api/w/{}/settings/signing-identity",
|
"/api/w/{}/settings/signing-identity",
|
||||||
workspace.workspace.workspace_id
|
workspace.workspace.workspace_id
|
||||||
|
|||||||
@@ -0,0 +1,495 @@
|
|||||||
|
{
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"CreateWorkspaceRepositoryRequest": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Browser/user intent for registering one Repository in a Workspace.\n\nThe Server parses and canonicalizes `source`; callers cannot assert a\ntransport classification or supply credential material through this DTO.",
|
||||||
|
"properties": {
|
||||||
|
"default_ref": {
|
||||||
|
"default": null,
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repository_key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"repository_key",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"CreateWorkspaceRepositoryResponse": {
|
||||||
|
"properties": {
|
||||||
|
"replayed": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"repository_key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspace_id",
|
||||||
|
"repository_key",
|
||||||
|
"replayed"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"Diagnostic": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"$ref": "#/components/schemas/DiagnosticSeverity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"code",
|
||||||
|
"severity",
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"DiagnosticSeverity": {
|
||||||
|
"enum": [
|
||||||
|
"info",
|
||||||
|
"warning",
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"GitRemoteSummary": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"fetch_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"fetch_url"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"GitRepositorySummary": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"branch": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dirty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"head": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"remotes": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/GitRemoteSummary"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"dirty",
|
||||||
|
"remotes"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RepositoryApiError": {
|
||||||
|
"description": "Error body shared by the existing repository routes and their generated adapters.",
|
||||||
|
"properties": {
|
||||||
|
"diagnostics": {
|
||||||
|
"default": [],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Diagnostic"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"error",
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RepositoryDetailResponse": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"item": {
|
||||||
|
"$ref": "#/components/schemas/RepositorySummary"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspace_id",
|
||||||
|
"item",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RepositoryDiagnostic": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"severity",
|
||||||
|
"code",
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RepositoryListResponse": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"diagnostics": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Diagnostic"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/RepositorySummary"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspace_id",
|
||||||
|
"items",
|
||||||
|
"source",
|
||||||
|
"diagnostics"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RepositoryObservedStatus": {
|
||||||
|
"enum": [
|
||||||
|
"unverified",
|
||||||
|
"ready",
|
||||||
|
"invalid"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"RepositorySource": {
|
||||||
|
"description": "Stable Repository source identity stored by Workspace authority.",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"$ref": "#/components/schemas/RepositorySourceKind"
|
||||||
|
},
|
||||||
|
"uri": {
|
||||||
|
"description": "Canonical source representation. This is an absolute local path for\n`local_path`, and a normalized URI/remote specification otherwise.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"kind",
|
||||||
|
"uri"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RepositorySourceKind": {
|
||||||
|
"description": "Provider-neutral classification of an authoritative Repository source.\n\nLocal paths remain distinct from network Git transports so callers cannot\naccidentally treat an unmaterialized remote as a server-local filesystem path.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"local_path",
|
||||||
|
"file",
|
||||||
|
"ssh",
|
||||||
|
"https"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "invalid",
|
||||||
|
"description": "A legacy value that could not be classified during migration. It remains\ninspectable but every provider operation must fail closed. Historical\n`http` wire values decode into this non-executable classification.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RepositorySummary": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"default_selector": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"diagnostics": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/RepositoryDiagnostic"
|
||||||
|
},
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/GitRepositorySummary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"observed_at": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observed_status": {
|
||||||
|
"$ref": "#/components/schemas/RepositoryObservedStatus"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"record_authority": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"repository_key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/components/schemas/RepositorySource"
|
||||||
|
},
|
||||||
|
"source_fingerprint": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"source_revision": {
|
||||||
|
"format": "uint64",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"repository_key",
|
||||||
|
"kind",
|
||||||
|
"provider",
|
||||||
|
"source",
|
||||||
|
"source_revision",
|
||||||
|
"source_fingerprint",
|
||||||
|
"observed_status",
|
||||||
|
"record_authority"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"string": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"title": "Yoi Server API",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"x-yoi-source-digest": "sha256:1a30fd87bc5d6e1a03acfb86c86630ad8d458e5c0db8b3d146459d5f2c376db6"
|
||||||
|
},
|
||||||
|
"openapi": "3.1.0",
|
||||||
|
"paths": {
|
||||||
|
"/api/w/{workspace_id}/repositories": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "repository_list",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"explode": false,
|
||||||
|
"in": "path",
|
||||||
|
"name": "workspace_id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/string"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RepositoryListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Successful response"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RepositoryApiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Error response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "repository_create",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"explode": false,
|
||||||
|
"in": "path",
|
||||||
|
"name": "workspace_id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/string"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateWorkspaceRepositoryRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateWorkspaceRepositoryResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Alternate successful response"
|
||||||
|
},
|
||||||
|
"201": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateWorkspaceRepositoryResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Successful response"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RepositoryApiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Error response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/w/{workspace_id}/repositories/{repository_key}": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "repository_detail",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"explode": false,
|
||||||
|
"in": "path",
|
||||||
|
"name": "workspace_id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/string"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"explode": false,
|
||||||
|
"in": "path",
|
||||||
|
"name": "repository_key",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/string"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RepositoryDetailResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Successful response"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RepositoryApiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Error response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user