1use crate::api::{Api, ApiError, RegisterBranch, TransView};
13use axum::extract::{Query, State};
14use axum::http::StatusCode;
15use axum::routing::{get, post};
16use axum::{Json, Router};
17use dtmrs_core::SagaStep;
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21#[derive(Clone)]
23pub struct App {
24 api: Api,
25}
26
27impl App {
28 pub fn new(api: Api) -> Self {
29 Self { api }
30 }
31}
32
33#[derive(Deserialize)]
34struct SubmitReq {
35 gid: String,
36 #[serde(default = "default_trans_type")]
37 trans_type: String,
38 #[serde(default)]
40 steps: Vec<SagaStep>,
41}
42
43#[derive(Deserialize)]
45struct PrepareReq {
46 gid: String,
47 trans_type: String,
48 #[serde(default)]
50 actions: Vec<String>,
51 #[serde(default)]
53 query_prepared: String,
54 #[serde(default)]
56 grace_secs: Option<i64>,
57}
58
59#[derive(Deserialize)]
61struct RegisterBranchReq {
62 gid: String,
63 branch_id: String,
64 #[serde(default)]
65 confirm: String,
66 #[serde(default)]
67 cancel: String,
68 #[serde(default)]
70 r#try: String,
71 #[serde(default)]
72 commit: String,
73 #[serde(default)]
74 rollback: String,
75}
76
77fn default_trans_type() -> String {
78 "saga".into()
79}
80
81#[derive(Serialize)]
82struct Reply {
83 dtm_result: &'static str,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 message: Option<String>,
86}
87
88impl Reply {
89 fn ok() -> Json<Self> {
90 Json(Self {
91 dtm_result: "SUCCESS",
92 message: None,
93 })
94 }
95 fn err(m: impl Into<String>) -> Json<Self> {
96 Json(Self {
97 dtm_result: "FAILURE",
98 message: Some(m.into()),
99 })
100 }
101}
102
103fn http_err(e: ApiError) -> (StatusCode, Json<Reply>) {
108 let code = match &e {
109 ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
110 ApiError::NotFound(_) => StatusCode::NOT_FOUND,
111 ApiError::Conflict(_) => StatusCode::OK,
112 ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
113 };
114 (code, Reply::err(e.message().to_string()))
115}
116
117fn http_result(r: Result<(), ApiError>) -> (StatusCode, Json<Reply>) {
118 match r {
119 Ok(()) => (StatusCode::OK, Reply::ok()),
120 Err(e) => http_err(e),
121 }
122}
123
124
125pub fn router(app: App) -> Router {
126 Router::new()
127 .route("/api/dtmsvr/newGid", get(new_gid))
128 .route("/api/dtmsvr/prepare", post(prepare))
129 .route("/api/dtmsvr/registerBranch", post(register_branch))
130 .route("/api/dtmsvr/submit", post(submit))
131 .route("/api/dtmsvr/abort", post(abort))
132 .route("/api/dtmsvr/retry", post(retry))
133 .route("/api/dtmsvr/query", get(query))
134 .route("/api/dtmsvr/all", get(all))
135 .route("/health", get(|| async { "ok" }))
136 .route("/", get(console))
139 .route("/console", get(console))
140 .with_state(app)
141}
142
143async fn new_gid(State(app): State<App>) -> Json<HashMap<&'static str, String>> {
144 Json(HashMap::from([("gid", app.api.new_gid())]))
145}
146
147async fn submit(State(app): State<App>, Json(req): Json<SubmitReq>) -> (StatusCode, Json<Reply>) {
148 http_result(app.api.submit(&req.gid, &req.trans_type, &req.steps).await)
149}
150
151async fn prepare(State(app): State<App>, Json(req): Json<PrepareReq>) -> (StatusCode, Json<Reply>) {
152 http_result(
153 app.api
154 .prepare(
155 &req.gid,
156 &req.trans_type,
157 &req.actions,
158 &req.query_prepared,
159 req.grace_secs,
160 )
161 .await,
162 )
163}
164
165async fn register_branch(
166 State(app): State<App>,
167 Json(req): Json<RegisterBranchReq>,
168) -> (StatusCode, Json<Reply>) {
169 http_result(
170 app.api
171 .register_branch(&RegisterBranch {
172 gid: req.gid,
173 branch_id: req.branch_id,
174 confirm: req.confirm,
175 cancel: req.cancel,
176 r#try: req.r#try,
177 commit: req.commit,
178 rollback: req.rollback,
179 })
180 .await,
181 )
182}
183
184#[derive(Deserialize)]
185struct GidQuery {
186 gid: String,
187}
188
189async fn abort(State(app): State<App>, Json(q): Json<GidQuery>) -> (StatusCode, Json<Reply>) {
190 http_result(app.api.abort(&q.gid).await)
191}
192
193async fn retry(State(app): State<App>, Json(q): Json<GidQuery>) -> (StatusCode, Json<Reply>) {
195 http_result(app.api.retry(&q.gid).await)
196}
197
198async fn console() -> axum::response::Html<&'static str> {
200 axum::response::Html(include_str!("console.html"))
201}
202
203async fn query(
204 State(app): State<App>,
205 Query(q): Query<GidQuery>,
206) -> Result<Json<TransView>, (StatusCode, Json<Reply>)> {
207 app.api.query(&q.gid).await.map(Json).map_err(http_err)
208}
209
210async fn all(State(app): State<App>) -> Json<Vec<TransView>> {
211 Json(app.api.list_recent(100).await)
212}