Skip to main content

geode_client/
proto.rs

1//! Protobuf types generated from geode.proto via prost-build/tonic-build.
2//!
3//! This module re-exports the generated types and provides helper functions
4//! for QUIC transport framing (4-byte big-endian length prefix).
5
6// Include the prost-generated code.
7#[allow(clippy::large_enum_variant)]
8#[path = "generated/geode.rs"]
9mod generated;
10
11// Re-export all generated types at this module level.
12pub use generated::*;
13
14use prost::Message;
15
16// Note: We use fully-qualified `crate::error::Error` to avoid collision with
17// the generated `proto::Error` message type (from `message Error` in geode.proto).
18use crate::error::Result;
19
20// =============================================================================
21// QUIC framing helpers (length-prefixed protobuf)
22// =============================================================================
23
24/// Encode a QuicClientMessage to protobuf bytes with a 4-byte big-endian
25/// length prefix, as required by the QUIC transport.
26pub fn encode_with_length_prefix(msg: &QuicClientMessage) -> Vec<u8> {
27    let data = msg.encode_to_vec();
28    let length = data.len() as u32;
29    let mut result = Vec::with_capacity(4 + data.len());
30    result.extend(&length.to_be_bytes());
31    result.extend(data);
32    result
33}
34
35/// Decode a 4-byte big-endian length prefix from the start of a byte slice.
36pub fn decode_length_prefix(data: &[u8]) -> Result<u32> {
37    if data.len() < 4 {
38        return Err(crate::error::Error::protocol(
39            "Insufficient data for length prefix",
40        ));
41    }
42    Ok(u32::from_be_bytes([data[0], data[1], data[2], data[3]]))
43}
44
45/// Decode a QuicServerMessage from raw protobuf bytes (without length prefix).
46pub fn decode_quic_server_message(data: &[u8]) -> Result<QuicServerMessage> {
47    QuicServerMessage::decode(data)
48        .map_err(|e| crate::error::Error::protocol(format!("Protobuf decode error: {}", e)))
49}
50
51// =============================================================================
52// Tests
53// =============================================================================
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn test_encode_decode_hello_roundtrip() {
61        let req = HelloRequest {
62            username: "admin".to_string(),
63            password: "secret".to_string(),
64            tenant_id: Some("tenant1".to_string()),
65            client_name: String::new(),
66            client_version: String::new(),
67            wanted_conformance: String::new(),
68            graph: None,
69            role: None,
70        };
71        let msg = QuicClientMessage {
72            msg: Some(quic_client_message::Msg::Hello(req)),
73        };
74        let encoded = msg.encode_to_vec();
75        assert!(!encoded.is_empty());
76
77        // Decode the client message as a QuicClientMessage
78        let decoded = QuicClientMessage::decode(encoded.as_slice()).unwrap();
79        match decoded.msg {
80            Some(quic_client_message::Msg::Hello(hello)) => {
81                assert_eq!(hello.username, "admin");
82                assert_eq!(hello.password, "secret");
83                assert_eq!(hello.tenant_id, Some("tenant1".to_string()));
84            }
85            _ => panic!("Expected Hello variant"),
86        }
87    }
88
89    /// GAP-0841: the HELLO handshake must carry both `tenant_id` (multi-tenancy)
90    /// and `role` (FLE field-level access control) so multi-tenant + FLE work
91    /// from the Rust client. This asserts both fields survive a protobuf
92    /// encode/decode round-trip on the wire wrapper (QuicClientMessage::Hello).
93    #[test]
94    fn test_hello_carries_tenant_and_role_gap0841() {
95        let req = HelloRequest {
96            username: "analyst".to_string(),
97            password: "secret".to_string(),
98            tenant_id: Some("acme-corp".to_string()),
99            client_name: "geode-rust".to_string(),
100            client_version: "0.2.0".to_string(),
101            wanted_conformance: "min".to_string(),
102            graph: Some("mygraph".to_string()),
103            role: Some("analyst-role".to_string()),
104        };
105        let msg = QuicClientMessage {
106            msg: Some(quic_client_message::Msg::Hello(req)),
107        };
108        let encoded = msg.encode_to_vec();
109
110        let decoded = QuicClientMessage::decode(encoded.as_slice()).unwrap();
111        match decoded.msg {
112            Some(quic_client_message::Msg::Hello(hello)) => {
113                assert_eq!(hello.tenant_id, Some("acme-corp".to_string()));
114                assert_eq!(hello.role, Some("analyst-role".to_string()));
115                assert_eq!(hello.graph, Some("mygraph".to_string()));
116            }
117            _ => panic!("Expected Hello variant"),
118        }
119    }
120
121    /// GAP-0841: when neither tenant nor role is configured, the optional
122    /// fields stay `None` (no wire bytes emitted) — backward compatible with a
123    /// single-tenant, non-FLE server.
124    #[test]
125    fn test_hello_omits_tenant_and_role_when_unset_gap0841() {
126        let req = HelloRequest {
127            username: "admin".to_string(),
128            password: "secret".to_string(),
129            tenant_id: None,
130            client_name: "geode-rust".to_string(),
131            client_version: "0.2.0".to_string(),
132            wanted_conformance: "min".to_string(),
133            graph: None,
134            role: None,
135        };
136        let encoded = req.encode_to_vec();
137        let decoded = HelloRequest::decode(encoded.as_slice()).unwrap();
138        assert_eq!(decoded.tenant_id, None);
139        assert_eq!(decoded.role, None);
140    }
141
142    #[test]
143    fn test_encode_decode_execute_roundtrip() {
144        let params = vec![
145            Param {
146                name: "name".to_string(),
147                value: Some(Value {
148                    kind: Some(value::Kind::StringVal(StringValue {
149                        value: "Alice".to_string(),
150                        kind: 0,
151                    })),
152                }),
153            },
154            Param {
155                name: "age".to_string(),
156                value: Some(Value {
157                    kind: Some(value::Kind::IntVal(IntValue { value: 30, kind: 0 })),
158                }),
159            },
160        ];
161
162        let req = ExecuteRequest {
163            session_id: "session123".to_string(),
164            query: "MATCH (n) RETURN n".to_string(),
165            params,
166        };
167        let msg = QuicClientMessage {
168            msg: Some(quic_client_message::Msg::Execute(req)),
169        };
170        let encoded = msg.encode_to_vec();
171        assert!(!encoded.is_empty());
172
173        let decoded = QuicClientMessage::decode(encoded.as_slice()).unwrap();
174        match decoded.msg {
175            Some(quic_client_message::Msg::Execute(exec)) => {
176                assert_eq!(exec.session_id, "session123");
177                assert_eq!(exec.query, "MATCH (n) RETURN n");
178                assert_eq!(exec.params.len(), 2);
179            }
180            _ => panic!("Expected Execute variant"),
181        }
182    }
183
184    #[test]
185    fn test_encode_with_length_prefix() {
186        let msg = QuicClientMessage {
187            msg: Some(quic_client_message::Msg::Ping(PingRequest {})),
188        };
189        let encoded = encode_with_length_prefix(&msg);
190        // Should have 4-byte length prefix
191        assert!(encoded.len() >= 4);
192        let length = u32::from_be_bytes([encoded[0], encoded[1], encoded[2], encoded[3]]);
193        assert_eq!(length as usize, encoded.len() - 4);
194    }
195
196    #[test]
197    fn test_decode_length_prefix() {
198        let data = [0x00, 0x00, 0x00, 0x10];
199        let length = decode_length_prefix(&data).unwrap();
200        assert_eq!(length, 16);
201    }
202
203    #[test]
204    fn test_decode_length_prefix_insufficient_data() {
205        let data = [0x00, 0x00];
206        let result = decode_length_prefix(&data);
207        assert!(result.is_err());
208    }
209
210    #[test]
211    fn test_decode_hello_response() {
212        // Build a HelloResponse, encode it, then decode
213        let resp = HelloResponse {
214            success: true,
215            session_id: "sess123".to_string(),
216            error_message: String::new(),
217            capabilities: vec![],
218            password_reset_required: false,
219            graph: None,
220        };
221        let encoded = resp.encode_to_vec();
222        let decoded = HelloResponse::decode(encoded.as_slice()).unwrap();
223        assert!(decoded.success);
224        assert_eq!(decoded.session_id, "sess123");
225    }
226
227    #[test]
228    fn test_decode_ping_response() {
229        let resp = PingResponse { ok: true };
230        let encoded = resp.encode_to_vec();
231        let decoded = PingResponse::decode(encoded.as_slice()).unwrap();
232        assert!(decoded.ok);
233    }
234
235    #[test]
236    fn test_value_null() {
237        let val = Value {
238            kind: Some(value::Kind::NullVal(NullValue {})),
239        };
240        assert!(matches!(val.kind, Some(value::Kind::NullVal(_))));
241    }
242
243    #[test]
244    fn test_value_default() {
245        let val = Value::default();
246        assert!(val.kind.is_none());
247    }
248
249    #[test]
250    fn test_message_defaults() {
251        let client_msg = QuicClientMessage::default();
252        assert!(client_msg.msg.is_none());
253
254        let server_msg = QuicServerMessage::default();
255        assert!(server_msg.msg.is_none());
256    }
257}