Skip to content

Commit

Permalink
Merge pull request #14 from iqlusioninc/application-lifecycle
Browse files Browse the repository at this point in the history
Full application lifecycle implementation
  • Loading branch information
tony-iqlusion authored Sep 1, 2018
2 parents b8da9cd + 94dc7d1 commit 4d26ec0
Show file tree
Hide file tree
Showing 15 changed files with 192 additions and 114 deletions.
7 changes: 5 additions & 2 deletions examples/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ extern crate abscissa;
#[macro_use]
extern crate serde_derive;

use abscissa::GlobalConfig;
use abscissa::{CanonicalPathBuf, GlobalConfig};

/// Configuration data to parse from TOML
// TODO: `derive(GlobalConfig)`
Expand All @@ -31,8 +31,11 @@ pub struct MyConfig {
impl_global_config!(MyConfig, MY_GLOBAL_CONFIG);

fn main() {
// Find the canonical filesystem path of the configuration
let config_path = CanonicalPathBuf::new("example_config.toml").unwrap();

// Load `MyConfig` from the given TOML file or exit
MyConfig::set_from_toml_file_or_exit("example_config.toml");
MyConfig::set_from_toml_file_or_exit(&config_path);

// Prints the `foo` record of `example_config.toml`
print_foo_from_global_config();
Expand Down
2 changes: 1 addition & 1 deletion src/application/exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub fn shutdown<A: Application>(app: &A, components: Components) -> ! {

/// Print a fatal error message and exit
pub fn fatal_error<A: Application>(app: &A, err: &Error) -> ! {
status_err!("fatal error for {}: {}", app.name(), err);
status_err!("{} fatal error: {}", app.name(), err);
process::exit(1)
}

Expand Down
67 changes: 37 additions & 30 deletions src/application/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ pub mod exit;

pub use self::components::{Component, Components};
use command::Command;
use config::{ConfigReader, GlobalConfig, MergeOptions};
use config::{ConfigReader, GlobalConfig, LoadConfig};
use error::FrameworkError;
use logging::{LoggingComponent, LoggingConfig};
use shell::{self, ColorConfig, ShellComponent};
use util::{self, CanonicalPathBuf, Version};

/// Core Abscissa trait used for managing the application lifecycle.
Expand All @@ -23,10 +25,10 @@ use util::{self, CanonicalPathBuf, Version};
#[allow(unused_variables)]
pub trait Application: Send + Sized + Sync {
/// Application (sub)command which serves as the main entry point
type Cmd: Command;
type Cmd: Command + LoadConfig<Self::Config>;

/// Configuration type used by this application
type Config: GlobalConfig + MergeOptions<Self::Cmd>;
type Config: GlobalConfig;

/// Get a read lock on the application's global configuration
fn config(&self) -> ConfigReader<Self::Config> {
Expand Down Expand Up @@ -60,18 +62,20 @@ pub trait Application: Send + Sized + Sync {
util::current_exe().unwrap()
}

/// Color configuration for this application
fn color_config(&self, command: &Self::Cmd) -> ColorConfig {
ColorConfig::default()
}

/// Load this application's configuration and initialize its components
fn init(&self, command: &Self::Cmd) -> Result<Components, FrameworkError> {
// We do this first to ensure that the configuration is loaded
// before the rest of the framework is initialized
let config = self.load_config_file()?;

// Set the global configuration to what we loaded from the config file
// overridden with flags from the command line
Self::Config::set_global(config.merge(command));
// Load the global configuration via the command's `LoadConfig` trait.
// We do this first to ensure that the configuration is loaded before
// the rest of the framework is initialized.
command.load_global_config()?;

// Create the application's components
let mut components = self.components()?;
let mut components = self.components(command)?;

// Initialize the components
components.init(self)?;
Expand All @@ -80,33 +84,33 @@ pub trait Application: Send + Sized + Sync {
Ok(components)
}

/// Load the application's global configuration from a file
fn load_config_file(&self) -> Result<Self::Config, FrameworkError> {
Self::Config::load_toml_file(self.path(ApplicationPath::ConfigFile)?)
/// Get this application's components
fn components(&self, command: &Self::Cmd) -> Result<Components, FrameworkError> {
Ok(Components::new(vec![
Box::new(ShellComponent::new(self.color_config(command))),
Box::new(LoggingComponent::new(self.logging_config(command))),
]))
}

/// Get this application's components
fn components(&self) -> Result<Components, FrameworkError> {
Ok(Components::default())
/// Get the logging configuration for this application
fn logging_config(&self, command: &Self::Cmd) -> LoggingConfig {
LoggingConfig::default()
}

/// Get a path associated with this application
fn path(&self, path_type: ApplicationPath) -> Result<CanonicalPathBuf, FrameworkError> {
Ok(match path_type {
//ApplicationPath::AppRoot => self.bin().parent()?,
ApplicationPath::AppRoot => self.bin().parent()?,
ApplicationPath::Bin => self.bin(),
other => panic!("KABOOM! {:?}", other)
//ApplicationPath::ConfigFile => Self::Config::default_location().ok_or_else(|| {
// self.fatal_error(err!(
// FrameworkErrorKind::Config,
// "no default configuration path configured for this config type"
// ))
//}),
//ApplicationPath::Secrets => self.bin().parent()?.join("secrets")?,
ApplicationPath::Secrets => self.bin().parent()?.join("secrets")?,
})
}

/// Register a component with this application. By default do nothing.
/// Display framework information as it relates to this application
fn print_framework_info(&self) {
shell::extras::print_framework_info();
}
/// Register a componen\t with this application. By default do nothing.
fn register(&self, component: &Component) -> Result<(), FrameworkError> {
Ok(())
}
Expand Down Expand Up @@ -136,9 +140,6 @@ pub enum ApplicationPath {
/// Path to the application's compiled binary
Bin,

/// Path to the application's configuration
ConfigFile,

/// Path to the application's secrets directory
Secrets,
}
Expand All @@ -153,6 +154,12 @@ pub fn boot<A: Application>(app: A) -> ! {
// Initialize the application
let components = app.init(&command).unwrap_or_else(|e| app.fatal_error(e));

// Show framework debug information if we're in debug mode
app.print_framework_info();

// TODO: call the parsed command here
command.run(&app);

// Exit gracefully
app.shutdown(components)
}
33 changes: 27 additions & 6 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Application (sub)command(s), i.e. app entry points
use std::process::exit;
use std::{fmt::Debug, process::exit};

#[cfg(feature = "application")]
use application::Application;
use options::Options;
use util::CanonicalPathBuf;

/// Something which can be called
pub trait Callable {
Expand All @@ -14,7 +17,7 @@ pub trait Callable {
/// trait, but also has a `call()` method which can be used to invoke the given
/// (sub)command.
// TODO: custom derive support, i.e. `derive(Command)`
pub trait Command: Callable + Options {
pub trait Command: Callable + Debug + Options {
/// Name of this program as a string
fn name() -> &'static str;

Expand Down Expand Up @@ -64,6 +67,16 @@ pub trait Command: Callable + Options {
Self::from_args(args)
}

/// Main entry point for commands run as part of applications. This is
/// called for the `Application::Cmd` associated type after parsing the
/// `Command`, loading the application's configuration, and initializing
/// all of its subcomponents (e.g. shell renderer, logging)
#[cfg(feature = "application")]
#[allow(unused_variables)]
fn run<A: Application>(&self, app: &A) {
self.call();
}

//
// TODO: the methods below should probably be factored into `Option`
//
Expand All @@ -83,7 +96,7 @@ pub trait Command: Callable + Options {

/// Print usage information for the given command to STDOUT and then exit with
/// a usage status code (i.e. `2`).
fn print_usage(args: &[String]) {
fn print_usage(args: &[String]) -> ! {
Self::print_package_info();
Self::print_package_authors();

Expand All @@ -103,20 +116,20 @@ pub trait Command: Callable + Options {
println!("SUBCOMMANDS:");
println!("{}", Self::command_list().unwrap());

exit(2);
exit(2)
}

/// Print subcommand usage
// TODO: less hax way of doing this
fn print_subcommand_usage(subcommand: &str) {
fn print_subcommand_usage(subcommand: &str) -> ! {
Self::print_subcommand_description(subcommand);
println!();
println!("USAGE:");
println!(" {} {} [OPTIONS]", Self::name(), subcommand);
println!();
Self::print_subcommand_flags(subcommand);

exit(2);
exit(2)
}

/// Print a description for a subcommand
Expand Down Expand Up @@ -145,3 +158,11 @@ pub trait Command: Callable + Options {
println!("{}", usage);
}
}

/// Commands which know the path to their configuration
pub trait CommandConfig {
/// Path from which to load this command's configuration
fn config_path(&self) -> Option<CanonicalPathBuf> {
None
}
}
39 changes: 25 additions & 14 deletions src/config/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@
//! a thread-safe manner (while also potentially supporting dynamic updates).
use serde::de::DeserializeOwned;
use std::{fs::File, io::Read, path::Path};
use std::{fs::File, io::Read};

pub use super::reader::ConfigReader;
use error::FrameworkError;
use util::{toml, CanonicalPathBuf};
use error::{FrameworkError, FrameworkErrorKind::ConfigError};
use util::{toml, CanonicalPath};

/// Common functions for loading and reading application configuration from
/// TOML files (providing a global lock which allows many readers, and can be
/// automatically implemented using the `impl_global_config!` macro.
// TODO: `derive(GlobalConfig)` using a proc macro.
pub trait GlobalConfig: 'static + Clone + DeserializeOwned {
/// Default location from which to load the configuration
// TODO: const fn?
fn default_location() -> Option<CanonicalPathBuf> {
None
}

/// Get the global configuration, acquiring a lock around it. If the
/// configuration hasn't been loaded, calls `Self::not_loaded()`.
fn get_global() -> ConfigReader<Self>;
Expand All @@ -33,23 +27,40 @@ pub trait GlobalConfig: 'static + Clone + DeserializeOwned {

/// Load the global configuration from the TOML file at the given path.
/// If an error occurs reading or parsing the file, print it out and exit.
fn load_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, FrameworkError> {
let mut file = File::open(path)?;
fn load_toml_file<P>(path: &P) -> Result<Self, FrameworkError>
where
P: AsRef<CanonicalPath>,
{
let mut file = File::open(path.as_ref()).map_err(|e| {
err!(
ConfigError,
"couldn't open {}: {}",
path.as_ref().display(),
e
)
})?;

let mut toml_string = String::new();
file.read_to_string(&mut toml_string)?;
Self::load_toml(toml_string)
}

/// Parse the given TOML file and set the global configuration to the result
fn set_from_toml_file<P: AsRef<Path>>(path: P) -> Result<ConfigReader<Self>, FrameworkError> {
fn set_from_toml_file<P>(path: &P) -> Result<ConfigReader<Self>, FrameworkError>
where
P: AsRef<CanonicalPath>,
{
Self::set_global(Self::load_toml_file(path)?);
Ok(Self::get_global())
}

/// Load the given TOML configuration file, printing an error message and
/// exiting if it's invalid
fn set_from_toml_file_or_exit<P: AsRef<Path>>(path: P) -> ConfigReader<Self> {
Self::set_from_toml_file(path.as_ref()).unwrap_or_else(|e| {
fn set_from_toml_file_or_exit<P>(path: &P) -> ConfigReader<Self>
where
P: AsRef<CanonicalPath>,
{
Self::set_from_toml_file(path).unwrap_or_else(|e| {
status_err!("error loading {}: {}", &path.as_ref().display(), e);
::std::process::exit(1);
})
Expand Down
40 changes: 40 additions & 0 deletions src/config/load.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::path::PathBuf;

use config::GlobalConfig;
use error::FrameworkError;
use util::{CanonicalPath, CanonicalPathBuf};

/// Support for loading configuration from a file.
/// Does not modify the global configuration. Only handles parsing and
/// deserializing it from files.
pub trait LoadConfig<C: GlobalConfig> {
/// Path to the command's configuration file. Returns an error by default.
fn config_path(&self) -> Option<PathBuf> {
None
}

/// Load the configuration from `self.config_path()` if present
fn load_global_config(&self) -> Result<(), FrameworkError> {
// Only attempt to load configuration if `config_path` returned the
// path to a configuration file.
if let Some(path) = self.config_path() {
let canonical_path = CanonicalPathBuf::canonicalize(path)?;
let config = self.load_config_file(&canonical_path)?;
C::set_global(config);
}

Ok(())
}

/// Load the configuration for this command
fn load_config_file<P: AsRef<CanonicalPath>>(&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> {
Ok(config)
}
}
6 changes: 2 additions & 4 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
//! Support for managing global configuration, as well as loading it from TOML
mod global;
#[cfg(feature = "options")]
mod options;
mod load;
mod reader;

pub use self::global::GlobalConfig;
#[cfg(feature = "options")]
pub use self::options::MergeOptions;
pub use self::load::LoadConfig;
pub use self::reader::ConfigReader;
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ pub use application::{boot, Application, ApplicationPath, Component, Components}
#[cfg(feature = "options")]
pub use command::{Callable, Command};
#[cfg(feature = "config")]
pub use config::{ConfigReader, GlobalConfig};
pub use config::{ConfigReader, GlobalConfig, LoadConfig};
#[cfg(feature = "errors")]
pub use error::{Error, Fail, FrameworkError, FrameworkErrorKind};
#[cfg(feature = "logging")]
Expand All @@ -131,4 +131,4 @@ pub use secrets::Secret;
#[cfg(feature = "shell")]
pub use shell::{status, ColorConfig, Stream};
#[cfg(feature = "application")]
pub use util::Version;
pub use util::{CanonicalPath, CanonicalPathBuf, Version};
7 changes: 7 additions & 0 deletions src/logging/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ use {Component, FrameworkError, Version};
#[derive(Debug, Default)]
pub struct LoggingComponent(LoggingConfig);

impl LoggingComponent {
/// Create a new `LoggingComponent` with the given configuration
pub fn new(config: LoggingConfig) -> Self {
LoggingComponent(config)
}
}

impl Component for LoggingComponent {
/// Name of this component
fn name(&self) -> &'static str {
Expand Down
Loading

0 comments on commit 4d26ec0

Please sign in to comment.