Skip to main content

turnbase_protocol/
lib.rs

1//! Request/response wire types shared by every Turnbase client and host.
2//!
3//! The envelope is typed: [`Request<A>`] carries a game's action and
4//! [`Response<V>`] carries a seat's view, rather than opaque bytes. This keeps
5//! the in-process and file-backed paths serialization-free and type-safe, and
6//! lets a real transport serialize the whole envelope in one clean pass (no
7//! bytes-inside-JSON double-encoding).
8//!
9//! These types are generic over the action and view types, not over
10//! `turnbase::Game`, so this crate has no dependency on `turnbase` and one
11//! definition serves every game. If a future remote transport ever needs to
12//! move messages without knowing the concrete types (a universal host, a
13//! polyglot bus), that erasure belongs *inside the transport*, not here: the
14//! port stays typed.
15
16use serde::{Deserialize, Serialize};
17
18/// Wire-format version, bumped on any breaking change to a type in this crate.
19///
20/// Stamped into save files and (eventually) exchanged with a remote host so a
21/// mismatch fails fast on an explicit check instead of surfacing as a
22/// confusing deserialization error deeper in the stack.
23pub const PROTOCOL_VERSION: u32 = 0;
24
25/// A message from a client to whichever process holds the authoritative state.
26///
27/// `A` is a game's `Action` type. The action is carried by value so a host can
28/// move it straight into `Game::apply` without cloning.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum Request<A> {
31    /// Ask for the requesting seat's current view, with no side effects.
32    Query,
33    /// Submit an action to apply.
34    Act(A),
35}
36
37/// A message back to the client. `V` is a game's `View` type.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub enum Response<V> {
40    /// A seat's current view, plus the state version it was taken at.
41    ///
42    /// `version` is a counter the host bumps once per applied action. Nothing
43    /// consumes it yet; it is banked now so a later `QuerySince(version)` can
44    /// answer "nothing changed" or send a delta without a wire-format break,
45    /// and so a polling client can tell a stale snapshot from a fresh one.
46    State {
47        /// The state version this view was taken at.
48        version: u64,
49        /// The requesting seat's view.
50        view: V,
51    },
52    /// An action was accepted and applied. Deliberately carries no state: a
53    /// write is acknowledged cheaply, and a client fetches the result with an
54    /// explicit [`Request::Query`] only when it actually needs to render.
55    Ack,
56    /// The request was rejected. The message is for humans and logs, not for
57    /// programmatic matching.
58    Error(String),
59}
60
61#[cfg(test)]
62mod tests {
63    use super::{Request, Response};
64
65    #[test]
66    fn act_request_round_trips_through_json() {
67        let request = Request::Act(42u32);
68        let json = serde_json::to_string(&request).expect("serialize");
69        let decoded: Request<u32> = serde_json::from_str(&json).expect("deserialize");
70        assert_eq!(request, decoded);
71    }
72
73    #[test]
74    fn query_request_round_trips_through_json() {
75        let request: Request<u32> = Request::Query;
76        let json = serde_json::to_string(&request).expect("serialize");
77        let decoded: Request<u32> = serde_json::from_str(&json).expect("deserialize");
78        assert_eq!(request, decoded);
79    }
80
81    #[test]
82    fn state_response_encodes_its_view_inline_not_as_bytes() {
83        let response = Response::State {
84            version: 7,
85            view: 99u32,
86        };
87        let json = serde_json::to_string(&response).expect("serialize");
88        // The whole point of the typed envelope: the view is a plain value in
89        // the JSON, not a nested byte array.
90        assert!(json.contains("\"view\":99"), "got {json}");
91        let decoded: Response<u32> = serde_json::from_str(&json).expect("deserialize");
92        assert_eq!(response, decoded);
93    }
94
95    #[test]
96    fn error_response_round_trips() {
97        let response: Response<u32> = Response::Error("no session found".to_owned());
98        let json = serde_json::to_string(&response).expect("serialize");
99        let decoded: Response<u32> = serde_json::from_str(&json).expect("deserialize");
100        assert_eq!(response, decoded);
101    }
102}