Files
Genarrative/server-rs/crates/shared-contracts/src/api.rs
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

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())
);
}
}