refactor: move server rs workspace entries into crates
This commit is contained in:
141
server-rs/crates/api-server/src/api_response.rs
Normal file
141
server-rs/crates/api-server/src/api_response.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use axum::Json;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
|
||||
use crate::{http_error::ApiErrorPayload, request_context::RequestContext};
|
||||
|
||||
pub const API_VERSION: &str = "2026-04-08";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ApiResponseMeta {
|
||||
#[serde(rename = "apiVersion")]
|
||||
api_version: &'static str,
|
||||
#[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
|
||||
request_id: Option<String>,
|
||||
#[serde(rename = "routeVersion")]
|
||||
route_version: &'static str,
|
||||
operation: Option<String>,
|
||||
#[serde(rename = "latencyMs")]
|
||||
latency_ms: u64,
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
// 当前阶段先把成功响应 envelope helper 准备好,后续 `/healthz` 与业务 handler 会直接复用这里的输出逻辑。
|
||||
#[allow(dead_code)]
|
||||
pub fn json_success_body<T>(request_context: Option<&RequestContext>, data: T) -> Json<Value>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
if let Some(context) = request_context {
|
||||
if context.wants_envelope() {
|
||||
return Json(json!({
|
||||
"ok": true,
|
||||
"data": data,
|
||||
"error": null,
|
||||
"meta": build_api_response_meta(Some(context)),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Json(serde_json::to_value(data).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
pub fn json_error_body(
|
||||
request_context: Option<&RequestContext>,
|
||||
error: &ApiErrorPayload,
|
||||
) -> Json<Value> {
|
||||
let meta = build_api_response_meta(request_context);
|
||||
|
||||
if request_context.is_some_and(RequestContext::wants_envelope) {
|
||||
return Json(json!({
|
||||
"ok": false,
|
||||
"data": null,
|
||||
"error": error,
|
||||
"meta": meta,
|
||||
}));
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
"error": error,
|
||||
"meta": meta,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_api_response_meta(request_context: Option<&RequestContext>) -> ApiResponseMeta {
|
||||
ApiResponseMeta {
|
||||
api_version: API_VERSION,
|
||||
request_id: request_context.map(|context| context.request_id().to_string()),
|
||||
route_version: API_VERSION,
|
||||
operation: request_context.map(|context| context.operation().to_string()),
|
||||
latency_ms: request_context
|
||||
.map(RequestContext::elapsed)
|
||||
.unwrap_or_default(),
|
||||
timestamp: OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::request_context::RequestContext;
|
||||
use std::time::Duration;
|
||||
|
||||
fn build_request_context(wants_envelope: bool) -> RequestContext {
|
||||
RequestContext::new(
|
||||
"req-test".to_string(),
|
||||
"GET /test".to_string(),
|
||||
Duration::from_millis(12),
|
||||
wants_envelope,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_body_returns_standard_envelope_when_requested() {
|
||||
let request_context = build_request_context(true);
|
||||
let body = json_success_body(Some(&request_context), json!({ "ok": "value" })).0;
|
||||
|
||||
assert_eq!(body["ok"], Value::Bool(true));
|
||||
assert_eq!(body["data"]["ok"], Value::String("value".to_string()));
|
||||
assert_eq!(
|
||||
body["meta"]["requestId"],
|
||||
Value::String("req-test".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
body["meta"]["routeVersion"],
|
||||
Value::String(API_VERSION.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_body_returns_raw_payload_when_envelope_not_requested() {
|
||||
let request_context = build_request_context(false);
|
||||
let body = json_success_body(Some(&request_context), json!({ "ok": "value" })).0;
|
||||
|
||||
assert_eq!(body["ok"], Value::String("value".to_string()));
|
||||
assert!(body.get("meta").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_body_returns_legacy_shape_without_envelope_header() {
|
||||
let request_context = build_request_context(false);
|
||||
let error = ApiErrorPayload {
|
||||
code: "NOT_FOUND",
|
||||
message: "资源不存在",
|
||||
details: None,
|
||||
};
|
||||
let body = json_error_body(Some(&request_context), &error).0;
|
||||
|
||||
assert_eq!(
|
||||
body["error"]["code"],
|
||||
Value::String("NOT_FOUND".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
body["meta"]["requestId"],
|
||||
Value::String("req-test".to_string())
|
||||
);
|
||||
assert!(body.get("ok").is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user