diff --git a/Cargo.toml b/Cargo.toml index 4ba24fb..7ddf4d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aerosol" -version = "0.1.1" +version = "0.1.2" authors = ["Diggory Blake "] edition = "2018" description = "Dependency injection with compile-time guarantees" diff --git a/src/context.rs b/src/context.rs index 17e95dc..08080e6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,4 +1,5 @@ +#[doc(hidden)] #[macro_export(local_inner_macros)] macro_rules! private_define_context { { @@ -144,6 +145,98 @@ macro_rules! private_define_context { }; } +/// Define a new context. Typically used at the top level of an +/// application to contain the full set of requried dependencies. +/// +/// Contexts follow a struct-like syntax, although the names of +/// fields are for the most part unimportant. +/// +/// Contexts automatically implement all applicable interfaces. +/// An interface is applicable if all of the dependencies +/// required by that interface are present in the context. +/// +/// Dependencies are identified by *type*, not by the field name. +/// Contexts may not contain two fields of the same type. Instead +/// use new-type wrappers to distinguish similar dependencies. +/// +/// Types used in a context must implement `Clone + Debug`, and +/// `Clone` should be a cheap operation. For this reason it is usual +/// to wrap dependencies in an `Rc` or `Arc`. +/// +/// A constructor function will be automatically implemented +/// for contexts, with one parameter for each dependency, to be +/// provided in the same order as when the context is defined. +/// +/// ## Example +/// +/// ``` +/// use std::sync::Arc; +/// +/// #[derive(Debug)] +/// struct Foo; +/// #[derive(Debug)] +/// struct Bar; +/// +/// aerosol::define_context!( +/// TestContext { +/// foo: Arc, +/// bar: Arc, +/// } +/// ); +/// +/// fn main() { +/// TestContext::new( +/// Arc::new(Foo), +/// Arc::new(Bar), +/// ); +/// } +/// ``` +/// +/// It is also possible to define a factory type to enable +/// dependencies to be automatically created. +/// +/// When a factory is specified for a dependency, it will be +/// omitted from the parameter list required by the context's +/// constructor. Instead, the constructor will call the `build` +/// method on the specified factory. +/// +/// To conditionally use a factory, or use different factories +/// for the same dependency, define separate contexts, or +/// call the factory manually and pass the result to the +/// context's constructor in the normal way. +/// +/// ## Example +/// +/// ``` +/// use std::sync::Arc; +/// use failure; +/// +/// #[derive(Debug)] +/// struct Foo; +/// #[derive(Debug)] +/// struct Bar; +/// +/// struct FooFactory; +/// impl aerosol::Factory for FooFactory { +/// type Object = Arc; +/// fn build() -> Result, failure::Error> { Ok(Arc::new(Foo)) } +/// } +/// +/// aerosol::define_context!( +/// TestContext { +/// foo: Arc [FooFactory], +/// bar: Arc, +/// } +/// ); +/// +/// fn main() { +/// TestContext::new( +/// Arc::new(Bar), +/// ); +/// } +/// ``` +/// +/// #[macro_export(local_inner_macros)] macro_rules! define_context { ($($input:tt)*) => ( diff --git a/src/interface.rs b/src/interface.rs index a56f8a8..7601ac7 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -1,4 +1,4 @@ - +#[doc(hidden)] #[macro_export(local_inner_macros)] macro_rules! generate_trait_def { { @@ -20,7 +20,7 @@ macro_rules! generate_trait_def { }; } - +#[doc(hidden)] #[macro_export(local_inner_macros)] macro_rules! generate_trait_impl { { @@ -44,6 +44,7 @@ macro_rules! generate_trait_impl { }; } +#[doc(hidden)] #[macro_export(local_inner_macros)] macro_rules! private_define_interface { { @@ -129,6 +130,57 @@ macro_rules! private_define_interface { }; } +/// Define a new interface. Used at any layer of your application +/// to declare what dependencies are required by that part of the +/// program. +/// +/// Interfaces follow a trait-like syntax, except that they may +/// only contain "getter" methods of a particular form. The names +/// of these methods are for the most part unimportant, but the +/// return types are used to identify dependencies required for +/// a context to implement this interface. +/// +/// ## Example +/// +/// ``` +/// use std::sync::Arc; +/// +/// #[derive(Debug)] +/// struct Foo; +/// +/// aerosol::define_interface!( +/// TestInterface { +/// fn foo(&self) -> Arc; +/// } +/// ); +/// ``` +/// +/// Interfaces may also specify super-traits, which can themselves +/// be interfaces. Interfaces do not need to explicitly list +/// dependencies if they are transitively required by one of their +/// super-traits, but repeating a dependency will still only +/// require it to be provided once. +/// +/// ## Example +/// +/// ``` +/// #![recursion_limit="128"] +/// use std::sync::Arc; +/// +/// #[derive(Debug)] +/// struct Foo; +/// +/// aerosol::define_interface!( +/// FooInterface { +/// fn foo(&self) -> Arc; +/// } +/// ); +/// +/// aerosol::define_interface!( +/// TestInterface: FooInterface + Clone {} +/// ); +/// ``` + #[macro_export(local_inner_macros)] macro_rules! define_interface { ($($input:tt)*) => ( diff --git a/src/join.rs b/src/join.rs index 95f2662..fd45363 100644 --- a/src/join.rs +++ b/src/join.rs @@ -1,4 +1,4 @@ - +#[doc(hidden)] #[macro_export(local_inner_macros)] macro_rules! join { { diff --git a/src/lib.rs b/src/lib.rs index 6a835c1..855b200 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,101 @@ +//! # aerosol +//! Simple dependency injection for Rust +//! +//! The two main exports of this crate are the `define_context` +//! and `define_interface` macros. +//! +//! Contexts are containers for multiple dependencies, allowing +//! them to be passed around as one with relative ease. Interfaces +//! are specialized traits which place constraints on contexts, +//! indicating exactly what dependencies a context must provide. +//! +//! Contexts are typically created at the top level of an application, +//! as they specify exactly what concrete versions of all dependencies +//! are going to be used. A single context is created with a precise +//! set of depenencies, and is then threaded through the rest of the +//! application as a generic parameter. +//! +//! Interfaces are used at every level of an application, as they +//! allow each piece of code to independently specify what dependencies +//! are required. Interfaces can "inherit" the dependencies of other +//! interfaces, with the idea being that this inheritance will form +//! a tree, such that there will be some "root interface" which contains +//! the union of all dependencies required by the whole application. +//! +//! This pattern allows dependencies to be added or removed from any +//! part of the application without having to modify the code at every +//! level, to thread or un-thread the new or old dependencies through. +//! +//! ## Example +//! +//! ``` +//! #![recursion_limit="128"] +//! use std::sync::Arc; +//! use std::fmt::Debug; +//! use failure; +//! +//! // We will depend on some kind of logger +//! trait Logger: Debug { +//! fn log(&self, msg: &str); +//! } +//! +//! // We have a specific implementation of a stdout logger +//! #[derive(Debug)] +//! struct StdoutLogger; +//! +//! impl Logger for StdoutLogger { +//! fn log(&self, msg: &str) { +//! println!("{}", msg); +//! } +//! } +//! +//! struct StdoutLoggerFactory; +//! impl aerosol::Factory for StdoutLoggerFactory { +//! type Object = Arc; +//! fn build() -> Result, failure::Error> { +//! Ok(Arc::new(StdoutLogger)) +//! } +//! } +//! +//! // Part of our application does some work +//! aerosol::define_interface!( +//! WorkerInterface { +//! fn logger(&self) -> Arc; +//! } +//! ); +//! +//! fn do_work(iface: I) { +//! iface.logger().log("Doing some work!"); +//! } +//! +//! // Our application does multiple pieces of work +//! aerosol::define_interface!( +//! AppInterface: WorkerInterface + Clone {} +//! ); +//! +//! fn run_app(iface: I, num_work_items: usize) { +//! for _ in 0..num_work_items { +//! do_work(iface.clone()); +//! } +//! } +//! +//! // At the very top level, we specify the implementations +//! // of our dependencies. +//! aerosol::define_context!( +//! AppContext { +//! logger: Arc [StdoutLoggerFactory], +//! } +//! ); +//! +//! fn main() { +//! let context = AppContext::new().unwrap(); +//! +//! run_app(context, 4); +//! } +//! ``` +//! +//! See the individual macro documentation for more details. + pub extern crate tt_call; pub extern crate failure; @@ -7,15 +105,25 @@ mod interface; mod context; +/// The building block for this crate. Automatically implemented +/// for contexts providing a dependency of type `T`. +/// +/// Super-trait of all interfaces requiring a dependency of type +/// `T`. pub trait Provide { fn provide(&self) -> T; } +/// Implement this trait to provide a convenient syntax for +/// constructing implementations of dependencies. pub trait Factory { type Object; fn build() -> Result; } +/// Allows cloning a context whilst replacing one dependency +/// with a different implementation. Must be explicitly listed +/// as a super-trait of an interface to use. pub trait ProvideWith: Provide + Sized { fn provide_with Result>(&self, f: F) -> Result; } diff --git a/src/main.rs b/src/main.rs index 833a7cb..e2cbdc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ aerosol::define_interface!( } ); +#[allow(dead_code)] struct FooFactory; #[derive(Clone, Debug)] struct Foo; diff --git a/src/parse.rs b/src/parse.rs index 40f7778..7f52ecc 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,3 +1,4 @@ +#[doc(hidden)] #[macro_export(local_inner_macros)] macro_rules! parse_bound { { @@ -51,6 +52,7 @@ macro_rules! parse_bound { }; } +#[doc(hidden)] #[macro_export(local_inner_macros)] macro_rules! parse_trait_def { {