use async_trait::async_trait;
use orion_client::{ClientError, OrionClient, StatusCode};
use serde::de::DeserializeOwned;
use serde_json::Value;
pub use axum::http::Method;
#[async_trait]
pub trait AdminApi: Send + Sync {
async fn request(
&self,
method: Method,
path: &str,
body: Option<&Value>,
) -> Result<Value, ClientError>;
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, ClientError>
where
Self: Sized,
{
decode(self.request(Method::GET, path, None).await?)
}
async fn get_data<T: DeserializeOwned>(&self, path: &str) -> Result<T, ClientError>
where
Self: Sized,
{
decode(unwrap_data(self.request(Method::GET, path, None).await?))
}
async fn get_data_opt<T: DeserializeOwned>(&self, path: &str) -> Result<Option<T>, ClientError>
where
Self: Sized,
{
match self.request(Method::GET, path, None).await {
Ok(value) => decode(unwrap_data(value)).map(Some),
Err(e) if e.status() == Some(StatusCode::NOT_FOUND) => Ok(None),
Err(e) => Err(e),
}
}
async fn post_data<T: DeserializeOwned>(
&self,
path: &str,
body: &Value,
) -> Result<T, ClientError>
where
Self: Sized,
{
decode(unwrap_data(
self.request(Method::POST, path, Some(body)).await?,
))
}
async fn post_data_empty<T: DeserializeOwned>(&self, path: &str) -> Result<T, ClientError>
where
Self: Sized,
{
decode(unwrap_data(self.request(Method::POST, path, None).await?))
}
async fn put_data<T: DeserializeOwned>(
&self,
path: &str,
body: &Value,
) -> Result<T, ClientError>
where
Self: Sized,
{
decode(unwrap_data(
self.request(Method::PUT, path, Some(body)).await?,
))
}
async fn patch_data<T: DeserializeOwned>(
&self,
path: &str,
body: &Value,
) -> Result<T, ClientError>
where
Self: Sized,
{
decode(unwrap_data(
self.request(Method::PATCH, path, Some(body)).await?,
))
}
async fn delete(&self, path: &str) -> Result<(), ClientError>
where
Self: Sized,
{
self.request(Method::DELETE, path, None).await.map(|_| ())
}
}
fn unwrap_data(value: Value) -> Value {
match value {
Value::Object(mut map) if map.contains_key("data") => {
map.remove("data").unwrap_or(Value::Null)
}
other => other,
}
}
fn decode<T: DeserializeOwned>(value: Value) -> Result<T, ClientError> {
serde_json::from_value(value).map_err(|source| ClientError::Decode { source })
}
#[async_trait]
impl AdminApi for OrionClient {
async fn request(
&self,
method: Method,
path: &str,
body: Option<&Value>,
) -> Result<Value, ClientError> {
match (method, body) {
(Method::GET, _) => OrionClient::get(self, path).await,
(Method::POST, Some(body)) => OrionClient::post(self, path, body).await,
(Method::POST, None) => OrionClient::post_empty(self, path).await,
(Method::PUT, Some(body)) => OrionClient::put(self, path, body).await,
(Method::PATCH, Some(body)) => OrionClient::patch(self, path, body).await,
(Method::DELETE, _) => OrionClient::delete(self, path).await.map(|()| Value::Null),
(method, _) => unreachable!("the apply engine sends no {method} request"),
}
}
}
pub struct InProcessAdmin {
router: axum::Router,
principal: String,
change_context: String,
}
impl InProcessAdmin {
pub fn new(
state: crate::server::state::AppState,
principal: &str,
change_context: String,
) -> Self {
let admin = crate::server::routes::admin::admin_routes(
state.config.server.max_admin_body_size,
crate::server::plugin_body_size(&state.config),
);
Self {
router: axum::Router::new()
.nest("/api/v1/admin", admin)
.with_state(state),
principal: principal.to_string(),
change_context,
}
}
}
#[async_trait]
impl AdminApi for InProcessAdmin {
async fn request(
&self,
method: Method,
path: &str,
body: Option<&Value>,
) -> Result<Value, ClientError> {
use tower::ServiceExt as _;
let mut builder = axum::http::Request::builder().method(method).uri(path);
let body = match body {
Some(body) => {
builder = builder.header(axum::http::header::CONTENT_TYPE, "application/json");
axum::body::Body::from(
serde_json::to_vec(body).map_err(|source| ClientError::Decode { source })?,
)
}
None => axum::body::Body::empty(),
};
let mut request = builder
.body(body)
.expect("a method, a path and a body build");
request
.extensions_mut()
.insert(crate::server::admin_auth::AdminPrincipal {
key_id: self.principal.clone(),
});
let context = crate::request_context::RequestContext {
request_id: uuid::Uuid::new_v4().to_string(),
client_ip: String::new(),
user_agent: None,
change_context: Some(self.change_context.clone()),
};
let response = crate::request_context::REQUEST_CONTEXT
.scope(context, self.router.clone().oneshot(request))
.await
.unwrap_or_else(|never| match never {});
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.map_err(|e| ClientError::Http {
status,
body: e.to_string(),
})?;
if !status.is_success() {
return Err(ClientError::from_response(status, &bytes));
}
if bytes.is_empty() {
return Ok(Value::Null);
}
serde_json::from_slice(&bytes).map_err(|source| ClientError::Decode { source })
}
}