A configuration language and parser library for Rust, with a flexible mapper for converting between configuration files and Rust structs.
- Simple, intuitive configuration syntax
- A powerful parser with customizable options
- Automatic mapping between configuration and Rust structs
- Support for custom data types
- Comprehensive error handling
Add this to your Cargo.toml:
[dependencies]
confetti-rs = { version = "0.1.1", features = ["derive"] }The derive feature enables the derive macros for automatic configuration mapping.
Here's a simple example of how to use Confetti-rs:
use confetti_rs::{ConfMap, from_str, to_string};
use std::error::Error;
// Define a configuration structure
#[derive(ConfMap, Debug)]
struct ServerConfig {
host: String,
port: i32,
#[conf_map(name = "ssl-enabled")]
ssl_enabled: bool,
max_connections: Option<i32>,
}
fn main() -> Result<(), Box<dyn Error>> {
// Configuration string in Confetti syntax
let config_str = r#"
ServerConfig {
host "localhost";
port 8080;
ssl-enabled false;
max_connections 100;
}
"#;
// Parse the configuration
let server_config = from_str::<ServerConfig>(config_str)?;
println!("Loaded config: {:?}", server_config);
// Modify the configuration
let new_config = ServerConfig {
host: "0.0.0.0".to_string(),
port: 443,
ssl_enabled: true,
max_connections: Some(200),
};
// Serialize to a string
let serialized = to_string(&new_config)?;
println!("Serialized config:\n{}", serialized);
Ok(())
}Here's how to work with configuration files:
use confetti_rs::{ConfMap, from_file, to_file};
use std::path::Path;
use std::error::Error;
// Define application configuration structures
#[derive(ConfMap, Debug)]
struct DatabaseConfig {
url: String,
username: String,
password: String,
max_connections: i32,
}
#[derive(ConfMap, Debug)]
struct ServerConfig {
host: String,
port: i32,
#[conf_map(name = "worker-threads")]
worker_threads: i32,
database: DatabaseConfig,
}
fn main() -> Result<(), Box<dyn Error>> {
// Create a sample configuration
let initial_config = ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
worker_threads: 4,
database: DatabaseConfig {
url: "jdbc:postgresql://localhost:5432/mydb".to_string(),
username: "app_user".to_string(),
password: "secret".to_string(),
max_connections: 20,
},
};
// Save the configuration to a file
let config_path = Path::new("server.conf");
to_file(&initial_config, config_path)?;
println!("Created initial configuration file");
// Later, load the configuration from the file
let loaded_config = from_file::<ServerConfig, _>(config_path)?;
println!("Loaded server configuration:");
println!("Server: {}:{}", loaded_config.host, loaded_config.port);
println!("Worker threads: {}", loaded_config.worker_threads);
println!("Database URL: {}", loaded_config.database.url);
// The generated file will look like:
// ServerConfig {
// host "127.0.0.1";
// port 8080;
// worker-threads 4;
// database {
// url "jdbc:postgresql://localhost:5432/mydb";
// username "app_user";
// password "secret";
// max_connections 20;
// }
// }
Ok(())
}This library implements the Confetti configuration language specification, providing a robust and fully compliant parser for the Confetti language.
Confetti-rs uses a simple, readable syntax:
DirectiveName {
nested_directive "value";
another_directive 123;
block_directive {
setting true;
array 1, 2, 3, 4;
}
}
The ConfMap derive macro automatically implements the required traits for mapping between your structs and the configuration format:
#[derive(ConfMap, Debug)]
struct AppConfig {
name: String,
version: String,
#[conf_map(name = "max-connections")]
max_connections: i32,
}use confetti_rs::{ConfMap, from_str};
#[derive(ConfMap, Debug)]
struct ServerConfig {
host: String,
port: i32,
}
// Parse from a string
let config_str = r#"
ServerConfig {
host "localhost";
port 8080;
}
"#;
let config = from_str::<ServerConfig>(config_str).unwrap();
println!("Server at {}:{}", config.host, config.port);use confetti_rs::{ConfMap, from_file};
use std::path::Path;
#[derive(ConfMap, Debug)]
struct AppConfig {
name: String,
version: String,
}
// Load from a file
let config = from_file::<AppConfig, _>(Path::new("config/app.conf")).unwrap();
println!("App: {} v{}", config.name, config.version);
// Note: When using from_file, you must specify two generic type parameters:
// - T: The type to deserialize into (must implement FromConf trait)
// - P: The path type (can be inferred with _ placeholder)
// Alternatively, you can use the trait method directly: AppConfig::from_file(path)
// Using the FromConf trait method directly:
let config2 = AppConfig::from_file("config/app.conf").unwrap();
println!("App (loaded with trait method): {} v{}", config2.name, config2.version);use confetti_rs::{ConfMap, to_file};
use std::path::Path;
#[derive(ConfMap, Debug)]
struct LogConfig {
level: String,
path: String,
}
let log_config = LogConfig {
level: "info".to_string(),
path: "/var/log/app.log".to_string(),
};
// Save to a file
to_file(&log_config, Path::new("config/logging.conf")).unwrap();For more comprehensive examples, including nested structures, collections, and custom field mappings, see the examples directory in the repository.
Use the conf_map attribute to customize field names in the configuration:
#[derive(ConfMap, Debug)]
struct Config {
#[conf_map(name = "api-key")]
api_key: String,
}Fields with Option<T> type are treated as optional:
#[derive(ConfMap, Debug)]
struct Config {
required_field: String,
optional_field: Option<i32>,
}Out of the box, Confetti-rs supports these types:
Stringi32,f64boolOption<T>where T is a supported typeVec<T>where T is a supported type
For custom types, implement the ValueConverter trait:
use confetti_rs::{ValueConverter, MapperError};
use std::net::IpAddr;
use std::str::FromStr;
impl ValueConverter for IpAddr {
fn from_conf_value(value: &str) -> Result<Self, MapperError> {
IpAddr::from_str(value).map_err(|e|
MapperError::ConversionError(format!("Invalid IP address: {}", e))
)
}
fn to_conf_value(&self) -> Result<String, MapperError> {
Ok(self.to_string())
}
}For nested configurations, you'll need to implement FromConf and ToConf:
#[derive(ConfMap, Debug)]
struct DatabaseConfig {
host: String,
port: i32,
}
#[derive(Debug)]
struct AppConfig {
name: String,
database: DatabaseConfig,
}
impl FromConf for AppConfig {
fn from_directive(directive: &ConfDirective) -> Result<Self, MapperError> {
// Extract simple fields
let name = directive.children.iter()
.find(|d| d.name.value == "name")
.and_then(|d| d.arguments.get(0))
.map(|arg| arg.value.clone())
.ok_or_else(|| MapperError::MissingField("name".into()))?;
// Extract nested configuration
let db_directive = directive.children.iter()
.find(|d| d.name.value == "database")
.ok_or_else(|| MapperError::MissingField("database".into()))?;
let database = DatabaseConfig::from_directive(db_directive)?;
Ok(AppConfig { name, database })
}
}
impl ToConf for AppConfig {
// Implementation omitted for brevity
}Confetti-rs allows you to customize the parser behavior:
use confetti_rs::{MapperOptions, parse};
let options = MapperOptions {
use_kebab_case: true,
indent: " ".to_string(),
parser_options: confetti_rs::ConfOptions {
allow_c_style_comments: true,
allow_triple_quotes: true,
..Default::default()
},
};Confetti-rs provides detailed error information:
match config_result {
Ok(config) => {
// Use the config
},
Err(e) => match e {
MapperError::ParseError(msg) => println!("Parse error: {}", msg),
MapperError::MissingField(field) => println!("Missing required field: {}", field),
MapperError::ConversionError(msg) => println!("Type conversion error: {}", msg),
MapperError::IoError(io_error) => println!("I/O error: {}", io_error),
MapperError::SerializeError(msg) => println!("Serialization error: {}", msg),
},
}This project is licensed under the MIT License - see the LICENSE file for details.