Skip to main content

silicon_dm_protocol/
lib.rs

1//! Shared external JSON contract. HTTP methods, headers, and empty responses
2//! retain their normal semantics; every JSON document has exactly type and data.
3use serde::{Deserialize, Serialize};
4
5pub const WEBSOCKET_VERSION: u16 = 3;
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct Envelope<T> {
10    #[serde(rename = "type")]
11    pub kind: String,
12    pub data: T,
13}
14
15impl<T> Envelope<T> {
16    pub fn new(kind: impl Into<String>, data: T) -> Self {
17        Self {
18            kind: kind.into(),
19            data,
20        }
21    }
22}
23
24/// Stable operation discriminator for REST requests and successful responses.
25pub fn http_type(method: &str, path: &str) -> &'static str {
26    let path = path.split('?').next().unwrap_or(path);
27    let path = path
28        .strip_prefix("/api/v1/")
29        .unwrap_or(path)
30        .trim_matches('/');
31    let p: Vec<_> = path.split('/').collect();
32    match (method, p.as_slice()) {
33        (_, ["iam"]) => "iam",
34        (_, ["auth", "login"]) => "login",
35        (_, ["auth", "refresh"]) => "refresh",
36        (_, ["auth", "logout"]) => "logout",
37        (_, ["auth", "me"]) => "me",
38        ("POST", ["conversations"]) => "create_conversation",
39        (_, ["conversations"]) => "conversations",
40        ("POST", ["conversations", _, "messages"]) => "new_message",
41        (_, ["conversations", _, "messages"]) => "messages",
42        ("PATCH", ["conversations", _, "messages", _]) => "edit_message",
43        ("DELETE", ["conversations", _, "messages", _]) => "delete_message",
44        (_, ["conversations", _, "messages", _]) => "message",
45        (_, ["conversations", _, "messages", _, "receipts"]) => "receipt",
46        (_, ["conversations", _, "bundles"]) => "create_bundle",
47        (_, ["conversations", _, "bundles", _]) => "bundle",
48        ("PUT", ["conversations", _, "draft"]) => "put_draft",
49        ("DELETE", ["conversations", _, "draft"]) => "delete_draft",
50        (_, ["conversations", _, "draft"]) => "draft",
51        (_, ["presence", _]) => "presence",
52        (_, ["gifs", _]) => "gifs",
53        ("POST", ["testing-environments"]) => "create_testing_environment",
54        (_, ["testing-environments"]) => "testing_environments",
55        ("PATCH", ["testing-environments", _]) => "update_testing_environment",
56        ("DELETE", ["testing-environments", _]) => "delete_testing_environment",
57        (_, ["testing-environments", _]) => "testing_environment",
58        (_, ["testing-environments", _, "key"]) => "testing_environment_key",
59        (_, ["testing-environments", _, "rotate-key"]) => "rotate_testing_environment_key",
60        (_, ["testing-environments", _, "restore"]) => "restore_testing_environment",
61        (_, ["testing-environments", _, "clean"]) => "clean_testing_environment",
62        (_, ["requests"]) => "request",
63        (_, ["requests", _, "status"]) => "request_status",
64        (_, ["requests", _]) => "request_result",
65        (_, ["status"]) => "relay_status",
66        (_, ["shutdown"]) => "shutdown",
67        _ => "response",
68    }
69}
70
71/// Wrap serialized HTTP responses without copying or parsing large message bodies.
72#[cfg(feature = "http")]
73pub async fn responses(
74    request: axum::extract::Request,
75    next: axum::middleware::Next,
76) -> axum::response::Response {
77    use axum::{
78        body::{Body, Bytes},
79        http::header,
80    };
81    use futures_util::{StreamExt, stream};
82    let kind = http_type(request.method().as_str(), request.uri().path());
83    let response = next.run(request).await;
84    if !response
85        .headers()
86        .get(header::CONTENT_TYPE)
87        .and_then(|h| h.to_str().ok())
88        .is_some_and(|h| h.starts_with("application/json"))
89    {
90        if response.status().is_client_error() || response.status().is_server_error() {
91            use axum::response::IntoResponse;
92            let (parts, _) = response.into_parts();
93            let body = axum::Json(Envelope::new("error", serde_json::json!({"error":{
94                "code":"http_error", "message":parts.status.canonical_reason().unwrap_or("request failed")
95            }}))).into_response().into_body();
96            let mut response = axum::response::Response::from_parts(parts, body);
97            response.headers_mut().remove(header::CONTENT_LENGTH);
98            response.headers_mut().insert(
99                header::CONTENT_TYPE,
100                header::HeaderValue::from_static("application/json"),
101            );
102            return response;
103        }
104        return response;
105    }
106    let kind = if response.status().is_success() {
107        kind
108    } else {
109        "error"
110    };
111    let (mut parts, body) = response.into_parts();
112    parts.headers.remove(header::CONTENT_LENGTH);
113    let prefix = Bytes::from(format!("{{\"type\":\"{kind}\",\"data\":"));
114    let stream = stream::once(async { Ok::<_, axum::Error>(prefix) })
115        .chain(body.into_data_stream())
116        .chain(stream::once(async { Ok(Bytes::from_static(b"}")) }));
117    axum::response::Response::from_parts(parts, Body::from_stream(stream))
118}