serini 
A serde-based INI file parser for Rust that supports automatic serialization and deserialization of structs to INI format.
Features
- 🚀 Automatic serialization - Convert Rust structs to INI format
- 🔧 Type-safe deserialization - Parse INI files into strongly-typed Rust structs
- 📁 Section support - Nested structs become INI sections automatically
- 💭 Option handling -
None values are serialized as commented lines if serialization is not skipped using #[serde(skip)] or similiar
- 🛡️ Escape sequences - Properly handles special characters in values
- 🏷️ Serde integration - Supports serde attributes like
#[serde(rename = "...")]
Installation
Add this to your Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serini = "0.2"
Quick Example
use serde::{Deserialize, Serialize};
use serini::{from_str, to_string};
#[derive(Debug, Serialize, Deserialize)]
struct Config {
name: String,
port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
debug: Option<usize>,
database: Database,
}
#[derive(Debug, Serialize, Deserialize)]
struct Database {
host: String,
username: String,
password: Option<String>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config {
name: "My App".to_string(),
port: 8080,
debug: None,
database: Database {
host: "localhost".to_string(),
username: "admin".to_string(),
password: None,
},
};
let ini_string = to_string(&config)?;
println!("{}", ini_string);
let parsed: Config = from_str(&ini_string)?;
assert_eq!(config, parsed);
Ok(())
}
Output:
name = My App
port = 8080
[database]
host = localhost
username = admin
; password =
Usage Guide
Basic Types
serini supports all basic Rust types:
#[derive(Serialize, Deserialize)]
struct Settings {
max_connections: u32, retry_count: i8,
timeout: f64,
debug_mode: bool,
server_name: String,
description: Option<String>, }
Sections from Nested Structs
Nested structs automatically become INI sections:
#[derive(Serialize, Deserialize)]
struct App {
version: String,
server: ServerConfig,
database: DbConfig,
}
#[derive(Serialize, Deserialize)]
struct ServerConfig {
host: String,
port: u16,
}
#[derive(Serialize, Deserialize)]
struct DbConfig {
url: String,
pool_size: u32,
}
Produces:
version = 1.0.0
[server]
host = 0.0.0.0
port = 8080
[database]
url = postgres://localhost/mydb
pool_size = 10
Self-Referential Structs
serini supports self-referential structs using Option<Box<T>>, allowing sections to override values from the root configuration:
#[derive(Debug, Serialize, Deserialize)]
struct Config {
speed: f32,
movie: Option<Box<Config>>,
anime: Option<Box<Config>>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let ini_str = r#"
speed = 1
[anime]
speed = 1.5
[movie]
speed = 2
"#;
let config: Config = from_str(ini_str)?;
println!("Default speed: {}", config.speed);
if let Some(anime_config) = &config.anime {
println!("Anime speed: {}", anime_config.speed);
}
if let Some(movie_config) = &config.movie {
println!("Movie speed: {}", movie_config.speed);
}
let new_config = Config {
speed: 1.0,
anime: Some(Box::new(Config {
speed: 1.5,
anime: None,
movie: None,
})),
movie: Some(Box::new(Config {
speed: 2.0,
anime: None,
movie: None,
})),
};
let ini_output = to_string(&new_config)?;
println!("{}", ini_output);
Ok(())
}
Output:
speed = 1
[anime]
speed = 1.5
[movie]
speed = 2
This pattern is useful for configuration files where different profiles or modes can override default settings.
Field Renaming
Use serde's rename attribute:
#[derive(Serialize, Deserialize)]
struct Config {
#[serde(rename = "app-name")]
app_name: String,
#[serde(rename = "log-level")]
log_level: String,
}
Escape Sequences
Special characters are automatically escaped:
| Character |
Escaped |
\ |
\\ |
| newline |
\n |
| tab |
\t |
" |
\" |
; |
\; |
# |
\# |
Real-World Example
use serde::{Deserialize, Serialize};
use serini::{from_str, to_string};
#[derive(Debug, Serialize, Deserialize)]
struct ServerConfig {
#[serde(rename = "app-name")]
app_name: String,
environment: String,
http: HttpConfig,
database: DatabaseConfig,
redis: Option<RedisConfig>,
}
#[derive(Debug, Serialize, Deserialize)]
struct HttpConfig {
host: String,
port: u16,
#[serde(rename = "request-timeout")]
request_timeout: u64,
#[serde(rename = "enable-tls")]
enable_tls: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct DatabaseConfig {
url: String,
#[serde(rename = "max-connections")]
max_connections: u32,
#[serde(rename = "connection-timeout")]
connection_timeout: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct RedisConfig {
url: String,
#[serde(rename = "connection-pool")]
connection_pool: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ServerConfig {
app_name: "my-awesome-app".to_string(),
environment: "production".to_string(),
http: HttpConfig {
host: "0.0.0.0".to_string(),
port: 443,
request_timeout: 30,
enable_tls: true,
},
database: DatabaseConfig {
url: "postgres://user:pass@localhost/mydb".to_string(),
max_connections: 100,
connection_timeout: 5,
},
redis: None,
};
let ini = to_string(&config)?;
println!("{}", ini);
Ok(())
}
Output:
app-name = my-awesome-app
environment = production
[http]
host = 0.0.0.0
port = 443
request-timeout = 30
enable-tls = true
[database]
url = postgres://user:pass@localhost/mydb
max-connections = 100
connection-timeout = 5
; redis =
API
pub fn to_string<T: Serialize>(value: &T) -> Result<String, Error>
pub fn from_str<'a, T: Deserialize<'a>>(s: &'a str) -> Result<T, Error>
Error Types
serini uses a custom error type with helpful error messages:
use serini::Error;
match result {
Err(Error::InvalidValue { typ, value }) => {
eprintln!("Invalid {} value: {}", typ, value);
}
Err(Error::UnsupportedFeature(feature)) => {
eprintln!("Unsupported feature: {}", feature);
}
Err(e) => eprintln!("Error: {}", e),
Ok(_) => {}
}
Limitations
The following types are not supported:
- Sequences (Vec, arrays)
- Tuples and tuple structs
- Enums with variants
- Maps (HashMap, BTreeMap)
- Nested arrays or complex data structures
Why serini?
- Simple - Minimal API with just two functions
- Type-safe - Leverages Rust's type system and serde
- Automatic - No manual parsing or writing required
- Flexible - Supports Options, escaping, and field renaming
- Fast - Zero-copy deserialization where possible
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.