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}