1use std::{collections::HashMap, sync::Arc};
5
6use axum::{
7 Json,
8 extract::{Path, Query, State},
9 http::{HeaderMap, HeaderValue, Method, StatusCode, header},
10 response::{IntoResponse, Response},
11};
12use reifydb_core::{
13 actors::server::{Operation, ServerAuthResponse, ServerLogoutResponse, ServerMessage},
14 interface::catalog::binding::{Binding, BindingFormat, BindingProtocol, HttpMethod},
15 metric::ExecutionMetrics,
16};
17use reifydb_runtime::actor::reply::reply_channel;
18use reifydb_sub_server::{
19 auth::{AuthError, extract_identity_from_auth_header},
20 binding::dispatch_binding,
21 dispatch::dispatch,
22 format::WireFormat,
23 interceptor::{Protocol, RequestContext, RequestMetadata},
24 response::{CONTENT_TYPE_FRAMES, CONTENT_TYPE_RBCF, encode_frames_rbcf, resolve_response_json},
25 wire::WireParams,
26};
27use reifydb_type::{
28 params::Params,
29 value::{Value, frame::frame::Frame, identity::IdentityId, r#type::Type},
30};
31use reifydb_wire_format::json::{to::convert_frames, types::ResponseFrame};
32use serde::{Deserialize, Serialize};
33use serde_json::to_string;
34
35use crate::{error::AppError, state::HttpServerState};
36
37#[derive(Debug, Deserialize)]
39pub struct StatementRequest {
40 pub rql: String,
42 #[serde(default)]
44 pub params: Option<WireParams>,
45}
46
47#[derive(Debug, Serialize)]
49pub struct QueryResponse {
50 pub frames: Vec<ResponseFrame>,
52}
53
54#[derive(Debug, Deserialize)]
56pub struct FormatParams {
57 #[serde(default)]
58 pub format: WireFormat,
59 pub unwrap: Option<bool>,
60}
61
62#[derive(Debug, Serialize)]
64pub struct HealthResponse {
65 pub status: &'static str,
66}
67
68pub async fn health() -> impl IntoResponse {
79 (
80 StatusCode::OK,
81 Json(HealthResponse {
82 status: "ok",
83 }),
84 )
85}
86
87#[derive(Debug, Serialize)]
89pub struct LogoutResponse {
90 pub status: String,
91}
92
93#[derive(Debug, Deserialize)]
95pub struct AuthenticateRequest {
96 pub method: String,
98 #[serde(default)]
100 pub credentials: HashMap<String, String>,
101}
102
103#[derive(Debug, Serialize)]
105pub struct AuthenticateResponse {
106 pub status: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub token: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub identity: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub challenge_id: Option<String>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub payload: Option<HashMap<String, String>>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub reason: Option<String>,
123}
124
125pub async fn handle_authenticate(
126 State(state): State<HttpServerState>,
127 Json(request): Json<AuthenticateRequest>,
128) -> Result<Response, AppError> {
129 let (reply, receiver) = reply_channel();
130 let (actor_ref, _handle) = state.spawn_actor();
131 actor_ref
132 .send(ServerMessage::Authenticate {
133 method: request.method,
134 credentials: request.credentials,
135 reply,
136 })
137 .ok()
138 .ok_or_else(|| AppError::Internal("actor mailbox closed".into()))?;
139
140 let auth_response = receiver.recv().await.map_err(|_| AppError::Internal("actor stopped".into()))?;
141
142 match auth_response {
143 ServerAuthResponse::Authenticated {
144 identity,
145 token,
146 } => Ok((
147 StatusCode::OK,
148 Json(AuthenticateResponse {
149 status: "authenticated".to_string(),
150 token: Some(token),
151 identity: Some(identity.to_string()),
152 challenge_id: None,
153 payload: None,
154 reason: None,
155 }),
156 )
157 .into_response()),
158 ServerAuthResponse::Challenge {
159 challenge_id,
160 payload,
161 } => Ok((
162 StatusCode::OK,
163 Json(AuthenticateResponse {
164 status: "challenge".to_string(),
165 token: None,
166 identity: None,
167 challenge_id: Some(challenge_id),
168 payload: Some(payload),
169 reason: None,
170 }),
171 )
172 .into_response()),
173 ServerAuthResponse::Failed {
174 reason,
175 } => Ok((
176 StatusCode::UNAUTHORIZED,
177 Json(AuthenticateResponse {
178 status: "failed".to_string(),
179 token: None,
180 identity: None,
181 challenge_id: None,
182 payload: None,
183 reason: Some(reason),
184 }),
185 )
186 .into_response()),
187 ServerAuthResponse::Error(reason) => Ok((
188 StatusCode::INTERNAL_SERVER_ERROR,
189 Json(AuthenticateResponse {
190 status: "failed".to_string(),
191 token: None,
192 identity: None,
193 challenge_id: None,
194 payload: None,
195 reason: Some(reason),
196 }),
197 )
198 .into_response()),
199 }
200}
201
202pub async fn handle_logout(State(state): State<HttpServerState>, headers: HeaderMap) -> Result<Response, AppError> {
203 let auth_header = headers.get("authorization").ok_or(AppError::Auth(AuthError::MissingCredentials))?;
204 let auth_str = auth_header.to_str().map_err(|_| AppError::Auth(AuthError::InvalidHeader))?;
205 let token = auth_str.strip_prefix("Bearer ").ok_or(AppError::Auth(AuthError::InvalidHeader))?.trim();
206
207 if token.is_empty() {
208 return Err(AppError::Auth(AuthError::InvalidToken));
209 }
210
211 let (reply, receiver) = reply_channel();
212 let (actor_ref, _handle) = state.spawn_actor();
213 actor_ref
214 .send(ServerMessage::Logout {
215 token: token.to_string(),
216 reply,
217 })
218 .ok()
219 .ok_or_else(|| AppError::Internal("actor mailbox closed".into()))?;
220
221 let logout_response = receiver.recv().await.map_err(|_| AppError::Internal("actor stopped".into()))?;
222
223 match logout_response {
224 ServerLogoutResponse::Ok => Ok((
225 StatusCode::OK,
226 Json(LogoutResponse {
227 status: "ok".to_string(),
228 }),
229 )
230 .into_response()),
231 ServerLogoutResponse::InvalidToken => Err(AppError::Auth(AuthError::InvalidToken)),
232 ServerLogoutResponse::Error(reason) => Err(AppError::Internal(reason)),
233 }
234}
235
236fn build_metadata(headers: &HeaderMap) -> RequestMetadata {
238 let mut metadata = RequestMetadata::new(Protocol::Http);
239 for (name, value) in headers.iter() {
240 if let Ok(v) = value.to_str() {
241 metadata.insert(name.as_str(), v);
242 }
243 }
244 metadata
245}
246
247pub async fn handle_query(
249 State(state): State<HttpServerState>,
250 Query(format_params): Query<FormatParams>,
251 headers: HeaderMap,
252 Json(request): Json<StatementRequest>,
253) -> Result<Response, AppError> {
254 execute_and_respond(&state, Operation::Query, &headers, request, &format_params).await
255}
256
257pub async fn handle_admin(
259 State(state): State<HttpServerState>,
260 Query(format_params): Query<FormatParams>,
261 headers: HeaderMap,
262 Json(request): Json<StatementRequest>,
263) -> Result<Response, AppError> {
264 execute_and_respond(&state, Operation::Admin, &headers, request, &format_params).await
265}
266
267pub async fn handle_command(
269 State(state): State<HttpServerState>,
270 Query(format_params): Query<FormatParams>,
271 headers: HeaderMap,
272 Json(request): Json<StatementRequest>,
273) -> Result<Response, AppError> {
274 execute_and_respond(&state, Operation::Command, &headers, request, &format_params).await
275}
276
277async fn execute_and_respond(
283 state: &HttpServerState,
284 operation: Operation,
285 headers: &HeaderMap,
286 request: StatementRequest,
287 format_params: &FormatParams,
288) -> Result<Response, AppError> {
289 let identity = extract_identity(state, headers)?;
290 let metadata = build_metadata(headers);
291 let params = match request.params {
292 None => Params::None,
293 Some(wp) => wp.into_params().map_err(AppError::InvalidParams)?,
294 };
295 let ctx = RequestContext {
296 identity,
297 operation,
298 rql: request.rql,
299 params,
300 metadata,
301 };
302
303 let (frames, metrics) = dispatch(state, ctx).await?;
304
305 let mut response = match format_params.format {
306 WireFormat::Rbcf => match encode_frames_rbcf(&frames) {
307 Ok(bytes) => (StatusCode::OK, [(header::CONTENT_TYPE, CONTENT_TYPE_RBCF.to_string())], bytes)
308 .into_response(),
309 Err(e) => return Err(AppError::BadRequest(format!("RBCF encode error: {}", e))),
310 },
311 WireFormat::Json => {
312 let resolved = resolve_response_json(frames, format_params.unwrap.unwrap_or(false))
313 .map_err(AppError::BadRequest)?;
314 (StatusCode::OK, [(header::CONTENT_TYPE, resolved.content_type)], resolved.body).into_response()
315 }
316 WireFormat::Frames => {
317 let body = to_string(&QueryResponse {
318 frames: convert_frames(&frames),
319 })
320 .map_err(|e| AppError::BadRequest(format!("JSON encode error: {}", e)))?;
321 (StatusCode::OK, [(header::CONTENT_TYPE, CONTENT_TYPE_FRAMES.to_string())], body)
322 .into_response()
323 }
324 };
325 insert_meta_headers(response.headers_mut(), &metrics);
326 Ok(response)
327}
328
329fn extract_identity(state: &HttpServerState, headers: &HeaderMap) -> Result<IdentityId, AppError> {
335 if let Some(auth_header) = headers.get("authorization") {
337 let auth_str = auth_header.to_str().map_err(|_| AppError::Auth(AuthError::InvalidHeader))?;
338
339 return extract_identity_from_auth_header(state.auth_service(), auth_str).map_err(AppError::Auth);
340 }
341
342 Ok(IdentityId::anonymous())
344}
345
346pub async fn handle_binding(
353 State(state): State<HttpServerState>,
354 Path(path): Path<String>,
355 method: Method,
356 Query(query_params): Query<HashMap<String, String>>,
357 headers: HeaderMap,
358) -> Result<Response, AppError> {
359 let http_method = match method.as_str() {
360 "GET" => HttpMethod::Get,
361 "POST" => HttpMethod::Post,
362 "PUT" => HttpMethod::Put,
363 "PATCH" => HttpMethod::Patch,
364 "DELETE" => HttpMethod::Delete,
365 _ => return Err(AppError::MethodNotAllowed(format!("method `{}` is not supported", method))),
366 };
367 let request_path = format!("/{}", path);
368
369 let bindings = state.engine().materialized_catalog().list_http_bindings();
371 let mut any_path_match = false;
372 let mut matched: Option<(Binding, HashMap<String, String>)> = None;
373 for b in &bindings {
374 let BindingProtocol::Http {
375 method: binding_method,
376 path: binding_path,
377 } = &b.protocol
378 else {
379 unreachable!("list_http_bindings returns only HTTP bindings")
380 };
381 if let Some(captures) = match_http_path(binding_path, &request_path) {
382 any_path_match = true;
383 if binding_method == &http_method {
384 matched = Some((b.clone(), captures));
385 break;
386 }
387 }
388 }
389 let (binding, path_captures) = match matched {
390 Some(m) => m,
391 None if any_path_match => {
392 return Err(AppError::MethodNotAllowed(format!(
393 "no binding for method `{}` at `{}`",
394 method, request_path
395 )));
396 }
397 None => return Err(AppError::NotFound(format!("no binding for `{}`", request_path))),
398 };
399
400 let procedure =
402 state.engine().materialized_catalog().find_procedure(binding.procedure_id).ok_or_else(|| {
403 AppError::Internal(format!(
404 "binding references missing procedure id {:?}",
405 binding.procedure_id
406 ))
407 })?;
408 let namespace = state.engine().materialized_catalog().find_namespace(binding.namespace).ok_or_else(|| {
409 AppError::Internal(format!("binding references missing namespace id {:?}", binding.namespace))
410 })?;
411
412 let param_names: Vec<&str> = procedure.params().iter().map(|p| p.name.as_str()).collect();
413 for key in query_params.keys() {
414 if !param_names.contains(&key.as_str()) {
415 return Err(AppError::BadRequest(format!("unknown parameter `{}`", key)));
416 }
417 if path_captures.contains_key(key) {
418 return Err(AppError::BadRequest(format!("parameter `{}` given in both path and query", key)));
419 }
420 }
421
422 let mut params: HashMap<String, Value> = HashMap::with_capacity(procedure.params().len());
423 for p in procedure.params() {
424 let raw = match path_captures.get(&p.name).or_else(|| query_params.get(&p.name)) {
425 Some(v) => v,
426 None => {
427 return Err(AppError::BadRequest(format!("missing required parameter `{}`", p.name)));
428 }
429 };
430 let value = coerce_str_to_value(raw, p.param_type.get_type()).map_err(|e| {
431 AppError::BadRequest(format!(
432 "parameter `{}`: cannot coerce `{}` to {:?}: {}",
433 p.name,
434 raw,
435 p.param_type.get_type(),
436 e
437 ))
438 })?;
439 params.insert(p.name.clone(), value);
440 }
441 let params = if params.is_empty() {
442 Params::None
443 } else {
444 Params::Named(Arc::new(params))
445 };
446
447 let identity = extract_identity(&state, &headers)?;
448 let metadata = build_metadata(&headers);
449
450 let (frames, metrics) =
451 dispatch_binding(&state, namespace.name(), procedure.name(), params, identity, metadata).await?;
452
453 let mut response = encode_binding_response(frames, binding.format)?;
454 insert_meta_headers(response.headers_mut(), &metrics);
455 Ok(response)
456}
457
458fn insert_meta_headers(headers: &mut HeaderMap, metrics: &ExecutionMetrics) {
459 headers.insert("x-fingerprint", HeaderValue::from_str(&metrics.fingerprint.to_hex()).unwrap());
460 headers.insert("x-duration", HeaderValue::from_str(&metrics.total.to_string()).unwrap());
461}
462
463fn match_http_path(template: &str, request: &str) -> Option<HashMap<String, String>> {
466 let t_segments: Vec<&str> = template.split('/').filter(|s| !s.is_empty()).collect();
467 let r_segments: Vec<&str> = request.split('/').filter(|s| !s.is_empty()).collect();
468 if t_segments.len() != r_segments.len() {
469 return None;
470 }
471 let mut captures = HashMap::new();
472 for (t, r) in t_segments.iter().zip(r_segments.iter()) {
473 if t.starts_with('{') && t.ends_with('}') {
474 let var = &t[1..t.len() - 1];
475 captures.insert(var.to_string(), r.to_string());
476 } else if t != r {
477 return None;
478 }
479 }
480 Some(captures)
481}
482
483fn coerce_str_to_value(s: &str, ty: Type) -> Result<Value, String> {
484 match ty {
485 Type::Boolean => match s {
486 "true" | "1" => Ok(Value::Boolean(true)),
487 "false" | "0" => Ok(Value::Boolean(false)),
488 _ => Err("expected `true`/`false`".into()),
489 },
490 Type::Utf8 => Ok(Value::Utf8(s.to_string())),
491 Type::Int1 => s.parse::<i8>().map(Value::Int1).map_err(|e| e.to_string()),
492 Type::Int2 => s.parse::<i16>().map(Value::Int2).map_err(|e| e.to_string()),
493 Type::Int4 => s.parse::<i32>().map(Value::Int4).map_err(|e| e.to_string()),
494 Type::Int8 => s.parse::<i64>().map(Value::Int8).map_err(|e| e.to_string()),
495 Type::Int16 => s.parse::<i128>().map(Value::Int16).map_err(|e| e.to_string()),
496 Type::Uint1 => s.parse::<u8>().map(Value::Uint1).map_err(|e| e.to_string()),
497 Type::Uint2 => s.parse::<u16>().map(Value::Uint2).map_err(|e| e.to_string()),
498 Type::Uint4 => s.parse::<u32>().map(Value::Uint4).map_err(|e| e.to_string()),
499 Type::Uint8 => s.parse::<u64>().map(Value::Uint8).map_err(|e| e.to_string()),
500 Type::Uint16 => s.parse::<u128>().map(Value::Uint16).map_err(|e| e.to_string()),
501 Type::Float4 => s
502 .parse::<f32>()
503 .map_err(|e| e.to_string())
504 .and_then(|v| v.try_into().map(Value::Float4).map_err(|_| "invalid f32".to_string())),
505 Type::Float8 => s
506 .parse::<f64>()
507 .map_err(|e| e.to_string())
508 .and_then(|v| v.try_into().map(Value::Float8).map_err(|_| "invalid f64".to_string())),
509 other => Err(format!("coercion to {:?} not supported from URL strings", other)),
510 }
511}
512
513fn encode_binding_response(frames: Vec<Frame>, format: BindingFormat) -> Result<Response, AppError> {
514 match format {
515 BindingFormat::Rbcf => match encode_frames_rbcf(&frames) {
516 Ok(bytes) => {
517 Ok((StatusCode::OK, [(header::CONTENT_TYPE, CONTENT_TYPE_RBCF.to_string())], bytes)
518 .into_response())
519 }
520 Err(e) => Err(AppError::BadRequest(format!("RBCF encode error: {}", e))),
521 },
522 BindingFormat::Json => {
523 let resolved = resolve_response_json(frames, false).map_err(AppError::BadRequest)?;
524 Ok((StatusCode::OK, [(header::CONTENT_TYPE, resolved.content_type)], resolved.body)
525 .into_response())
526 }
527 BindingFormat::Frames => Ok(Json(QueryResponse {
528 frames: convert_frames(&frames),
529 })
530 .into_response()),
531 }
532}
533
534#[cfg(test)]
535pub mod tests {
536 use serde_json::from_str;
537
538 use super::*;
539
540 #[test]
541 fn test_match_http_path_static() {
542 assert_eq!(match_http_path("/users", "/users"), Some(HashMap::new()));
543 assert_eq!(match_http_path("/users", "/other"), None);
544 }
545
546 #[test]
547 fn test_match_http_path_capture() {
548 let caps = match_http_path("/users/{id}", "/users/42").unwrap();
549 assert_eq!(caps.get("id"), Some(&"42".to_string()));
550 }
551
552 #[test]
553 fn test_match_http_path_mismatch_length() {
554 assert!(match_http_path("/users/{id}", "/users").is_none());
555 assert!(match_http_path("/users/{id}", "/users/42/extra").is_none());
556 }
557
558 #[test]
559 fn test_coerce_numeric() {
560 assert_eq!(coerce_str_to_value("42", Type::Int8).unwrap(), Value::Int8(42));
561 assert!(coerce_str_to_value("xx", Type::Int8).is_err());
562 }
563
564 #[test]
565 fn test_coerce_bool() {
566 assert_eq!(coerce_str_to_value("true", Type::Boolean).unwrap(), Value::Boolean(true));
567 assert!(coerce_str_to_value("maybe", Type::Boolean).is_err());
568 }
569
570 #[test]
571 fn test_statement_request_deserialization() {
572 let json = r#"{"rql": "SELECT 1"}"#;
573 let request: StatementRequest = from_str(json).unwrap();
574 assert_eq!(request.rql, "SELECT 1");
575 assert!(request.params.is_none());
576 }
577
578 #[test]
579 fn test_query_response_serialization() {
580 let response = QueryResponse {
581 frames: Vec::new(),
582 };
583 let json = to_string(&response).unwrap();
584 assert!(json.contains("frames"));
585 }
586
587 #[test]
588 fn test_health_response_serialization() {
589 let response = HealthResponse {
590 status: "ok",
591 };
592 let json = to_string(&response).unwrap();
593 assert_eq!(json, r#"{"status":"ok"}"#);
594 }
595}