Improving the loading of TOML to form a struct?

I'm just starting learning Rust, and decided to knock up a fairly simple UDP listener. My first step is to load and parse a TOML config file (as this requires using external dependencies, involves IO and structs - all things handy to learn as I go along). I've got a working solution, but I'm eager to know if I've perhaps taken the long way round (it certainly seems so to me) and if I can improve the solution. I've added my current code below.

config/mod.rs:

pub mod loader;
pub mod file_loader;

#[derive(Debug)]
pub struct Config {
    server: ServerConfig,
}

#[derive(Debug)]
pub struct ServerConfig {
	port: i16,
	max_packet_size: i64,
}

config/loader.rs:

use config::Config;
use std::{io, convert};

pub trait ConfigLoader {
	fn load(&self) -> Result<Config, ConfigLoadError>;
}

#[derive(Debug)]
pub enum ConfigLoadError {
	EmptyError,
	LoadIoError(io::Error),
}

impl convert::From<io::Error> for ConfigLoadError {
	fn from(err: io::Error) -> ConfigLoadError {
		ConfigLoadError::LoadIoError(err)
	}
}

config/file_loader.rs:

(or, where I think everything falls apart... everything up to now seems pretty simple and clean)

use std::fs::File;
use config::{Config,SyslogServerConfig};
use config::loader::{ConfigLoader,ConfigLoadError};
use std::io::Read;

extern crate toml;

#[derive(Debug)]
pub struct FileLoader {
    pub file_path: &'static str,
}

impl ConfigLoader for FileLoader {
	fn load(&self) -> Result<Config, ConfigLoadError> {
		let mut config_file = try!(File::open(self.file_path));
		let mut config_content = String::new();
		try!(config_file.read_to_string(&mut config_content));

		let value = toml::Parser::new(&config_content).parse().unwrap();

		match value.get("server") {
			Some(result) => {
				let mut conf = Config{
					syslog_server: ServerConfig{
						port: 8080,
						max_packet_size: 1024
					}
				};

				match result.as_table() {
					Some(server_table) => {
						match server_table.get("port") {
							Some(port) => conf.syslog_server.port = port.as_integer().unwrap() as i16,
							None => conf.syslog_server.port = 8080,
						};

						match server_table.get("max_packet_size") {
							Some(max_packet_size) => conf.syslog_server.max_packet_size = max_packet_size.as_integer().unwrap(),
							None => conf.syslog_server.max_packet_size = 1024,
						}
					},
					None => Err(ConfigLoadError::EmptyError),
				};

				Ok(conf)
			},
			None => Err(ConfigLoadError::EmptyError)
		}
	}
}

config.toml:

(The config file that is to be read and parsed)

[server]
port = 9000
max_packet_size = 1024

Testing this, everything works as expected, producing the below output. It seems like a fairly messy way around it though - especially the structure initialisation.

Ok(Config { server: ServerConfig { port: 9000, max_packet_size: 1024 } })

Any insight or pointers would be greatly appreciated!

2 Likes

I'd probably recommend attaching derive(RustcDecodable) to your ServerConfig structure, making the fields Option<T> (because the configuration may not be necessary). This allows you to leverage the toml::Decoder structure (or the toml::decode_str function if you don't want to worry about errors).

#[derive(RustcDecodable)]
struct Configuration { server: ServerConfig }

let config_content = ...;
let mut parser = toml::Parser::new(&config_content);
let toml = match parser.parse() {
    Some(toml) => toml,
    None => { /* return an error based on parser.errors */ }
};

// Note to make this work you'll need:
//     impl From<toml::DecodeError> for ConfigLoadError
let mut decoder = toml::Decoder::new(toml);
let config = try!(Configuration::decode(&mut decoder));

Ok(config.server)
3 Likes

Looks much neater, I'll give it a shot cheers. This should cut a lot of code, especially as the config file may grow in real projects with any more entries.

Just gave this a try, all looks good apart from the following errors. If I don't have use rustc_serialize::serialize::Decodable;, I get the following error:

src/file_loader.rs:34:23: 34:37 error: no associated item named `decode` found for type `config::Config` in the current scope
src/file_loader.rs:34 				let config = try!(Config::decode(&mut decoder));
                             				                  ^~~~~~~~~~~~~~
<std macros>:1:1: 6:48 note: in expansion of try!
src/file_loader.rs:34:18: 34:52 note: expansion site
src/file_loader.rs:34:23: 34:37 help: items from traits can only be used if the trait is in scope; the following trait is implemented but not in scope, perhaps add a `use` for it:
src/file_loader.rs:34:23: 34:37 help: candidate #1: use `rustc_serialize::serialize::Decodable`

As soon as I add the use clause, I get anew error:

src/file_loader.rs:5:5: 5:42 error: trait `Decodable` is private
src/file_loader.rs:5 use rustc_serialize::serialize::Decodable;
                                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Not quite sure on this one, I'm going to assume my use is wrong, but I have no idea what the correct import should be.

Ah the path to the trait is just rustc_serialize::Decodable, and that should do the trick!

1 Like

Almost there, getting different errors now:

src/config/file_loader.rs:5:5: 5:20 error: unresolved import `rustc_serialize::Decodable`. Did you mean `config::rustc_serialize`?
src/config/file_loader.rs:5 use rustc_serialize::Decodable;

My code is as follows:

src/main.rs

mod config;

use config::loader::ConfigLoader;
use config::file_loader::FileLoader;

fn main() {
	let config_file_loader = FileLoader{file_path: "/Users/euan/Desktop/syslog_server/config/server.toml"};
	let config = config_file_loader.load();
	println!("{:?}", config);
}

src/config/mod.rs

extern crate rustc_serialize;
extern crate toml;

pub mod loader;
pub mod file_loader;

#[derive(Debug)]
#[derive(RustcDecodable)]
pub struct Config {
    syslog_server: ServerConfig,
}

#[derive(Debug)]
#[derive(RustcDecodable)]
pub struct ServerConfig {
	port: i16,
	max_packet_size: i64,
}

src/config/loader.rs

use config::Config;
use std::{io, convert};

pub trait ConfigLoader {
	fn load(&self) -> Result<Config, ConfigLoadError>;
}

#[derive(Debug)]
pub enum ConfigLoadError {
	EmptyError,
	LoadIoError(io::Error),
	ParseError(toml::ParserError),
	DecodeError(toml::DecodeError),
}

impl convert::From<io::Error> for ConfigLoadError {
	fn from(err: io::Error) -> ConfigLoadError {
		ConfigLoadError::LoadIoError(err)
	}
}

impl From<toml::DecodeError> for ConfigLoadError {
	fn from(err: toml::DecodeError) -> ConfigLoadError {
		ConfigLoadError::DecodeError(err)
	}
}

src/config/file_loader.rs

use std::fs::File;
use config::{Config,ServerConfig};
use config::loader::{ConfigLoader,ConfigLoadError};
use std::io::Read;
use rustc_serialize::Decodable;

#[derive(Debug)]
pub struct FileLoader {
    pub file_path: &'static str,
}

impl FileLoader {
	fn loadToml(&self) -> Result<toml::Value, ConfigLoadError> {
		let mut config_file = try!(File::open(self.file_path));
		let mut config_content = String::new();
		try!(config_file.read_to_string(&mut config_content));

		let mut parser = toml::Parser::new(&config_content);
		match parser.parse() {
		    Some(toml) => Ok(toml::Value::Table(toml)),
		    None => Err(ConfigLoadError::ParseError(parser.errors.pop().unwrap())),
		}
	}
}

impl ConfigLoader for FileLoader {
	fn load(&self) -> Result<Config, ConfigLoadError> {
		match self.loadToml() {
			Ok(toml) => {
				let mut decoder = toml::Decoder::new(toml);
				let config = try!(Config::decode(&mut decoder));

				Ok(config)
			},
			Err(err) => Err(err)
		}
	}
}

I definitely feel that I'm getting closer. I do have a question regarding the namespacing though.

Try putting your extern crate statements in your crate root (in this case, probably src/main.rs). It's not strictly necessary, but is convention and makes importing it from other modules (like src/config/file_loader.rs) much simpler. In fact, the compiler's suggestion, config::rustc_serialize, would probably also fix your error.

1 Like

Yep, that worked. Just moved the extern entries to src/main.rs and use toml in the module files needing it and everything is working as expected. Thanks!