Project 1: Custom INI Serialization Engine
Problem Statement
Build a complete serialization and deserialization engine for the INI file format. Your engine will use Serde’s core traits (Serialize, Deserialize, Serializer, Deserializer) to convert Rust structs to and from INI-formatted strings. The final product should be able to handle sections, key-value pairs, and basic data types like strings, numbers, and booleans.
Use Cases
- Application Configuration: Many applications (especially in the Windows ecosystem) use
.inifiles for human-readable configuration. - Game Development: Simple game settings (graphics, controls) are often stored in INI format.
- Embedded Systems: Lightweight configuration for devices where a full JSON or TOML parser is too heavy.
- Legacy System Integration: Interfacing with older systems that use INI as their data exchange format.
Why It Matters
Understanding Serde’s Core: Implementing Serializer and Deserializer demystifies how Serde works. You’ll learn how data structures are broken down into primitives and reassembled, giving you the power to support any data format.
Performance & Control: While libraries exist, writing your own deserializer can be highly optimized for specific use cases. A parser that only handles the expected format can be faster and smaller than a general-purpose one.
Extending the Ecosystem: The skills learned here allow you to contribute new format support to the Rust ecosystem. If you need to interface with a proprietary or obscure format, you’ll know exactly how to do it.
A simple INI file looks like this:
[database]
host = localhost
port = 5432
user = admin
[server]
enabled = true
Milestone 1: Data Model for INI
Introduction
Before we can serialize or deserialize, we need a Rust representation of an INI file’s structure. This milestone focuses on creating the data structures that will hold the parsed INI data in memory.
Why Start Here: A solid data model is the foundation of any parser or serializer. It defines the boundaries and capabilities of your engine. We will represent the INI file as a map of section names to another map of key-value pairs.
Architecture
Structs:
Ini: The top-level container for an INI file.- Field
sections: HashMap<String, HashMap<String, String>>- A map where keys are section names (e.g., “database”) and values are another map containing the key-value pairs for that section.
- Field
Key Functions:
impl Ini::new() -> Self- Creates an emptyIniobject.impl Ini::get(&self, section: &str, key: &str) -> Option<&String>- Retrieves a value.impl Ini::set(&mut self, section: String, key: String, value: String)- Inserts or updates a value.
Role Each Plays:
Ini: Represents the entire INI file in a structured way, making it easy to query and manipulate.HashMap<String, ...>: An efficient choice for looking up sections and keys by name.
Checkpoint Tests
#![allow(unused)]
fn main() {
#[test]
fn test_ini_data_model() {
let mut ini = Ini::new();
ini.set("database".to_string(), "host".to_string(), "localhost".to_string());
ini.set("database".to_string(), "port".to_string(), "5432".to_string());
ini.set("server".to_string(), "enabled".to_string(), "true".to_string());
assert_eq!(ini.get("database", "host"), Some(&"localhost".to_string()));
assert_eq!(ini.get("database", "port"), Some(&"5432".to_string()));
assert_eq!(ini.get("server", "enabled"), Some(&"true".to_string()));
assert_eq!(ini.get("database", "nonexistent"), None);
}
}
Starter Code
#![allow(unused)]
fn main() {
use std::collections::HashMap;
#[derive(Debug, PartialEq)]
pub struct Ini {
pub sections: HashMap<String, HashMap<String, String>>,
}
impl Ini {
pub fn new() -> Self {
// TODO: Initialize an empty Ini struct.
todo!("Implement Ini::new");
}
pub fn get(&self, section: &str, key: &str) -> Option<&String> {
// TODO: Get a value from the specified section and key.
// Hint: Use HashMap's .get() method twice.
todo!("Implement Ini::get");
}
pub fn set(&mut self, section: String, key: String, value: String) {
// TODO: Insert a value into the specified section and key.
// Hint: Use HashMap's .entry().or_default() pattern.
todo!("Implement Ini::set");
}
}
}
Milestone 2: A Manual INI Deserializer
Introduction
Why Milestone 1 Isn’t Enough: We have a data model, but we can’t populate it from a string yet. This milestone is about writing a simple, manual parser that reads an INI-formatted string and populates our Ini struct.
The Improvement: This step bridges the gap between raw text and our structured data model. It forces us to handle the INI format’s syntax rules, like section headers ([section]) and key-value pairs (key = value).
Architecture
Key Functions:
fn from_str(s: &str) -> Result<Ini, String>- The core parsing function.
Role Each Plays:
from_str: Iterates over the lines of the input string, maintaining the current section context. It parses each line and populates anIniobject. This function will be the foundation for ourserde::Deserializerlater.
Checkpoint Tests
#![allow(unused)]
fn main() {
#[test]
fn test_manual_parser() {
let ini_str = "
[database]
host = localhost
port = 5432
[server]
enabled = true
";
let ini = from_str(ini_str).expect("Failed to parse");
assert_eq!(ini.get("database", "host"), Some(&"localhost".to_string()));
assert_eq!(ini.get("database", "port"), Some(&"5432".to_string()));
assert_eq!(ini.get("server", "enabled"), Some(&"true".to_string()));
}
#[test]
fn test_parser_invalid_format() {
let invalid_ini = "just some text";
assert!(from_str(invalid_ini).is_err());
let no_section = "key = value";
assert!(from_str(no_section).is_err(), "Keys must be under a section");
}
}
Starter Code
#![allow(unused)]
fn main() {
// Assume Ini struct from Milestone 1 is available
pub fn from_str(s: &str) -> Result<Ini, String> {
let mut ini = Ini::new();
let mut current_section = None;
for line in s.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with(';') {
// Ignore empty lines and comments
continue;
}
// TODO: Handle section headers `[section_name]`
// - If a line is a section header, update `current_section`.
// - Remember to handle the closing `]` bracket.
// TODO: Handle key-value pairs `key = value`
// - If a line is a key-value pair, split it at the '='.
// - Ensure a section has been declared before adding a key-value pair.
// - Add the pair to the `current_section` in the `ini` object.
// TODO: Return an error for malformed lines.
}
Ok(ini)
}
}
Implementation Hints:
- Use
line.starts_with('[')andline.ends_with(']')to detect section headers. - Use
line.split_once('=')to parse key-value pairs. - Keep track of the current section name in a variable. If a key-value pair is found before any section header, it’s an error.
Milestone 3: Implementing serde::Deserializer
Introduction
Why Milestone 2 Isn’t Enough: Our manual parser only produces Ini. It can’t deserialize into any Rust struct. By implementing serde::Deserializer, we can leverage the full power of Serde to deserialize our Ini representation into any struct that derives Deserialize.
The Improvement: We are making our parser generic. Instead of a one-off Ini parser, we’re creating a de::from_str function that works just like serde_json::from_str.
Architecture
Structs:
Deserializer<'de>: Our custom deserializer struct. It will hold theInidata we parsed in the previous step.
Key Traits & Functions:
impl<'de> de::Deserializer<'de> for Deserializer<'de>: The main implementation block where we teach Serde how to interpret ourInidata model.pub fn from_str<'a, T>(s: &'a str) -> Result<T, Error>whereT: Deserialize<'a>: The public-facing function users will call.
Role Each Plays:
Deserializer: The state machine that Serde calls into. Serde will say “I expect a struct”, and ourDeserializerwill provide it by traversing ourInidata.de::from_str: The entry point. It first does a manual parse into our intermediateInirepresentation, then passes that to ourDeserializerto drive theT::deserializeprocess.
Checkpoint Tests
#![allow(unused)]
fn main() {
use serde::Deserialize;
#[derive(Deserialize, Debug, PartialEq)]
struct Config {
server: ServerConfig,
database: DbConfig,
}
#[derive(Deserialize, Debug, PartialEq)]
struct ServerConfig {
enabled: bool,
}
#[derive(Deserialize, Debug, PartialEq)]
struct DbConfig {
host: String,
port: u16,
}
#[test]
fn test_serde_deserialization() {
let ini_str = "
[server]
enabled = true
[database]
host = localhost
port = 5432
";
let config: Config = from_str(ini_str).unwrap();
assert_eq!(config, Config {
server: ServerConfig { enabled: true },
database: DbConfig { host: "localhost".to_string(), port: 5432 },
});
}
}
Starter Code
#![allow(unused)]
fn main() {
use serde::de::{self, Deserializer as SerdeDeserializer, MapAccess, Visitor};
use serde::Deserialize;
// Error type for our deserializer
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("INI parsing error: {0}")]
Message(String),
// ... other error variants
}
// Our custom Deserializer
pub struct Deserializer<'de> {
// We'll use our `Ini` data model as the input
ini: Ini,
}
impl<'de> Deserializer<'de> {
pub fn from_ini(ini: Ini) -> Self {
Deserializer { ini }
}
}
// Public API
pub fn from_str<'a, T>(s: &'a str) -> Result<T, Error>
where
T: Deserialize<'a>,
{
// First, parse the string into our intermediate `Ini` representation
let ini = manual_from_str(s).map_err(Error::Message)?;
// Then, use our custom deserializer
let mut deserializer = Deserializer::from_ini(ini);
T::deserialize(&mut deserializer)
}
// The core Serde implementation
impl<'de, 'a> SerdeDeserializer<'de> for &'a mut Deserializer<'de> {
type Error = Error;
// `deserialize_any` is not supported for this format
fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(Error::Message("deserialize_any is not supported".to_string()))
}
// We are deserializing a struct (the top-level Config) from the INI file
fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
// TODO: The visitor expects a `MapAccess`. We need to create something
// that wraps our `Ini` and implements `MapAccess`.
// This is the most complex part!
todo!("Implement deserialize_struct");
}
// Handle primitive types. These will be called when deserializing
// the values within the sections (e.g., `enabled = true`).
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error> {
// This is tricky because the Deserializer doesn't know which
// key it's on. The `MapAccess` implementation will need to handle this.
todo!("Forward to MapAccess");
}
// ... implement for i64, u64, f64, string, etc.
}
// You will also need a struct that implements `MapAccess` for both the
// top-level sections and the key-value pairs within them.
}
Milestone 4: A Manual INI Serializer
Introduction
Why We Need This: We can now read INI files, but we can’t write them. This milestone focuses on the reverse process: taking our Ini data structure and converting it back into a formatted string.
The Improvement: This gives us a complete round-trip capability (read, modify, write). The logic developed here will form the basis of our serde::Serializer implementation.
Architecture
Key Functions:
impl Ini::to_string(&self) -> String- The core serialization function.
Role Each Plays:
to_string: Iterates through the sections and key-value pairs in theInistruct and builds aStringthat conforms to the INI format rules.
Checkpoint Tests
#![allow(unused)]
fn main() {
#[test]
fn test_manual_serializer() {
let mut ini = Ini::new();
ini.set("database".to_string(), "host".to_string(), "localhost".to_string());
ini.set("database".to_string(), "port".to_string(), "5432".to_string());
ini.set("server".to_string(), "enabled".to_string(), "true".to_string());
let expected = "\
[database]
host = localhost
port = 5432
[server]
enabled = true
";
let actual = ini.to_string();
// Note: HashMaps don't guarantee order, so we parse the output back
// and check for equality of the data model.
let reparsed_ini = manual_from_str(&actual).unwrap();
assert_eq!(ini, reparsed_ini);
}
}
Starter Code
#![allow(unused)]
fn main() {
// In impl Ini { ... }
pub fn to_string(&self) -> String {
let mut result = String::new();
// TODO: Iterate over the sections in `self.sections`.
// The iteration order doesn't matter for the INI format.
for (section_name, section_values) in &self.sections {
// TODO: Append the section header to the result string, e.g., `[section_name]\n`.
// TODO: Iterate over the key-value pairs in `section_values`.
for (key, value) in section_values {
// TODO: Append the key-value pair, e.g., `key = value\n`.
}
// TODO: Add a blank line after each section for readability.
}
result
}
}
Implementation Hints:
- Use a
StringBuilderorString::push_strfor efficient string construction. - Remember to add newlines (
\n) after each line and section.
Milestone 5: Implementing serde::Serializer
Introduction
Why Milestone 4 Isn’t Enough: Our manual serializer only works with our intermediate Ini struct. To serialize any Rust struct that derives Serialize, we need to implement Serde’s Serializer trait.
The Improvement: This turns our one-off serializer into a generic ser::to_string function, capable of serializing a wide variety of Rust types into the INI format.
Architecture
Structs:
Serializer: Our custom serializer. It will write the formatted output to aString.- Helper structs for
serialize_structandserialize_map.
Key Traits & Functions:
impl ser::Serializer for &mut Serializer: The main implementation block where we define how to handle Rust types (bools, strings, structs, etc.).pub fn to_string<T>(value: &T) -> Result<String, Error>whereT: Serialize: The public-facing function.
Role Each Plays:
Serializer: The state machine that receives Rust data types from Serde and writes them out as INI-formatted text.ser::to_string: The entry point. It creates aSerializerand callsvalue.serialize()to start the process.
Checkpoint Tests
#![allow(unused)]
fn main() {
use serde::Serialize;
#[derive(Serialize)]
struct Config {
server: ServerConfig,
database: DbConfig,
}
#[derive(Serialize)]
struct ServerConfig {
enabled: bool,
}
#[derive(Serialize)]
struct DbConfig {
host: String,
port: u16,
}
#[test]
fn test_serde_serialization() {
let config = Config {
server: ServerConfig { enabled: true },
database: DbConfig {
host: "localhost".to_string(),
port: 5432,
},
};
let ini_string = to_string(&config).unwrap();
// Again, parse back to test correctness due to order.
let deserialized_config: crate::milestone3::Config = crate::milestone3::from_str(&ini_string).unwrap();
assert_eq!(deserialized_config.server.enabled, true);
assert_eq!(deserialized_config.database.host, "localhost");
assert_eq!(deserialized_config.database.port, 5432);
}
}
Starter Code
#![allow(unused)]
fn main() {
use serde::ser;
// Our custom Serializer
pub struct Serializer {
output: String,
current_section: Option<String>,
}
// Public API
pub fn to_string<T>(value: &T) -> Result<String, Error>
where
T: ser::Serialize,
{
let mut serializer = Serializer { output: String::new(), current_section: None };
value.serialize(&mut serializer)?;
Ok(serializer.output)
}
impl ser::Serializer for &mut Serializer {
type Ok = ();
type Error = Error;
// Helper types for compound values.
type SerializeStruct = Self;
// ... other helpers
// This will be called for primitive values like `true`, `5432`, `"localhost"`.
fn serialize_str(self, v: &str) -> Result<(), Error> {
// TODO: Append the string value to the current line.
// This is tricky: we need to know the key first. The `SerializeStruct`
// implementation will handle writing `key = ` before calling this.
self.output.push_str(v);
self.output.push('\n');
Ok(())
}
// ... implement serialize_bool, serialize_u64, etc. They will convert the
// value to a string and call `serialize_str`.
// This is called for the top-level `Config` struct.
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
// The `Config` struct's fields are the section names.
// We will return `self` and let `serialize_field` handle it.
Ok(self)
}
// Other methods can be left as `unsupported`.
}
// This is where the magic happens for structs.
impl ser::SerializeStruct for &mut Serializer {
type Ok = ();
type Error = Error;
// Called for each field of a struct.
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + ser::Serialize,
{
// If we're at the top level, the `key` is a section name.
if self.current_section.is_none() {
// TODO: Start a new section. Store `key` as the current section name.
// Write the `[key]` header to the output.
// Then, serialize the `value` (which is another struct).
} else {
// If we are already in a section, the `key` is a key in a key-value pair.
// TODO: Write `key = ` to the output.
// Then, serialize the `value` (which is a primitive).
}
Ok(())
}
// Called after all fields are serialized.
fn end(self) -> Result<(), Error> {
// If we just finished a section, add a newline and reset current_section.
if self.current_section.is_some() {
self.output.push('\n');
self.current_section = None;
}
Ok(())
}
}
}
Milestone 6: Handling Comments and Whitespace
Introduction
Why Milestone 5 Isn’t Enough: Our current implementation is functional but rigid. Real-world INI files often contain comments (lines starting with ; or #) and extra whitespace, which our parser currently handles but our serializer doesn’t write.
The Improvement: This milestone is about polishing the engine to gracefully handle and even preserve comments and formatting, making it more robust and user-friendly.
Architecture
Changes to Data Model:
- Modify the
Inistruct to store comments and the order of sections/keys. Instead ofHashMap, we could useVec<(String, String)>for key-value pairs andVec<(String, Section)>for sections to preserve order. Comments could be stored in a special field. For this project, we will focus on a simpler goal: adding comments programmatically.
Serializer/Deserializer Enhancements:
- Deserializer: Modify the manual parser to ignore lines starting with
;or#. - Serializer: Add a method
add_commentto ourInistruct to insert comments before sections or keys. Theto_stringmethod would then write these out.
Checkpoint Tests
#![allow(unused)]
fn main() {
#[test]
fn test_parser_with_comments() {
let ini_str = "
; Database configuration
[database]
host = localhost ; The server hostname
port = 5432
";
let ini = manual_from_str(ini_str).unwrap();
assert_eq!(ini.get("database", "host"), Some(&"localhost".to_string()));
assert!(ini.to_string().contains("[database]"));
}
#[test]
fn test_serializer_with_programmatic_comments() {
let mut ini = Ini::new();
ini.set_with_comment(
"database".to_string(),
"host".to_string(),
"localhost".to_string(),
Some("The server hostname".to_string())
);
let output = ini.to_string();
assert!(output.contains("; The server hostname"));
}
}
Starter Code
#![allow(unused)]
fn main() {
// Modify the `Ini` struct to support comments.
// A simple way is to change the value type in the map.
// pub sections: HashMap<String, HashMap<String, (String, Option<String>)>>
// In the manual parser:
for line in s.lines() {
let line = line.trim();
// Your existing logic...
// When parsing a key-value pair, check for a comment.
let (pair, comment) = line.split_once(';').unwrap_or((line, ""));
// ... then parse the pair
}
// In the manual serializer:
for (key, (value, comment)) in section_values {
let mut line = format!("{} = {}", key, value);
if let Some(c) = comment {
line.push_str(" ; ");
line.push_str(c);
}
line.push('\n');
result.push_str(&line);
}
}
Complete Working Example
Here is a complete, simplified implementation covering the core concepts of the milestones. Note that a production-ready library would have more extensive error handling and would be more feature-rich. This example focuses on demonstrating the Serializer and Deserializer implementations.
// Add to Cargo.toml:
// serde = { version = "1.0", features = ["derive"] }
// thiserror = "1.0"
use serde::{de, ser};
use std::collections::HashMap;
// --- Data Model (Milestone 1) ---
#[derive(Debug, PartialEq, Clone)]
pub struct Ini {
pub sections: HashMap<String, HashMap<String, String>>,
}
impl Ini {
pub fn new() -> Self {
Ini {
sections: HashMap::new(),
}
}
pub fn get(&self, section: &str, key: &str) -> Option<&String> {
self.sections.get(section).and_then(|s| s.get(key))
}
pub fn set(&mut self, section: String, key: String, value: String) {
self.sections
.entry(section)
.or_default()
.insert(key, value);
}
}
// --- Error Type ---
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("INI parsing error: {0}")]
Message(String),
#[error("Unsupported type")]
Unsupported,
}
impl de::Error for Error {
fn custom<T: std::fmt::Display>(msg: T) -> Self {
Error::Message(msg.to_string())
}
}
impl ser::Error for Error {
fn custom<T: std::fmt::Display>(msg: T) -> Self {
Error::Message(msg.to_string())
}
}
// --- Manual Deserializer (Milestone 2) ---
pub fn manual_from_str(s: &str) -> Result<Ini, String> {
let mut ini = Ini::new();
let mut current_section_name = None;
for line in s.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with(';') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
current_section_name = Some(line[1..line.len() - 1].to_string());
} else if let Some(name) = ¤t_section_name {
let parts: Vec<_> = line.splitn(2, '=').collect();
if parts.len() == 2 {
ini.set(name.clone(), parts[0].trim().to_string(), parts[1].trim().to_string());
} else {
return Err(format!("Malformed line: {}", line));
}
} else {
return Err("Line found outside of a section".to_string());
}
}
Ok(ini)
}
// --- Serde Deserializer (Milestone 3) ---
pub fn from_str<'a, T>(s: &'a str) -> Result<T, Error>
where
T: de::Deserialize<'a>,
{
let ini = manual_from_str(s).map_err(Error::Message)?;
let mut deserializer = Deserializer { ini: ini.clone() };
T::deserialize(&mut deserializer)
}
pub struct Deserializer {
ini: Ini,
}
impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer {
type Error = Error;
fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error> where V: de::Visitor<'de> {
Err(Error::Unsupported)
}
fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> where V: de::Visitor<'de> {
visitor.visit_map(SectionMapAccess {
iter: self.ini.sections.iter(),
current_section_map: None,
})
}
// Other deserialize_* methods are not needed at the top level
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map enum identifier ignored_any
}
}
// Helper to deserialize the map of sections
struct SectionMapAccess<'a> {
iter: std::collections::hash_map::Iter<'a, String, HashMap<String, String>>,
current_section_map: Option<&'a HashMap<String, String>>,
}
impl<'de, 'a> de::MapAccess<'de> for SectionMapAccess<'a> {
type Error = Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where K: de::DeserializeSeed<'de> {
if let Some((key, value)) = self.iter.next() {
self.current_section_map = Some(value);
let key_de = de::IntoDeserializer::into_deserializer(key.as_str());
seed.deserialize(key_de).map(Some)
} else {
Ok(None)
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where V: de::DeserializeSeed<'de> {
let section_map = self.current_section_map.take().unwrap();
let mut val_deserializer = ValueDeserializer { map: section_map.clone() };
seed.deserialize(&mut val_deserializer)
}
}
// We need another deserializer for the inner struct (the key-value pairs)
struct ValueDeserializer {
map: HashMap<String, String>,
}
impl<'de, 'a> de::Deserializer<'de> for &'a mut ValueDeserializer {
type Error = Error;
fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error> where V: de::Visitor<'de> {
Err(Error::Unsupported)
}
fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> where V: de::Visitor<'de> {
visitor.visit_map(KeyValueMapAccess {
iter: self.map.iter(),
current_value: None,
})
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map enum identifier ignored_any
}
}
struct KeyValueMapAccess<'a> {
iter: std::collections::hash_map::Iter<'a, String, String>,
current_value: Option<&'a String>,
}
impl<'de, 'a> de::MapAccess<'de> for KeyValueMapAccess<'a> {
type Error = Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error> where K: de::DeserializeSeed<'de> {
if let Some((key, value)) = self.iter.next() {
self.current_value = Some(value);
let key_de = de::IntoDeserializer::into_deserializer(key.as_str());
seed.deserialize(key_de).map(Some)
} else {
Ok(None)
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error> where V: de::DeserializeSeed<'de> {
let value = self.current_value.take().unwrap();
let val_de = de::IntoDeserializer::into_deserializer(value.as_str());
seed.deserialize(val_de)
}
}
// --- MAIN function to tie it all together ---
fn main() {
// --- Define a struct to deserialize into ---
#[derive(serde::Deserialize, Debug, PartialEq)]
struct Config {
server: ServerConfig,
database: DbConfig,
}
#[derive(serde::Deserialize, Debug, PartialEq)]
struct ServerConfig {
enabled: bool,
}
#[derive(serde::Deserialize, Debug, PartialEq)]
struct DbConfig {
host: String,
port: u16,
}
let ini_data = "
; App Config
[server]
enabled = true
[database]
host = db.example.com
port = 5432
";
let config: Config = from_str(ini_data).unwrap();
println!("Deserialized Config: {:#?}", config);
assert_eq!(config, Config {
server: ServerConfig { enabled: true },
database: DbConfig { host: "db.example.com".to_string(), port: 5432 }
});
println!("\nSuccessfully deserialized INI string into Rust struct!");
}
This project provides a deep dive into the mechanics of Serde. While complex, mastering these concepts allows you to integrate Rust with virtually any data format imaginable.