use serde::{Deserialize, Serialize}; use serde_json::Value; pub const API_VERSION: &str = "2026-04-08"; pub const API_RESPONSE_ENVELOPE_HEADER: &str = "x-genarrative-response-envelope"; pub const X_REQUEST_ID_HEADER: &str = "x-request-id"; pub const API_VERSION_HEADER: &str = "x-api-version"; pub const ROUTE_VERSION_HEADER: &str = "x-route-version"; pub const RESPONSE_TIME_HEADER: &str = "x-response-time-ms"; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ApiResponseMeta { #[serde(rename = "apiVersion")] pub api_version: String, #[serde(rename = "requestId", skip_serializing_if = "Option::is_none")] pub request_id: Option, #[serde(rename = "routeVersion")] pub route_version: String, #[serde(skip_serializing_if = "Option::is_none")] pub operation: Option, #[serde(rename = "latencyMs")] pub latency_ms: u64, pub timestamp: String, } impl ApiResponseMeta { pub fn new( api_version: impl Into, request_id: Option, route_version: impl Into, operation: Option, latency_ms: u64, timestamp: impl Into, ) -> Self { Self { api_version: api_version.into(), request_id, route_version: route_version.into(), operation, latency_ms, timestamp: timestamp.into(), } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct ApiErrorPayload { pub code: String, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub details: Option, } impl ApiErrorPayload { pub fn new( code: impl Into, message: impl Into, details: Option, ) -> Self { Self { code: code.into(), message: message.into(), details, } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct ApiSuccessEnvelope { pub ok: bool, pub data: T, pub error: Option, pub meta: ApiResponseMeta, } impl ApiSuccessEnvelope { pub fn new(data: T, meta: ApiResponseMeta) -> Self { Self { ok: true, data, error: None, meta, } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct ApiErrorEnvelope { pub ok: bool, pub data: Option, pub error: ApiErrorPayload, pub meta: ApiResponseMeta, } impl ApiErrorEnvelope { pub fn new(error: ApiErrorPayload, meta: ApiResponseMeta) -> Self { Self { ok: false, data: None, error, meta, } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct LegacyApiErrorResponse { pub error: ApiErrorPayload, pub meta: ApiResponseMeta, } impl LegacyApiErrorResponse { pub fn new(error: ApiErrorPayload, meta: ApiResponseMeta) -> Self { Self { error, meta } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn success_envelope_serializes_null_error_field() { let payload = serde_json::to_value(ApiSuccessEnvelope::new( json!({ "service": "genarrative" }), ApiResponseMeta::new( API_VERSION, Some("req-1".to_string()), API_VERSION, Some("GET /healthz".to_string()), 12, "2026-04-21T00:00:00Z", ), )) .expect("payload should serialize"); assert_eq!(payload["ok"], Value::Bool(true)); assert_eq!(payload["error"], Value::Null); assert_eq!( payload["meta"]["apiVersion"], Value::String(API_VERSION.to_string()) ); } #[test] fn error_envelope_serializes_null_data_field() { let payload = serde_json::to_value(ApiErrorEnvelope::new( ApiErrorPayload::new("BAD_REQUEST", "请求参数不合法", None), ApiResponseMeta::new( API_VERSION, Some("req-2".to_string()), API_VERSION, Some("POST /api/test".to_string()), 21, "2026-04-21T00:00:01Z", ), )) .expect("payload should serialize"); assert_eq!(payload["ok"], Value::Bool(false)); assert_eq!(payload["data"], Value::Null); assert_eq!( payload["error"]["code"], Value::String("BAD_REQUEST".to_string()) ); } }