169 lines
4.5 KiB
Rust
169 lines
4.5 KiB
Rust
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<String>,
|
|
#[serde(rename = "routeVersion")]
|
|
pub route_version: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub operation: Option<String>,
|
|
#[serde(rename = "latencyMs")]
|
|
pub latency_ms: u64,
|
|
pub timestamp: String,
|
|
}
|
|
|
|
impl ApiResponseMeta {
|
|
pub fn new(
|
|
api_version: impl Into<String>,
|
|
request_id: Option<String>,
|
|
route_version: impl Into<String>,
|
|
operation: Option<String>,
|
|
latency_ms: u64,
|
|
timestamp: impl Into<String>,
|
|
) -> 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<Value>,
|
|
}
|
|
|
|
impl ApiErrorPayload {
|
|
pub fn new(
|
|
code: impl Into<String>,
|
|
message: impl Into<String>,
|
|
details: Option<Value>,
|
|
) -> Self {
|
|
Self {
|
|
code: code.into(),
|
|
message: message.into(),
|
|
details,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
pub struct ApiSuccessEnvelope<T> {
|
|
pub ok: bool,
|
|
pub data: T,
|
|
pub error: Option<ApiErrorPayload>,
|
|
pub meta: ApiResponseMeta,
|
|
}
|
|
|
|
impl<T> ApiSuccessEnvelope<T> {
|
|
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<Value>,
|
|
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())
|
|
);
|
|
}
|
|
}
|