后端重写提交
This commit is contained in:
168
server-rs/crates/shared-contracts/src/api.rs
Normal file
168
server-rs/crates/shared-contracts/src/api.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
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())
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user