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(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_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()), ) } #[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()); } }