use crate::api::{Api, ApiError, RegisterBranch, TransView};
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use dtmrs_core::SagaStep;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone)]
pub struct App {
api: Api,
}
impl App {
pub fn new(api: Api) -> Self {
Self { api }
}
}
#[derive(Deserialize)]
struct SubmitReq {
gid: String,
#[serde(default = "default_trans_type")]
trans_type: String,
#[serde(default)]
steps: Vec<SagaStep>,
}
#[derive(Deserialize)]
struct PrepareReq {
gid: String,
trans_type: String,
#[serde(default)]
actions: Vec<String>,
#[serde(default)]
query_prepared: String,
#[serde(default)]
grace_secs: Option<i64>,
}
#[derive(Deserialize)]
struct RegisterBranchReq {
gid: String,
branch_id: String,
#[serde(default)]
confirm: String,
#[serde(default)]
cancel: String,
#[serde(default)]
r#try: String,
#[serde(default)]
commit: String,
#[serde(default)]
rollback: String,
}
fn default_trans_type() -> String {
"saga".into()
}
#[derive(Serialize)]
struct Reply {
dtm_result: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
}
impl Reply {
fn ok() -> Json<Self> {
Json(Self {
dtm_result: "SUCCESS",
message: None,
})
}
fn err(m: impl Into<String>) -> Json<Self> {
Json(Self {
dtm_result: "FAILURE",
message: Some(m.into()),
})
}
}
fn http_err(e: ApiError) -> (StatusCode, Json<Reply>) {
let code = match &e {
ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
ApiError::NotFound(_) => StatusCode::NOT_FOUND,
ApiError::Conflict(_) => StatusCode::OK,
ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
(code, Reply::err(e.message().to_string()))
}
fn http_result(r: Result<(), ApiError>) -> (StatusCode, Json<Reply>) {
match r {
Ok(()) => (StatusCode::OK, Reply::ok()),
Err(e) => http_err(e),
}
}
pub fn router(app: App) -> Router {
Router::new()
.route("/api/dtmsvr/newGid", get(new_gid))
.route("/api/dtmsvr/prepare", post(prepare))
.route("/api/dtmsvr/registerBranch", post(register_branch))
.route("/api/dtmsvr/submit", post(submit))
.route("/api/dtmsvr/abort", post(abort))
.route("/api/dtmsvr/retry", post(retry))
.route("/api/dtmsvr/query", get(query))
.route("/api/dtmsvr/all", get(all))
.route("/health", get(|| async { "ok" }))
.route("/", get(console))
.route("/console", get(console))
.with_state(app)
}
async fn new_gid(State(app): State<App>) -> Json<HashMap<&'static str, String>> {
Json(HashMap::from([("gid", app.api.new_gid())]))
}
async fn submit(State(app): State<App>, Json(req): Json<SubmitReq>) -> (StatusCode, Json<Reply>) {
http_result(app.api.submit(&req.gid, &req.trans_type, &req.steps).await)
}
async fn prepare(State(app): State<App>, Json(req): Json<PrepareReq>) -> (StatusCode, Json<Reply>) {
http_result(
app.api
.prepare(
&req.gid,
&req.trans_type,
&req.actions,
&req.query_prepared,
req.grace_secs,
)
.await,
)
}
async fn register_branch(
State(app): State<App>,
Json(req): Json<RegisterBranchReq>,
) -> (StatusCode, Json<Reply>) {
http_result(
app.api
.register_branch(&RegisterBranch {
gid: req.gid,
branch_id: req.branch_id,
confirm: req.confirm,
cancel: req.cancel,
r#try: req.r#try,
commit: req.commit,
rollback: req.rollback,
})
.await,
)
}
#[derive(Deserialize)]
struct GidQuery {
gid: String,
}
async fn abort(State(app): State<App>, Json(q): Json<GidQuery>) -> (StatusCode, Json<Reply>) {
http_result(app.api.abort(&q.gid).await)
}
async fn retry(State(app): State<App>, Json(q): Json<GidQuery>) -> (StatusCode, Json<Reply>) {
http_result(app.api.retry(&q.gid).await)
}
async fn console() -> axum::response::Html<&'static str> {
axum::response::Html(include_str!("console.html"))
}
async fn query(
State(app): State<App>,
Query(q): Query<GidQuery>,
) -> Result<Json<TransView>, (StatusCode, Json<Reply>)> {
app.api.query(&q.gid).await.map(Json).map_err(http_err)
}
async fn all(State(app): State<App>) -> Json<Vec<TransView>> {
Json(app.api.list_recent(100).await)
}