use std::convert::Infallible; use axum::{ Json, body::Body, http::{HeaderValue, header}, response::{IntoResponse, Response}, }; use bytes::Bytes; use futures_util::stream; 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(request_context: Option<&RequestContext>, data: T) -> Json 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_success_data_bytes_response( request_context: Option<&RequestContext>, data_json: Bytes, ) -> Response { if let Some(context) = request_context && context.wants_envelope() { let meta = serde_json::to_vec(&build_api_response_meta(Some(context))) .map(Bytes::from) .unwrap_or_else(|_| Bytes::from_static(b"null")); let chunks = [ Bytes::from_static(b"{\"ok\":true,\"data\":"), data_json, Bytes::from_static(b",\"error\":null,\"meta\":"), meta, Bytes::from_static(b"}"), ]; let stream = stream::iter(chunks.into_iter().map(Ok::)); return json_body_response(Body::from_stream(stream)); } json_bytes_response(data_json) } 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( 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()), ) } fn json_bytes_response(bytes: Bytes) -> Response { json_body_response(Body::from(bytes)) } fn json_body_response(body: Body) -> Response { let mut response = body.into_response(); response.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static("application/json; charset=utf-8"), ); response } #[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()); } #[tokio::test] async fn success_response_streams_cached_data_inside_standard_envelope() { use http_body_util::BodyExt; let request_context = build_request_context(true); let response = json_success_data_bytes_response( Some(&request_context), Bytes::from_static(br#"{"items":[]}"#), ); let body = response .into_body() .collect() .await .expect("response body should collect") .to_bytes(); let payload: Value = serde_json::from_slice(&body).expect("body should be json"); assert_eq!(payload["ok"], Value::Bool(true)); assert_eq!(payload["data"]["items"], Value::Array(Vec::new())); assert_eq!( payload["meta"]["requestId"], Value::String("req-test".to_string()) ); } #[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()); } }