Skip to content

Commit

Permalink
[WIP] Configuration loading improvements (fixes #79)
Browse files Browse the repository at this point in the history
- Have `command::EntryPoint` delegate its `Configurable` impl to the
  command it wraps.
- Simplify configuration loading logic in `Application`.
- Rename `config::MergeOptions` to `config::Override` and provide an
  example of how to use it in the boilerplate.
  • Loading branch information
tony-iqlusion committed Jul 9, 2019
1 parent 626874b commit 77dc56d
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 55 deletions.
20 changes: 17 additions & 3 deletions abscissa_generator/template/src/commands.rs.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod version;

use self::{start::StartCommand, version::VersionCommand};
use crate::config::{{~config_type~}};
use abscissa::{Command, Configurable, Help, Options, Runnable};
use abscissa::{config::Override, Command, Configurable, FrameworkError, Help, Options, Runnable};
use std::path::PathBuf;

/// {{title}} Subcommands
Expand All @@ -36,9 +36,23 @@ pub enum {{command_type}} {

/// This trait allows you to define how application configuration is loaded.
impl Configurable<{{~config_type~}}> for {{command_type}} {
// Have `config_path` return `Some(path)` in order to trigger the
// application configuration being loaded.
fn config_path(&self) -> Option<PathBuf> {
// Have `config_path` return `Some(path)` in order to trigger the
// application configuration being loaded.
None
}

// Apply changes to the config after it's been loaded, e.g. overriding
// values in a config file using command-line options.
//
// This can be safely deleted if you don't intend to use it.
fn process_config(
&self,
config: {{config_type}},
) -> Result<{{~config_type~}}, FrameworkError> {
match self {
{{command_type}}::Start(cmd) => cmd.override_config(config),
_ => Ok(config),
}
}
}
19 changes: 18 additions & 1 deletion abscissa_generator/template/src/commands/start.rs.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
#[allow(unused_imports)]
use crate::prelude::*;

use abscissa::{Command, Options, Runnable};
use crate::config::{{~config_type~}};
use abscissa::{config, Command, FrameworkError, Options, Runnable};

/// `start` subcommand
///
Expand All @@ -31,3 +32,19 @@ impl Runnable for StartCommand {
}
}
}

impl config::Override<{{~config_type~}}> for StartCommand {
// Process the given command line options, overriding settings from
// a configuration file using explicit flags taken from command-line
// arguments.
fn override_config(
&self,
mut config: {{config_type}},
) -> Result<{{config_type}}, FrameworkError> {
if !self.recipient.is_empty() {
config.example_section.example_value = self.recipient.join(" ");
}

Ok(config)
}
}
39 changes: 16 additions & 23 deletions src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{
terminal::{component::TerminalComponent, ColorChoice},
Version,
};
use std::{env, process, vec};
use std::{env, path::Path, process, vec};

/// Application types implementing this trait own global application state,
/// including configuration and arbitrary other values stored within
Expand Down Expand Up @@ -100,7 +100,7 @@ pub trait Application: Default + Sized {
// Load configuration
let config = command
.config_path()
.map(|_| self.load_config(command))
.map(|path| command.process_config(self.load_config(&path)?))
.transpose()?;

// Fire callback regardless of whether any config was loaded to
Expand All @@ -123,28 +123,21 @@ pub trait Application: Default + Sized {
Ok(vec![Box::new(terminal), Box::new(logging)])
}

/// Load configuration from command's `config_path()`.
/// Load configuration from the given path.
///
/// Returns an error if the configuration could not be loaded or if the
/// command's `config_path()` is none.
fn load_config(&mut self, command: &Self::Cmd) -> Result<Self::Cfg, FrameworkError> {
// Only attempt to load configuration if `config_path` returned the
// path to a configuration file.
if let Some(ref path) = command.config_path() {
let canonical_path = AbsPathBuf::canonicalize(path)
.map_err(|e| err!(ConfigError, "invalid path '{}': {}", path.display(), e))?;

command.load_config_file(&canonical_path).map_err(|e| {
err!(
ConfigError,
"error loading config from '{}': {}",
canonical_path.display(),
e
)
})
} else {
fail!(PathError, "no config path for command: {:?}", command);
}
/// Returns an error if the configuration could not be loaded.
fn load_config(&mut self, path: &Path) -> Result<Self::Cfg, FrameworkError> {
let canonical_path = AbsPathBuf::canonicalize(path)
.map_err(|e| err!(ConfigError, "invalid path '{}': {}", path.display(), e))?;

Self::Cfg::load_toml_file(&canonical_path).map_err(|e| {
err!(
ConfigError,
"error loading config from '{}': {}",
canonical_path.display(),
e
)
})
}

/// Name of this application as a string.
Expand Down
32 changes: 25 additions & 7 deletions src/command/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
//! Toplevel entrypoint command.
use super::{Command, Usage};
use crate::{Config, Configurable, Options, Runnable};
use crate::{Config, Configurable, FrameworkError, Options, Runnable};
use std::path::PathBuf;

/// Toplevel entrypoint command.
///
/// Handles obtaining toplevel help as well as verbosity settings.
#[derive(Debug, Options)]
pub struct EntryPoint<Cmd: Runnable + Command> {
pub struct EntryPoint<Cmd>
where
Cmd: Command + Runnable,
{
/// Path to the configuration file
#[options(help = "path to configuration file")]
pub config: Option<PathBuf>,
Expand All @@ -31,7 +34,7 @@ pub struct EntryPoint<Cmd: Runnable + Command> {

impl<Cmd> EntryPoint<Cmd>
where
Cmd: Runnable + Command,
Cmd: Command + Runnable,
{
/// Borrow the underlying command type or print usage info and exit
fn command(&self) -> &Cmd {
Expand All @@ -43,7 +46,7 @@ where

impl<Cmd> Runnable for EntryPoint<Cmd>
where
Cmd: Runnable + Command,
Cmd: Command + Runnable,
{
fn run(&self) {
self.command().run()
Expand All @@ -52,7 +55,7 @@ where

impl<Cmd> Command for EntryPoint<Cmd>
where
Cmd: Runnable + Command,
Cmd: Command + Runnable,
{
/// Name of this program as a string
fn name() -> &'static str {
Expand Down Expand Up @@ -82,11 +85,26 @@ where

impl<Cfg, Cmd> Configurable<Cfg> for EntryPoint<Cmd>
where
Cmd: Runnable + Command,
Cmd: Command + Configurable<Cfg> + Runnable,
Cfg: Config,
{
/// Path to the command's configuration file
fn config_path(&self) -> Option<PathBuf> {
self.config.clone()
match &self.config {
// Use explicit `-c`/`--config` argument if passed
Some(cfg) => Some(cfg.clone()),

// Otherwise defer to the toplevel command's config path logic
None => self.command.as_ref().and_then(|cmd| cmd.config_path()),
}
}

/// Process the configuration after it has been loaded, potentially
/// modifying it or returning an error if options are incompatible
fn process_config(&self, config: Cfg) -> Result<Cfg, FrameworkError> {
match &self.command {
Some(cmd) => cmd.process_config(config),
None => Ok(config),
}
}
}
4 changes: 2 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! Support for managing global configuration, as well as loading it from TOML
mod configurable;
mod merge;
mod overrides;
mod reader;

pub use self::{configurable::Configurable, merge::MergeOptions, reader::Reader};
pub use self::{configurable::Configurable, overrides::Override, reader::Reader};
use crate::{
error::{FrameworkError, FrameworkErrorKind::ConfigError},
path::AbsPath,
Expand Down
12 changes: 3 additions & 9 deletions src/config/configurable.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
//! Configuration loader
use super::Config;
use crate::{error::FrameworkError, path::AbsPath};
use crate::error::FrameworkError;
use std::path::PathBuf;

/// Command type with which a configuration file is associated
pub trait Configurable<C: Config> {
pub trait Configurable<Cfg: Config> {
/// Path to the command's configuration file. Returns an error by default.
fn config_path(&self) -> Option<PathBuf> {
None
}

/// Load the configuration for this command
fn load_config_file<P: AsRef<AbsPath>>(&self, path: &P) -> Result<C, FrameworkError> {
self.preprocess_config(C::load_toml_file(path)?)
}

/// Process the configuration after it has been loaded, potentially
/// modifying it or returning an error if options are incompatible
#[allow(unused_mut)]
fn preprocess_config(&self, mut config: C) -> Result<C, FrameworkError> {
fn process_config(&self, config: Cfg) -> Result<Cfg, FrameworkError> {
Ok(config)
}
}
16 changes: 6 additions & 10 deletions src/config/merge.rs → src/config/overrides.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
//! Support for sourcing/overriding configuration values from arguments
//! given on the command-line.
//! Override values in the configuration file with command-line options
use crate::Options;
use crate::{Command, Config, FrameworkError};

/// Merge the given options into this configuration. This allows setting of
/// global configuration values using command-line options, and also unifies
/// the global config as the one way to get application settings.
#[allow(unused_variables)]
pub trait MergeOptions<O: Options>: Sized {
/// Use options from the given `Command` to override settings in the config.
pub trait Override<Cfg: Config>: Command {
/// Process the given command line options, overriding settings from
/// a configuration file using explicit flags taken from command-line
/// arguments.
Expand All @@ -16,7 +12,7 @@ pub trait MergeOptions<O: Options>: Sized {
/// settings when dealing with both a config file and options passed
/// on the command line, and a unified way of accessing this information
/// from components or in the application: from the global config.
fn merge(self, options: &O) -> Self {
self
fn override_config(&self, config: Cfg) -> Result<Cfg, FrameworkError> {
Ok(config)
}
}

0 comments on commit 77dc56d

Please sign in to comment.