Problem
I'm looking for design pattern suggestions, possibly including crate recommendations.
In most of my projects there's some configuration the app loads from a file at the start of the runtime. The problem happens when I need to transform the configuration data: the code becomes dirty, and the problem gets bigger as the number of configuration variables and nesting increases.
Right now I'm using serde deserialization with RON files in the pattern outlined bellow. I'm looking to change this approach.
Example
This contrived example is of a car trip app. In real projects the config file is longer and more complex.
Suppose I want to refactor the code bellow so the RON file accepts color as a hex String
rather than [f32; 3]
, but Car
object construction will still need color as [f32; 3]
.
src/main.rs
mod config;
fn main() {
let config = config::init("config.ron");
// use configs in various ways, like to construct structs
let car = Car::from_config(&config.car_config);
let fuel_cost_efficiency = car.fuel_cost_efficiency();
let trip_cost = fuel_cost_efficiency * config.road_length;
let fun_factor = (car.loudness() + car.red_amount()) / trip_cost;
}
src/config.rs
#[derive(serde::Deserialize)]
pub struct CarConfig {
pub fuel_type: String,
pub color: [f32; 3],
}
#[derive(serde::Deserialize)]
pub struct Config {
pub car_config: CarConfig,
pub road_length: f32,
}
pub fn init(filename: &str) -> Config {
let config_str = std::fs::read_to_string(filename)
.expect(&format!("No configuration file `{filename}` found"));
ron::from_str(&config_str).expect("Error while reading the configuration file")
}
config.ron
(
car_config: (
fuel_type: "gasoline",
color: [1.0, 0.0, 0.0],
),
road_length: 100.0,
)
PS: Solutions I've considered
I can think of 3 ways to refactor the code but none of them are ideal:
- Splitting the
CarConfig
struct into:CarConfigRaw
that gets deserialized and stores color asString
, andCarConfig
that gets passed toCar::from_config
and stores color as[f32; 3]
. The problem appears when there are many nested config structs and each one has many fields. Converting between*ConfigRaw
and*Config
requires affecting all the parent structs and copying the unaffected fields one-by-one. This creates ugly code and requires repetition. - Converting from a hex
String
to[f32; 3]
insideCar::from_config
. This is not ideal because now everything is coupled: config validation and transformation happens deep inside unrelated code. - Constructing
Car
usingCar::new(car_config.fuel_type, transform(car_config.color))
instead ofCar::from_config(&car_config)
. This requires repetition when there are many fields.