Files
Genarrative/server-rs/crates/api-server/src/api_response.rs
2026-04-22 12:34:49 +08:00

130 lines
4.0 KiB
Rust

use axum::Json;
use serde::Serialize;
use serde_json::Value;
#[cfg(test)]
use serde_json::json;
use shared_contracts::api::{
API_VERSION, ApiErrorEnvelope, ApiErrorPayload, ApiResponseMeta, ApiSuccessEnvelope,
LegacyApiErrorResponse,
};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use crate::request_context::RequestContext;
// 当前阶段先把成功响应 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
&& context.wants_envelope()
{
return Json(
serde_json::to_value(ApiSuccessEnvelope::new(
data,
build_api_response_meta(Some(context)),
))
.unwrap_or(Value::Null),
);
}
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(
serde_json::to_value(ApiErrorEnvelope::new(error.clone(), meta)).unwrap_or(Value::Null),
);
}
Json(
serde_json::to_value(LegacyApiErrorResponse::new(error.clone(), meta))
.unwrap_or(Value::Null),
)
}
fn build_api_response_meta(request_context: Option<&RequestContext>) -> ApiResponseMeta {
ApiResponseMeta::new(
API_VERSION,
request_context.map(|context| context.request_id().to_string()),
API_VERSION,
request_context.map(|context| context.operation().to_string()),
request_context
.map(RequestContext::elapsed)
.unwrap_or_default(),
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".to_string(),
message: "资源不存在".to_string(),
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());
}
}