Skip to main content

galeon_engine/
protocol.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3//! Protocol marker traits and metadata for Galeon's boundary abstraction.
4//!
5//! The protocol layer defines four concepts that let the same game logic work
6//! in-process, over HTTP/WS, or through native bindings:
7//!
8//! - [`Command`] — state-changing requests
9//! - [`ProtocolQuery`] — read-only requests
10//! - [`Event`] — authoritative facts emitted after state transitions
11//! - [`Dto`] — boundary-facing data structures
12//!
13//! Each protocol item can implement [`ProtocolMeta`] to expose its name and
14//! [`ProtocolKind`] for manifest generation and codegen tooling.
15//!
16//! # Design
17//!
18//! These traits define *vocabulary only*. They carry no transport semantics,
19//! no manifest emission, and no runtime behavior. Attribute macros in
20//! `galeon-engine-macros` emit concrete [`ProtocolMeta`] implementations per
21//! annotated item — see issue #46.
22
23use serde::{Deserialize, Serialize};
24
25/// Discriminant for protocol item kinds.
26///
27/// Used by [`ProtocolMeta`] to identify what role a protocol item plays.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum ProtocolKind {
30    /// A state-changing request.
31    Command,
32    /// A read-only request.
33    Query,
34    /// An authoritative fact emitted after a state transition.
35    Event,
36    /// A boundary-facing data structure.
37    Dto,
38}
39
40/// Metadata trait for protocol items.
41///
42/// Provides the item's stable name and [`ProtocolKind`] discriminant.
43/// This is the smallest surface macros need to target for manifest
44/// generation (#47).
45///
46/// # Implementors
47///
48/// Do not implement this trait manually in production code. Use the
49/// `#[galeon::command]`, `#[galeon::query]`, `#[galeon::event]`, or
50/// `#[galeon::dto]` attribute macros, which emit one concrete impl per
51/// annotated item.
52///
53/// Manual implementation is valid for testing and for types that cannot
54/// use the attribute macros.
55pub trait ProtocolMeta {
56    /// The stable protocol name for this item.
57    fn name() -> &'static str;
58
59    /// The protocol kind discriminant.
60    fn kind() -> ProtocolKind;
61}
62
63/// Marker trait for state-changing requests.
64///
65/// Commands represent intent to mutate game state. They travel from client
66/// to server and are validated before execution.
67///
68/// # Bounds
69///
70/// Requires `Serialize + Deserialize + Send + Sync + 'static` so commands
71/// can cross thread and serialization boundaries.
72pub trait Command: Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static {}
73
74/// Marker trait for read-only requests.
75///
76/// Queries request a snapshot of game state without side effects.
77///
78/// # Bounds
79///
80/// Same as [`Command`]: `Serialize + Deserialize + Send + Sync + 'static`.
81///
82/// Renamed from `Query` to `ProtocolQuery` in #57 to free up the `Query` name
83/// for the far more frequently used ECS system parameter.
84pub trait ProtocolQuery: Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static {}
85
86/// Marker trait for authoritative facts emitted after state transitions.
87///
88/// Events are immutable records of something that happened. They flow from
89/// server to client (and potentially to other server-side subscribers).
90///
91/// # Bounds
92///
93/// Same as [`Command`]: `Serialize + Deserialize + Send + Sync + 'static`.
94pub trait Event: Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static {}
95
96/// Marker trait for boundary-facing data structures.
97///
98/// DTOs are snapshot/view types that get copied across boundaries. They
99/// carry no behavior — only data.
100///
101/// # Bounds
102///
103/// Adds `Clone` on top of the standard protocol bounds because DTOs are
104/// value types that get copied freely.
105pub trait Dto: Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static {}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    // --- Sample structs for each trait ---
112
113    #[derive(Debug, Serialize, Deserialize, PartialEq)]
114    struct SpawnUnit {
115        unit_id: u64,
116        location_id: u64,
117    }
118    impl Command for SpawnUnit {}
119    impl ProtocolMeta for SpawnUnit {
120        fn name() -> &'static str {
121            "SpawnUnit"
122        }
123        fn kind() -> ProtocolKind {
124            ProtocolKind::Command
125        }
126    }
127
128    #[derive(Debug, Serialize, Deserialize, PartialEq)]
129    struct GetWorldSnapshot;
130    impl ProtocolQuery for GetWorldSnapshot {}
131    impl ProtocolMeta for GetWorldSnapshot {
132        fn name() -> &'static str {
133            "GetWorldSnapshot"
134        }
135        fn kind() -> ProtocolKind {
136            ProtocolKind::Query
137        }
138    }
139
140    #[derive(Debug, Serialize, Deserialize, PartialEq)]
141    struct UnitDestroyed {
142        unit_id: u64,
143        arrived_at: u64,
144    }
145    impl Event for UnitDestroyed {}
146    impl ProtocolMeta for UnitDestroyed {
147        fn name() -> &'static str {
148            "UnitDestroyed"
149        }
150        fn kind() -> ProtocolKind {
151            ProtocolKind::Event
152        }
153    }
154
155    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
156    struct WorldSnapshot {
157        unit_count: u32,
158    }
159    impl Dto for WorldSnapshot {}
160    impl ProtocolMeta for WorldSnapshot {
161        fn name() -> &'static str {
162            "WorldSnapshot"
163        }
164        fn kind() -> ProtocolKind {
165            ProtocolKind::Dto
166        }
167    }
168
169    // --- T6: ProtocolKind ---
170
171    #[test]
172    fn protocol_kind_variants() {
173        assert_ne!(ProtocolKind::Command, ProtocolKind::Query);
174        assert_ne!(ProtocolKind::Event, ProtocolKind::Dto);
175    }
176
177    #[test]
178    fn protocol_kind_serde_roundtrip() {
179        for kind in [
180            ProtocolKind::Command,
181            ProtocolKind::Query,
182            ProtocolKind::Event,
183            ProtocolKind::Dto,
184        ] {
185            let json = serde_json::to_string(&kind).unwrap();
186            let back: ProtocolKind = serde_json::from_str(&json).unwrap();
187            assert_eq!(kind, back);
188        }
189    }
190
191    // --- T5: ProtocolMeta returns correct kind ---
192
193    #[test]
194    fn protocol_meta_command() {
195        assert_eq!(SpawnUnit::name(), "SpawnUnit");
196        assert_eq!(SpawnUnit::kind(), ProtocolKind::Command);
197    }
198
199    #[test]
200    fn protocol_meta_query() {
201        assert_eq!(GetWorldSnapshot::name(), "GetWorldSnapshot");
202        assert_eq!(GetWorldSnapshot::kind(), ProtocolKind::Query);
203    }
204
205    #[test]
206    fn protocol_meta_event() {
207        assert_eq!(UnitDestroyed::name(), "UnitDestroyed");
208        assert_eq!(UnitDestroyed::kind(), ProtocolKind::Event);
209    }
210
211    #[test]
212    fn protocol_meta_dto() {
213        assert_eq!(WorldSnapshot::name(), "WorldSnapshot");
214        assert_eq!(WorldSnapshot::kind(), ProtocolKind::Dto);
215    }
216
217    // --- T1-T4: Serde round-trip ---
218
219    #[test]
220    fn command_serde_roundtrip() {
221        let cmd = SpawnUnit {
222            unit_id: 1,
223            location_id: 42,
224        };
225        let json = serde_json::to_string(&cmd).unwrap();
226        let back: SpawnUnit = serde_json::from_str(&json).unwrap();
227        assert_eq!(cmd, back);
228    }
229
230    #[test]
231    fn query_serde_roundtrip() {
232        let q = GetWorldSnapshot;
233        let json = serde_json::to_string(&q).unwrap();
234        let back: GetWorldSnapshot = serde_json::from_str(&json).unwrap();
235        assert_eq!(q, back);
236    }
237
238    #[test]
239    fn event_serde_roundtrip() {
240        let evt = UnitDestroyed {
241            unit_id: 1,
242            arrived_at: 1000,
243        };
244        let json = serde_json::to_string(&evt).unwrap();
245        let back: UnitDestroyed = serde_json::from_str(&json).unwrap();
246        assert_eq!(evt, back);
247    }
248
249    #[test]
250    fn dto_serde_roundtrip() {
251        let dto = WorldSnapshot { unit_count: 5 };
252        let json = serde_json::to_string(&dto).unwrap();
253        let back: WorldSnapshot = serde_json::from_str(&json).unwrap();
254        assert_eq!(dto, back);
255    }
256
257    // --- Acceptance: Component + Command coexistence ---
258
259    #[test]
260    fn component_and_command_no_conflict() {
261        use crate::component::Component;
262
263        #[derive(Debug, Serialize, Deserialize)]
264        struct Health {
265            hp: u32,
266        }
267        impl Component for Health {}
268        impl Command for Health {}
269        impl ProtocolMeta for Health {
270            fn name() -> &'static str {
271                "Health"
272            }
273            fn kind() -> ProtocolKind {
274                ProtocolKind::Command
275            }
276        }
277
278        // Both traits coexist without conflict.
279        let h = Health { hp: 100 };
280        assert_eq!(Health::kind(), ProtocolKind::Command);
281        let json = serde_json::to_string(&h).unwrap();
282        assert!(json.contains("100"));
283    }
284}