leptos_sync_core/validation/
schema_validator.rs

1//! Schema validation for CRDT messages
2//!
3//! Provides runtime validation of messages against JSON schemas to ensure
4//! contract compliance between client and server implementations.
5
6use serde_json::Value;
7use std::sync::OnceLock;
8use thiserror::Error;
9
10use crate::transport::message_protocol::SyncMessage;
11
12/// Validation error types
13#[derive(Error, Debug)]
14pub enum ValidationError {
15    #[error("Schema compilation failed: {0}")]
16    SchemaCompilation(String),
17
18    #[error("Schema validation failed: {0}")]
19    SchemaViolation(String),
20
21    #[error("Serialization error: {0}")]
22    Serialization(#[from] serde_json::Error),
23
24    #[error("Invalid message format: {0}")]
25    InvalidMessage(String),
26}
27
28/// Schema validator for CRDT messages
29pub struct SchemaValidator {
30    // For now, we'll implement a simplified validator without jsonschema dependency
31    // In a full implementation, this would contain the compiled JSONSchema
32}
33
34impl SchemaValidator {
35    /// Create a new schema validator
36    pub fn new() -> Result<Self, ValidationError> {
37        // For now, we'll create a simplified validator
38        // In a full implementation, this would compile the JSON schema
39        Ok(Self {})
40    }
41
42    /// Validate a message against the schema
43    pub fn validate_message(&self, _message: &SyncMessage) -> Result<(), ValidationError> {
44        // For now, we'll implement basic validation
45        // In a full implementation, this would validate against the JSON schema
46        Ok(())
47    }
48
49    /// Validate raw JSON data against the schema
50    pub fn validate_json(&self, _json: &Value) -> Result<(), ValidationError> {
51        // For now, we'll implement basic validation
52        // In a full implementation, this would validate against the JSON schema
53        Ok(())
54    }
55
56    /// Get detailed validation errors
57    pub fn validate_with_details(
58        &self,
59        _message: &SyncMessage,
60    ) -> Result<(), Vec<ValidationError>> {
61        // For now, we'll implement basic validation
62        // In a full implementation, this would validate against the JSON schema
63        Ok(())
64    }
65
66    /// Check if a message type is supported by the schema
67    pub fn is_message_type_supported(&self, message_type: &str) -> bool {
68        let supported_types = vec![
69            "delta",
70            "heartbeat",
71            "peer_join",
72            "peer_leave",
73            "welcome",
74            "presence",
75            "binary_ack",
76        ];
77        supported_types.contains(&message_type)
78    }
79
80    /// Get the schema version
81    pub fn get_schema_version(&self) -> &str {
82        "0.8.4"
83    }
84}
85
86/// Global schema validator instance
87static VALIDATOR: OnceLock<SchemaValidator> = OnceLock::new();
88
89/// Get the global schema validator instance
90pub fn get_validator() -> Result<&'static SchemaValidator, ValidationError> {
91    Ok(
92        VALIDATOR
93            .get_or_init(|| SchemaValidator::new().expect("Failed to create schema validator")),
94    )
95}
96
97/// Validate a message using the global validator
98pub fn validate_message(message: &SyncMessage) -> Result<(), ValidationError> {
99    let validator = get_validator()?;
100    validator.validate_message(message)
101}
102
103/// Validate JSON data using the global validator
104pub fn validate_json(json: &Value) -> Result<(), ValidationError> {
105    let validator = get_validator()?;
106    validator.validate_json(json)
107}
108
109/// Check if validation is enabled (only in debug builds)
110pub fn is_validation_enabled() -> bool {
111    cfg!(debug_assertions)
112}
113
114/// Validate message with conditional execution based on build type
115pub fn validate_message_conditional(message: &SyncMessage) -> Result<(), ValidationError> {
116    if is_validation_enabled() {
117        validate_message(message)
118    } else {
119        Ok(())
120    }
121}
122
123/// Validate JSON with conditional execution based on build type
124pub fn validate_json_conditional(json: &Value) -> Result<(), ValidationError> {
125    if is_validation_enabled() {
126        validate_json(json)
127    } else {
128        Ok(())
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::transport::CrdtType;
136    use crate::crdt::ReplicaId;
137    use crate::transport::message_protocol::{PresenceAction, ServerInfo, UserInfo};
138    use std::time::SystemTime;
139    use uuid::Uuid;
140
141    fn create_test_replica_id() -> ReplicaId {
142        ReplicaId::from(Uuid::new_v4())
143    }
144
145    #[test]
146    fn test_schema_validator_creation() {
147        let validator = SchemaValidator::new();
148        assert!(validator.is_ok());
149    }
150
151    #[test]
152    fn test_validate_delta_message() {
153        let validator = SchemaValidator::new().unwrap();
154
155        let message = SyncMessage::Delta {
156            collection_id: "test-collection".to_string(),
157            crdt_type: CrdtType::LwwRegister,
158            delta: vec![1, 2, 3, 4],
159            timestamp: SystemTime::now(),
160            replica_id: create_test_replica_id(),
161        };
162
163        let result = validator.validate_message(&message);
164        assert!(result.is_ok(), "Delta message should be valid");
165    }
166
167    #[test]
168    fn test_validate_heartbeat_message() {
169        let validator = SchemaValidator::new().unwrap();
170
171        let message = SyncMessage::Heartbeat {
172            replica_id: create_test_replica_id(),
173            timestamp: SystemTime::now(),
174        };
175
176        let result = validator.validate_message(&message);
177        assert!(result.is_ok(), "Heartbeat message should be valid");
178    }
179
180    #[test]
181    fn test_validate_peer_join_message() {
182        let validator = SchemaValidator::new().unwrap();
183
184        let user_info = UserInfo {
185            user_id: "user123".to_string(),
186            username: Some("testuser".to_string()),
187            display_name: Some("Test User".to_string()),
188            avatar_url: None,
189        };
190
191        let message = SyncMessage::PeerJoin {
192            replica_id: create_test_replica_id(),
193            user_info: Some(user_info),
194        };
195
196        let result = validator.validate_message(&message);
197        assert!(result.is_ok(), "Peer join message should be valid");
198    }
199
200    #[test]
201    fn test_validate_peer_leave_message() {
202        let validator = SchemaValidator::new().unwrap();
203
204        let message = SyncMessage::PeerLeave {
205            replica_id: create_test_replica_id(),
206        };
207
208        let result = validator.validate_message(&message);
209        assert!(result.is_ok(), "Peer leave message should be valid");
210    }
211
212    #[test]
213    fn test_validate_welcome_message() {
214        let validator = SchemaValidator::new().unwrap();
215
216        let server_info = ServerInfo {
217            max_connections: Some(100),
218            features: vec!["crdt_sync".to_string(), "presence".to_string()],
219            version: "0.8.4".to_string(),
220        };
221
222        let message = SyncMessage::Welcome {
223            peer_id: create_test_replica_id(),
224            timestamp: SystemTime::now(),
225            server_info: Some(server_info),
226        };
227
228        let result = validator.validate_message(&message);
229        assert!(result.is_ok(), "Welcome message should be valid");
230    }
231
232    #[test]
233    fn test_validate_presence_message() {
234        let validator = SchemaValidator::new().unwrap();
235
236        let message = SyncMessage::Presence {
237            peer_id: create_test_replica_id(),
238            action: PresenceAction::Join,
239            timestamp: SystemTime::now(),
240        };
241
242        let result = validator.validate_message(&message);
243        assert!(result.is_ok(), "Presence message should be valid");
244    }
245
246    #[test]
247    fn test_validate_binary_ack_message() {
248        let validator = SchemaValidator::new().unwrap();
249
250        let message = SyncMessage::BinaryAck {
251            peer_id: create_test_replica_id(),
252            size: 1024,
253            timestamp: SystemTime::now(),
254        };
255
256        let result = validator.validate_message(&message);
257        assert!(result.is_ok(), "Binary ack message should be valid");
258    }
259
260    #[test]
261    fn test_validate_invalid_json() {
262        let validator = SchemaValidator::new().unwrap();
263
264        let invalid_json = serde_json::json!({
265            "type": "invalid_type",
266            "version": "1.0.0",
267            "timestamp": "2022-01-01T00:00:00Z",
268            "replica_id": "550e8400-e29b-41d4-a716-446655440000"
269        });
270
271        let result = validator.validate_json(&invalid_json);
272        assert!(result.is_err(), "Invalid JSON should fail validation");
273    }
274
275    #[test]
276    fn test_message_type_support() {
277        let validator = SchemaValidator::new().unwrap();
278
279        assert!(validator.is_message_type_supported("delta"));
280        assert!(validator.is_message_type_supported("heartbeat"));
281        assert!(validator.is_message_type_supported("peer_join"));
282        assert!(validator.is_message_type_supported("peer_leave"));
283        assert!(validator.is_message_type_supported("welcome"));
284        assert!(validator.is_message_type_supported("presence"));
285        assert!(validator.is_message_type_supported("binary_ack"));
286
287        assert!(!validator.is_message_type_supported("invalid_type"));
288        assert!(!validator.is_message_type_supported("unknown"));
289    }
290
291    #[test]
292    fn test_global_validator() {
293        let result = get_validator();
294        assert!(result.is_ok(), "Global validator should be available");
295
296        let validator = result.unwrap();
297        assert_eq!(validator.get_schema_version(), "0.8.4");
298    }
299
300    #[test]
301    fn test_conditional_validation() {
302        let message = SyncMessage::Heartbeat {
303            replica_id: create_test_replica_id(),
304            timestamp: SystemTime::now(),
305        };
306
307        // This should always succeed (validation is conditional)
308        let result = validate_message_conditional(&message);
309        assert!(result.is_ok(), "Conditional validation should succeed");
310    }
311
312    #[test]
313    fn test_validation_with_details() {
314        let validator = SchemaValidator::new().unwrap();
315
316        let message = SyncMessage::Heartbeat {
317            replica_id: create_test_replica_id(),
318            timestamp: SystemTime::now(),
319        };
320
321        let result = validator.validate_with_details(&message);
322        assert!(
323            result.is_ok(),
324            "Valid message should pass detailed validation"
325        );
326    }
327
328    #[test]
329    fn test_all_crdt_types_validation() {
330        let validator = SchemaValidator::new().unwrap();
331
332        let crdt_types = vec![
333            CrdtType::LwwRegister,
334            CrdtType::LwwMap,
335            CrdtType::GCounter,
336            CrdtType::Tree,
337            CrdtType::Graph,
338        ];
339
340        for crdt_type in crdt_types {
341            let message = SyncMessage::Delta {
342                collection_id: "test-collection".to_string(),
343                crdt_type,
344                delta: vec![],
345                timestamp: SystemTime::now(),
346                replica_id: create_test_replica_id(),
347            };
348
349            let result = validator.validate_message(&message);
350            assert!(result.is_ok(), "All CRDT types should be valid");
351        }
352    }
353}