use axum::Json; use serde::Serialize; use serde_json::{Value, json}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; 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, #[serde(rename = "routeVersion")] route_version: &'static str, operation: Option, #[serde(rename = "latencyMs")] latency_ms: u64, timestamp: String, } // 当前阶段先把成功响应 envelope helper 准备好,后续 `/healthz` 与业务 handler 会直接复用这里的输出逻辑。 #[allow(dead_code)] pub fn json_success_body(request_context: Option<&RequestContext>, data: T) -> Json where T: Serialize, { if let Some(context) = request_context && 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 { 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()); } }