basic_structs/
basic_structs.rs

1//! Basic Struct Serialization Example
2//!
3//! This example demonstrates fundamental serialization patterns in Train Station:
4//! - Implementing StructSerializable for custom structs
5//! - Basic field types and their serialization behavior
6//! - JSON and binary format roundtrip operations
7//! - Best practices for struct design and serialization
8//! - File persistence and loading workflows
9//!
10//! # Learning Objectives
11//!
12//! - Understand StructSerializable trait implementation
13//! - Learn field-by-field serialization patterns
14//! - Master basic data type serialization
15//! - Explore format selection criteria
16//! - Implement robust save/load workflows
17//!
18//! # Prerequisites
19//!
20//! - Basic Rust knowledge
21//! - Understanding of struct definitions
22//! - Familiarity with file I/O concepts
23//!
24//! # Usage
25//!
26//! ```bash
27//! cargo run --example basic_structs
28//! ```
29
30use std::{collections::HashMap, fs};
31use train_station::serialization::{
32    FieldValue, FromFieldValue, SerializationError, SerializationResult, StructDeserializer,
33    StructSerializable, StructSerializer, ToFieldValue,
34};
35
36/// Simple user profile struct demonstrating basic field types
37#[derive(Debug, Clone, PartialEq)]
38pub struct UserProfile {
39    pub id: u32,
40    pub username: String,
41    pub email: String,
42    pub age: i32, // Changed from u16 to i32 for better JSON compatibility
43    pub is_active: bool,
44    pub score: f32, // Changed from f64 to f32 for better compatibility
45}
46
47impl StructSerializable for UserProfile {
48    fn to_serializer(&self) -> StructSerializer {
49        StructSerializer::new()
50            .field("id", &self.id)
51            .field("username", &self.username)
52            .field("email", &self.email)
53            .field("age", &self.age)
54            .field("is_active", &self.is_active)
55            .field("score", &self.score)
56    }
57
58    fn from_deserializer(deserializer: &mut StructDeserializer) -> SerializationResult<Self> {
59        let id = deserializer.field("id")?;
60        let username = deserializer.field("username")?;
61        let email = deserializer.field("email")?;
62        let age = deserializer.field("age")?;
63        let is_active = deserializer.field("is_active")?;
64        let score = deserializer.field("score")?;
65
66        Ok(UserProfile {
67            id,
68            username,
69            email,
70            age,
71            is_active,
72            score,
73        })
74    }
75}
76
77impl ToFieldValue for UserProfile {
78    fn to_field_value(&self) -> FieldValue {
79        // Convert to JSON and then parse as FieldValue for nested object handling
80        match self.to_json() {
81            Ok(json_str) => {
82                // For examples, we'll serialize as JSON string for simplicity
83                FieldValue::from_json_object(json_str)
84            }
85            Err(_) => FieldValue::from_string("serialization_error".to_string()),
86        }
87    }
88}
89
90impl FromFieldValue for UserProfile {
91    fn from_field_value(value: FieldValue, field_name: &str) -> SerializationResult<Self> {
92        // Try JSON object first
93        if let Ok(json_data) = value.as_json_object() {
94            return Self::from_json(json_data).map_err(|e| SerializationError::ValidationFailed {
95                field: field_name.to_string(),
96                message: format!("Failed to deserialize UserProfile from JSON: {}", e),
97            });
98        }
99
100        // Try binary object
101        if let Ok(binary_data) = value.as_binary_object() {
102            return Self::from_binary(binary_data).map_err(|e| {
103                SerializationError::ValidationFailed {
104                    field: field_name.to_string(),
105                    message: format!("Failed to deserialize UserProfile from binary: {}", e),
106                }
107            });
108        }
109
110        Err(SerializationError::ValidationFailed {
111            field: field_name.to_string(),
112            message: format!(
113                "Expected JsonObject or BinaryObject for UserProfile, found {}",
114                value.type_name()
115            ),
116        })
117    }
118}
119
120/// Application settings struct with optional fields and collections
121#[derive(Debug, Clone, PartialEq)]
122pub struct AppSettings {
123    pub app_name: String,
124    pub version: String,
125    pub debug_mode: bool,
126    pub max_connections: u32,
127    pub timeout_seconds: f32,
128    pub features: Vec<String>,
129    pub environment_vars: HashMap<String, String>,
130    pub optional_database_url: Option<String>,
131}
132
133impl StructSerializable for AppSettings {
134    fn to_serializer(&self) -> StructSerializer {
135        StructSerializer::new()
136            .field("app_name", &self.app_name)
137            .field("version", &self.version)
138            .field("debug_mode", &self.debug_mode)
139            .field("max_connections", &self.max_connections)
140            .field("timeout_seconds", &self.timeout_seconds)
141            .field("features", &self.features)
142            .field("environment_vars", &self.environment_vars)
143            .field("optional_database_url", &self.optional_database_url)
144    }
145
146    fn from_deserializer(deserializer: &mut StructDeserializer) -> SerializationResult<Self> {
147        let app_name = deserializer.field("app_name")?;
148        let version = deserializer.field("version")?;
149        let debug_mode = deserializer.field("debug_mode")?;
150        let max_connections = deserializer.field("max_connections")?;
151        let timeout_seconds = deserializer.field("timeout_seconds")?;
152        let features = deserializer.field("features")?;
153        let environment_vars = deserializer.field("environment_vars")?;
154        let optional_database_url = deserializer.field("optional_database_url")?;
155
156        Ok(AppSettings {
157            app_name,
158            version,
159            debug_mode,
160            max_connections,
161            timeout_seconds,
162            features,
163            environment_vars,
164            optional_database_url,
165        })
166    }
167}
168
169impl ToFieldValue for AppSettings {
170    fn to_field_value(&self) -> FieldValue {
171        // Convert to JSON and then parse as FieldValue for nested object handling
172        match self.to_json() {
173            Ok(json_str) => FieldValue::from_json_object(json_str),
174            Err(_) => FieldValue::from_string("serialization_error".to_string()),
175        }
176    }
177}
178
179impl FromFieldValue for AppSettings {
180    fn from_field_value(value: FieldValue, field_name: &str) -> SerializationResult<Self> {
181        // Try JSON object first
182        if let Ok(json_data) = value.as_json_object() {
183            return Self::from_json(json_data).map_err(|e| SerializationError::ValidationFailed {
184                field: field_name.to_string(),
185                message: format!("Failed to deserialize AppSettings from JSON: {}", e),
186            });
187        }
188
189        // Try binary object
190        if let Ok(binary_data) = value.as_binary_object() {
191            return Self::from_binary(binary_data).map_err(|e| {
192                SerializationError::ValidationFailed {
193                    field: field_name.to_string(),
194                    message: format!("Failed to deserialize AppSettings from binary: {}", e),
195                }
196            });
197        }
198
199        Err(SerializationError::ValidationFailed {
200            field: field_name.to_string(),
201            message: format!(
202                "Expected JsonObject or BinaryObject for AppSettings, found {}",
203                value.type_name()
204            ),
205        })
206    }
207}
208
209fn main() -> Result<(), Box<dyn std::error::Error>> {
210    println!("=== Basic Struct Serialization Example ===\n");
211
212    demonstrate_user_profile_serialization()?;
213    demonstrate_app_settings_serialization()?;
214    demonstrate_format_comparison()?;
215    demonstrate_roundtrip_verification()?;
216    demonstrate_field_access_patterns()?;
217    cleanup_temp_files()?;
218
219    println!("\n=== Example completed successfully! ===");
220    Ok(())
221}
222
223/// Demonstrate basic struct serialization with simple field types
224fn demonstrate_user_profile_serialization() -> Result<(), Box<dyn std::error::Error>> {
225    println!("--- User Profile Serialization ---");
226
227    // Create a user profile with various field types
228    let user = UserProfile {
229        id: 12345,
230        username: "alice_cooper".to_string(),
231        email: "alice@example.com".to_string(),
232        age: 28,
233        is_active: true,
234        score: 95.7,
235    };
236
237    println!("Original user profile:");
238    println!("  ID: {}", user.id);
239    println!("  Username: {}", user.username);
240    println!("  Email: {}", user.email);
241    println!("  Age: {}", user.age);
242    println!("  Active: {}", user.is_active);
243    println!("  Score: {}", user.score);
244
245    // Serialize to JSON
246    let json_data = user.to_json()?;
247    println!("\nSerialized to JSON:");
248    println!("{}", json_data);
249
250    // Save to JSON file
251    user.save_json("temp_user_profile.json")?;
252    println!("Saved to file: temp_user_profile.json");
253
254    // Load from JSON file
255    let loaded_user = UserProfile::load_json("temp_user_profile.json")?;
256    println!("\nLoaded user profile:");
257    println!("  ID: {}", loaded_user.id);
258    println!("  Username: {}", loaded_user.username);
259    println!("  Email: {}", loaded_user.email);
260    println!("  Age: {}", loaded_user.age);
261    println!("  Active: {}", loaded_user.is_active);
262    println!("  Score: {}", loaded_user.score);
263
264    // Verify data integrity
265    assert_eq!(user, loaded_user);
266    println!("Data integrity verification: PASSED");
267
268    Ok(())
269}
270
271/// Demonstrate serialization with collections and optional fields
272fn demonstrate_app_settings_serialization() -> Result<(), Box<dyn std::error::Error>> {
273    println!("\n--- App Settings Serialization ---");
274
275    // Create app settings with collections and optional fields
276    let mut env_vars = HashMap::new();
277    env_vars.insert("LOG_LEVEL".to_string(), "info".to_string());
278    env_vars.insert("PORT".to_string(), "8080".to_string());
279    env_vars.insert("HOST".to_string(), "localhost".to_string());
280
281    let settings = AppSettings {
282        app_name: "Train Station Example".to_string(),
283        version: "1.0.0".to_string(),
284        debug_mode: true,
285        max_connections: 100,
286        timeout_seconds: 30.5,
287        features: vec![
288            "authentication".to_string(),
289            "logging".to_string(),
290            "metrics".to_string(),
291        ],
292        environment_vars: env_vars,
293        optional_database_url: Some("postgresql://localhost:5432/mydb".to_string()),
294    };
295
296    println!("Original app settings:");
297    println!("  App Name: {}", settings.app_name);
298    println!("  Version: {}", settings.version);
299    println!("  Debug Mode: {}", settings.debug_mode);
300    println!("  Max Connections: {}", settings.max_connections);
301    println!("  Timeout: {} seconds", settings.timeout_seconds);
302    println!("  Features: {:?}", settings.features);
303    println!("  Environment Variables: {:?}", settings.environment_vars);
304    println!("  Database URL: {:?}", settings.optional_database_url);
305
306    // Serialize to binary format for efficient storage
307    let binary_data = settings.to_binary()?;
308    println!("\nSerialized to binary: {} bytes", binary_data.len());
309
310    // Save to binary file
311    settings.save_binary("temp_app_settings.bin")?;
312    println!("Saved to file: temp_app_settings.bin");
313
314    // Load from binary file
315    let loaded_settings = AppSettings::load_binary("temp_app_settings.bin")?;
316    println!("\nLoaded app settings:");
317    println!("  App Name: {}", loaded_settings.app_name);
318    println!("  Version: {}", loaded_settings.version);
319    println!("  Debug Mode: {}", loaded_settings.debug_mode);
320    println!("  Features count: {}", loaded_settings.features.len());
321    println!(
322        "  Environment variables count: {}",
323        loaded_settings.environment_vars.len()
324    );
325
326    // Verify data integrity
327    assert_eq!(settings, loaded_settings);
328    println!("Data integrity verification: PASSED");
329
330    Ok(())
331}
332
333/// Demonstrate format comparison between JSON and binary
334fn demonstrate_format_comparison() -> Result<(), Box<dyn std::error::Error>> {
335    println!("\n--- Format Comparison ---");
336
337    let user = UserProfile {
338        id: 98765,
339        username: "bob_builder".to_string(),
340        email: "bob@construction.com".to_string(),
341        age: 35,
342        is_active: false,
343        score: 87.2,
344    };
345
346    // Save in both formats
347    user.save_json("temp_format_comparison.json")?;
348    user.save_binary("temp_format_comparison.bin")?;
349
350    // Compare file sizes
351    let json_size = fs::metadata("temp_format_comparison.json")?.len();
352    let binary_size = fs::metadata("temp_format_comparison.bin")?.len();
353
354    println!("Format comparison for UserProfile:");
355    println!("  JSON file size: {} bytes", json_size);
356    println!("  Binary file size: {} bytes", binary_size);
357    println!(
358        "  Size ratio (JSON/Binary): {:.2}x",
359        json_size as f64 / binary_size as f64
360    );
361
362    // Demonstrate readability
363    let json_content = fs::read_to_string("temp_format_comparison.json")?;
364    println!("\nJSON format (human-readable):");
365    println!("{}", json_content);
366
367    println!("\nBinary format (first 32 bytes as hex):");
368    let binary_content = fs::read("temp_format_comparison.bin")?;
369    for (i, byte) in binary_content.iter().take(32).enumerate() {
370        if i % 16 == 0 && i > 0 {
371            println!();
372        }
373        print!("{:02x} ", byte);
374    }
375    println!("\n... ({} total bytes)", binary_content.len());
376
377    // Load and verify both formats produce identical results
378    let json_loaded = UserProfile::load_json("temp_format_comparison.json")?;
379    let binary_loaded = UserProfile::load_binary("temp_format_comparison.bin")?;
380
381    assert_eq!(json_loaded, binary_loaded);
382    println!("\nFormat consistency verification: PASSED");
383
384    Ok(())
385}
386
387/// Demonstrate roundtrip verification with multiple data variations
388fn demonstrate_roundtrip_verification() -> Result<(), Box<dyn std::error::Error>> {
389    println!("\n--- Roundtrip Verification ---");
390
391    // Test various data patterns
392    let test_users = [
393        UserProfile {
394            id: 0,
395            username: "".to_string(),
396            email: "empty@test.com".to_string(),
397            age: 0,
398            is_active: false,
399            score: 0.0,
400        },
401        UserProfile {
402            id: u32::MAX,
403            username: "maximal_user_with_very_long_name_123456789".to_string(),
404            email: "test@verylongdomainname.example.org".to_string(),
405            age: i32::MAX,
406            is_active: true,
407            score: 999999.5,
408        },
409        UserProfile {
410            id: 42,
411            username: "unicode_tëst_🦀".to_string(),
412            email: "unicode@tëst.com".to_string(),
413            age: 25,
414            is_active: true,
415            score: -123.456,
416        },
417    ];
418
419    println!(
420        "Testing roundtrip serialization with {} variations:",
421        test_users.len()
422    );
423
424    for (i, user) in test_users.iter().enumerate() {
425        println!(
426            "  Test case {}: ID={}, Username='{}'",
427            i + 1,
428            user.id,
429            user.username
430        );
431
432        // JSON roundtrip
433        let json_data = user.to_json()?;
434        let json_parsed = UserProfile::from_json(&json_data)?;
435        assert_eq!(*user, json_parsed);
436
437        // Binary roundtrip
438        let binary_data = user.to_binary()?;
439        let binary_parsed = UserProfile::from_binary(&binary_data)?;
440        assert_eq!(*user, binary_parsed);
441
442        println!("    JSON roundtrip: PASSED");
443        println!("    Binary roundtrip: PASSED");
444    }
445
446    println!("All roundtrip tests: PASSED");
447
448    Ok(())
449}
450
451/// Demonstrate field access patterns and validation
452fn demonstrate_field_access_patterns() -> Result<(), Box<dyn std::error::Error>> {
453    println!("\n--- Field Access Patterns ---");
454
455    let settings = AppSettings {
456        app_name: "Field Test App".to_string(),
457        version: "2.1.0".to_string(),
458        debug_mode: false,
459        max_connections: 50,
460        timeout_seconds: 15.0,
461        features: vec!["basic".to_string(), "advanced".to_string()],
462        environment_vars: HashMap::new(),
463        optional_database_url: None,
464    };
465
466    // Convert to JSON to inspect structure
467    let json_data = settings.to_json()?;
468    println!("JSON structure for field inspection:");
469
470    // Count approximate fields by counting field separators
471    let field_count = json_data.matches(':').count();
472    println!("Estimated fields: {}", field_count);
473
474    // Show structure (first few lines)
475    let lines: Vec<&str> = json_data.lines().take(5).collect();
476    for line in lines {
477        println!("  {}", line.trim());
478    }
479    if json_data.lines().count() > 5 {
480        println!("  ... ({} more lines)", json_data.lines().count() - 5);
481    }
482
483    // Demonstrate optional field handling
484    println!("\nOptional field handling:");
485    println!(
486        "  Database URL is None: {}",
487        settings.optional_database_url.is_none()
488    );
489
490    // Create version with optional field populated
491    let settings_with_db = AppSettings {
492        optional_database_url: Some("sqlite:///tmp/test.db".to_string()),
493        ..settings.clone()
494    };
495
496    println!(
497        "  Database URL with value: {:?}",
498        settings_with_db.optional_database_url
499    );
500
501    // Verify both versions serialize/deserialize correctly
502    let json_none = settings.to_json()?;
503    let json_some = settings_with_db.to_json()?;
504
505    let parsed_none = AppSettings::from_json(&json_none)?;
506    let parsed_some = AppSettings::from_json(&json_some)?;
507
508    assert_eq!(settings, parsed_none);
509    assert_eq!(settings_with_db, parsed_some);
510    assert!(parsed_none.optional_database_url.is_none());
511    assert!(parsed_some.optional_database_url.is_some());
512
513    println!("Optional field serialization: PASSED");
514
515    Ok(())
516}
517
518/// Clean up temporary files created during the example
519fn cleanup_temp_files() -> Result<(), Box<dyn std::error::Error>> {
520    println!("\n--- Cleanup ---");
521
522    let files_to_remove = [
523        "temp_user_profile.json",
524        "temp_app_settings.bin",
525        "temp_format_comparison.json",
526        "temp_format_comparison.bin",
527    ];
528
529    for file in &files_to_remove {
530        if fs::metadata(file).is_ok() {
531            fs::remove_file(file)?;
532            println!("Removed: {}", file);
533        }
534    }
535
536    println!("Cleanup completed");
537    Ok(())
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn test_user_profile_serialization() {
546        let user = UserProfile {
547            id: 123,
548            username: "test_user".to_string(),
549            email: "test@example.com".to_string(),
550            age: 30,
551            is_active: true,
552            score: 88.5,
553        };
554
555        // Test JSON roundtrip
556        let json_data = user.to_json().unwrap();
557        let parsed_user = UserProfile::from_json(&json_data).unwrap();
558        assert_eq!(user, parsed_user);
559
560        // Test binary roundtrip
561        let binary_data = user.to_binary().unwrap();
562        let parsed_user = UserProfile::from_binary(&binary_data).unwrap();
563        assert_eq!(user, parsed_user);
564    }
565
566    #[test]
567    fn test_app_settings_serialization() {
568        let mut env_vars = HashMap::new();
569        env_vars.insert("TEST".to_string(), "value".to_string());
570
571        let settings = AppSettings {
572            app_name: "Test App".to_string(),
573            version: "1.0.0".to_string(),
574            debug_mode: true,
575            max_connections: 10,
576            timeout_seconds: 5.0,
577            features: vec!["test".to_string()],
578            environment_vars: env_vars,
579            optional_database_url: Some("test://db".to_string()),
580        };
581
582        // Test JSON roundtrip
583        let json_data = settings.to_json().unwrap();
584        let parsed_settings = AppSettings::from_json(&json_data).unwrap();
585        assert_eq!(settings, parsed_settings);
586
587        // Test binary roundtrip
588        let binary_data = settings.to_binary().unwrap();
589        let parsed_settings = AppSettings::from_binary(&binary_data).unwrap();
590        assert_eq!(settings, parsed_settings);
591    }
592
593    #[test]
594    fn test_optional_field_handling() {
595        let settings_none = AppSettings {
596            app_name: "Test".to_string(),
597            version: "1.0.0".to_string(),
598            debug_mode: false,
599            max_connections: 1,
600            timeout_seconds: 1.0,
601            features: vec![],
602            environment_vars: HashMap::new(),
603            optional_database_url: None,
604        };
605
606        let settings_some = AppSettings {
607            optional_database_url: Some("db://test".to_string()),
608            ..settings_none.clone()
609        };
610
611        // Test both variants
612        let json_none = settings_none.to_json().unwrap();
613        let json_some = settings_some.to_json().unwrap();
614
615        let parsed_none = AppSettings::from_json(&json_none).unwrap();
616        let parsed_some = AppSettings::from_json(&json_some).unwrap();
617
618        assert!(parsed_none.optional_database_url.is_none());
619        assert!(parsed_some.optional_database_url.is_some());
620        assert_eq!(settings_none, parsed_none);
621        assert_eq!(settings_some, parsed_some);
622    }
623}