Introduction

This book covers the design patterns for context-generic programming (CGP), a new programming paradigm for Rust that allows strongly-typed components to be implemented and composed in a modular, generic, and type-safe way.

What is Context-Generic Programming

A high level overview of CGP is available on the project website. This section contains a summarized version of the overview.

At its core, CGP makes use of Rust's trait system to build generic component interfaces that decouple code that consumes an interface from code that implements an interface. Through this decoupling, code can be written to be generic over any context, and then be wired to be used on a concrete context by writing few lines of code. CGP makes use of Rust's strong type system to help ensure that any such wiring is type-safe, catching any unsatisfied dependencies as compile-time errors.

CGP shares some similarities with other modular programming patterns, such as OCaml modules, Scala implicits, mixins, and dependency injection. Compared to these other patterns, CGP has a unique advantage that it enables high modularity while also being type-safe and concise. With Rust as its host language, CGP also allows high-performance and low-level code to be written in a modular way, without requiring complex runtime support.

CGP is designed to solve a wide range of common problems in programming, including error handling, logging, encoding, and modular dispatch. In particular, it allows writing static-typed Rust programs to be almost as expressive as writing dynamic-typed programs, but with the additional type safety guarantees. If you ever feel that Rust's type system is restricting your ability to reuse code, or be forced to duplicate code through copy-pasting or macros, then CGP may be of help to solve your problem.

That said, programming in CGP is as expressive, but not as easy, as dynamic-typed programming. There may be a steep learning curve in learning how to program in a generic way, and this book aims to help make that learning process more approachable. Thoughout this book, we will slowly understand how CGP works, and learn about useful design patterns that can be used in any programming situation.

Work In Progress

This book is currently a work in progress. A majority of the chapter is yet to be written. Please come back later to check out a completed version of this book.

Scope of This Book

This book is written in the style of a reference material for readers with all levels of expertise. As a result, it may not be as easy to understand for beginners who need a little more introduction to more basic programming techniques in Rust.

A separate book will be written in the future, to provide beginner-friendly tutorials for learning context-generic programming.

For brievity, this book also does not cover motivation or concrete examples of why you should learn and use context-generic programming. We will cover that in blog posts, and a separate book that covers real world use of CGP.

Chapter Outlines

The first section of this book, Terminology, will introduce common terms to be used to understand CGP. We will learn about what is a context, and what are consumer and provider traits.

In the next section, Core Concepts, we will cover the core concepts and the Rust-specific design patterns that we use to enable context-generic programming.

Following that, Design Patterns will introduce general design patterns that are built on top of the foundation of context-generic programming.

The section Domain-Specific Patterns will cover use-case-specific design patterns, such as error handling and logging.

Finally, the secion Related Concepts will compare context-generic programming with related concepts, such as the similarity and differences of context-generic programming as compared to object-oriented programming.

Contribution

This book is open sourced under the MIT license on GitHub.

Anyone is welcome to contribute by submitting pull requests for grammatical correction, content improvement, or adding new design patterns.

A GitHub Discussions forum is available for readers to ask questions or have discussions for topics covered in this book.

Context

In CGP, we use the term context to refer to a type that provide certain functionalities, or dependencies. The most common kind of functionality a context may provide is a method.

Following is a simple hello world example of a context providing a method:

#![allow(unused)]
fn main() {
struct MyContext;

impl MyContext {
    fn hello(&self) {
        println!("Hello World!");
    }
}
}

The example above is mostly self explanatory. We first define a struct called MyContext, followed by an impl block for MyContext. Inside the impl block, a hello method is provided, which prints out "Hello World!" to the terminal when called.

We can then use the hello method anywhere that we have a value of type MyContext, such as:

#![allow(unused)]
fn main() {
struct MyContext;

impl MyContext {
    fn hello(&self) {
        println!("Hello World!");
    }
}

let my_context = MyContext;

my_context.hello();
}

Contexts vs Classes

The above example may seem trivial for most programmers, especially for those who come from object-oriented programming (OOP) background. In fact, one way we can have a simplified view of a context is that it is similar to OOP concepts such as classes, objects, and interfaces.

Beyond the surface-level similarity, the concept of contexts in CGP is more general than classes and other similar concepts. As a result, it is useful to think of the term context as a new concept that we will learn in this book.

As we will learn in later chapters, aside from methods, a context may provide other kinds of functionalities, such as associated types and constants.

Contexts vs Types

Although a context is usually made of a type, in CGP we do not treat all types as contexts. Instead, we expect CGP contexts to offer some level of modularity, which can be achived by using the programming patterns introduced in this book.

Consumer

In CGP, a consumer is a piece of code that consumes certain functionalities from a context. There are several ways which a consumer may consume a functionality. At its most basic, if a consumer has access to the concrete type of a context, it can access any methods defined by an impl block of that context.

#![allow(unused)]
fn main() {
struct Person { name: String }

impl Person {
    fn name(&self) -> &str {
        &self.name
    }
}

fn greet(person: &Person) {
    println!("Hello, {}!", person.name());
}
}

in the above example, we have a greet function that prints a greeting to a person using the method Person::name. In other words, we say that the greet function is a consumer to the Person::name method.

Context-Generic Consumers

The greet function in our previous example can only work with the Person struct. However, if we inspect the implementation of greet, we can see that it is possible to generalize greet to work with any type that has a name.

To generalize greet, we first need to define a trait that acts as an interface for getting a name:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

fn greet<Context>(context: &Context)
where
    Context: HasName
{
    println!("Hello, {}", context.name());
}
}

In the example above, we define a HasName trait that provides a name method. We then redefine greet to work generically with any Context type, with the where clause requiring Context to implement HasName. Inside the function body, we call the name method, and print out the greeting of that name.

Notice that in this example, we are able to implement greet before we have any concrete implementation of HasName. Compared to before, greet is now decoupled from the Person type, thus making our code more modular.

In CGP, this new version of greet is considered a context-generic consumer, as it is able to generically consume the HasName::name method from any Context type that implements HasName.

The concept of context-generic consumer is not unique to CGP. In fact, it is already commonly used in most of the Rust code that uses traits. However, we make an effort to study this concept, so that we can further generalize the concept in the later chapters of this book.

Provider

In CGP, a provider is a piece of code that implements certain functionality for a context. At its most basic, a provider is consist of an impl block for a trait.

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

struct Person { name: String }

impl HasName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}
}

In the above example, we implement the HasName for the Person struct. The block impl HasName for Person is a provider of the HasName trait for the Person context.

Similar to the concept of a consumer, the use of provider is common in any Rust code that implements a trait. However, compared to cosumers, there are limitations on how providers can be defined in Rust.

For this example, the impl block is a context-specific provider for the Person context. Furthermore, due to the restrictions of Rust's trait system, there can be at most one provider of HasName for the Person context. Another common restriction is that the provider has to be defined in the same crate as either the trait or the context.

The asymetry between what can be done with a provider, as compared to a consumer, is often a source of complexity in many Rust programs. As we will learn in later chapters, one of the goals of CGP is to break this asymetry, and make it easy to implement context-generic providers.

Providers as Consumers

Although we have providers and consumers as distinct concepts, it is common to have code that serve as both providers and consumers.

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

struct Person { name: String }

impl HasName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}

trait CanGreet {
    fn greet(&self);
}

impl CanGreet for Person {
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}
}

The example above shows a new CanGreet trait, which provides a greet method. We then implement CanGreet for Person, with the greet implementation using self.name() to print out the name to be greeted.

Here, the block impl CanGreet for Person is a provider of CanGreet for the Person context. At the same time, it is also the consumer of HasName for the Person context. In terms of genericity, the example code is context-specific to the Person context for both the consumer and provider side.

As we will see in later chapters, a powerful idea introduced by CGP is that a piece of code can have multiple spectrums of genericity on the consumer and provider sides.

Blanket Trait Implementations

In the previous chapter, we have an implementation of CanGreet for Person that makes use of HasName to retrieve the person's name to be printed. However, the implementation is context-specific to the Person context, and cannot be reused for other contexts.

Ideally, we want to be able to define context-generic implementations of Greet that works with any context type that also implements HasName. For this, the blanket trait implementations pattern is one basic way which we can use for defining context-generic implementations:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}
}

The above example shows a blanket trait implementation of CanGreet for any Context type that implements HasName. With that, contexts like Person do not need to explicitly implement CanGreet, if they already implement HasName:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

struct Person { name: String }

impl HasName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}

let person = Person { name: "Alice".to_owned() };
person.greet();
}

As shown above, we are able to call person.greet() without having a context-specific implementation of CanGreet for Person.

Extension Traits

The use of blanket trait implementation is commonly found in many Rust libraries today. For example, Itertools provides a blanket implementation for any context that implements Iterator. Another example is StreamExt, which is implemented for any context that implements Stream.

Traits such as Itertools and StreamExt are sometimes known as extension traits. This is because the purpose of the trait is to extend the behavior of existing types, without having to own the type or base traits. While the use of extension traits is a common use case for blanket implementations, there are other ways we can make use of blanket implementations.

Overriding Blanket Implementations

Traits containing blanket implementation are usually not meant to be implemented manually by individual contexts. They are usually meant to serve as convenient methods that extends the functionality of another trait. However, Rust's trait system does not completely prevent us from overriding the blanket implementation.

Supposed that we have a VipPerson context that we want to implement a different way of greeting the VIP person. We could override the implementation as follows:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

struct VipPerson { name: String, /* other fields */ }

impl CanGreet for VipPerson {
    fn greet(&self) {
        println!("A warm welcome to you, {}!", self.name);
    }
}
}

The example above shows two providers of CanGreet. The first provider is a context-generic provider that we covered previously, but the second provider is a context-specific provider for the VipPerson context.

Conflicting Implementations

In the previous example, we are able to define a custom provider for VipPerson, but with an important caveat: that VipPerson does not implement HasName. If we try to define a custom provider for contexts that already implement HasName, such as for Person, the compilation would fail:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

struct Person { name: String }

impl HasName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}

impl CanGreet for Person {
    fn greet(&self) {
        println!("Hi, {}!", self.name());
    }
}
}

If we try to compile the example code above, we would get an error with the message:

conflicting implementations of trait `CanGreet` for type `Person`

The reason for the conflict is because Rust trait system requires all types to have unambigious implementation of any given trait. To see why such requirement is necessary, consider the following example:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

fn call_greet_generically<Context>(context: &Context)
where
    Context: HasName,
{
    context.greet()
}
}

The example above shows a generic function call_greet_generically, which work with any Context that implements HasName. Even though it does not require Context to implement CanGreet, it nevertheless can call context.greet(). This is because with the guarantee from Rust's trait system, the compiler can always safely use the blanket implementation of CanGreet during compilation.

If Rust were to allow ambiguous override of blanket implementations, such as what we tried with Person, it would have resulted in inconsistencies in the compiled code, depending on whether it is known that the generic type is instantiated to Person.

Note that in general, it is not always possible to know locally about the concrete type that is instantiated in a generic code. This is because a generic function like call_greet_generically can once again be called by other generic code. This is why even though there are unstable Rust features such as trait specialization, such feature has to be carefully considered to ensure that no inconsistency can arise.

Limitations of Blanket Implementations

Due to potential conflicting implementations, the use of blanket implementations offer limited customizability, in case if a context wants to have a different implementation. Although a context many define its own context-specific provider to override the blanket provider, it would face other limitations such as not being able to implement other traits that may cause a conflict.

In practice, we consider that blanket implementations allow for a singular context-generic provider to be defined. In future chapters, we will look at how to relax the singular constraint, to make it possible to allow multiple context-generic or context-specific providers to co-exist.

Impl-side Dependencies

When writing generic code, we often need to specify the trait bounds that we would like to use with a generic type. However, when the trait bounds involve traits that make use of blanket implementations, there are different ways that we can specify the trait bounds.

Supposed that we want to define a generic function that formats a list of items into a comma-separated string. Our generic function could make use the Itertools::join to format an iterator. Our first attempt would be to define our generic function as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

fn format_iter<I>(mut iter: I) -> String
where
    I: Iterator,
    I::Item: Display,
{
    iter.join(", ")
}

assert_eq!(format_iter(vec![1, 2, 3].into_iter()), "1, 2, 3");
}

The generic function format_iter takes a generic type I that implements Iterator. Additionally, we require I::Item to implement Display. With both constraints in place, we are able to call Itertools::join inside the generic function to join the items using ", " as separator.

In the above example, we are able to use the method from Itertools on I, even though we do not specify the constraint I: Itertools in our where clause. This is made possible because the trait Itertools has a blanket implementation on all types that implement Iterator, including the case when we do not know the concrete type behind I. Additionally, the method Itertools::join requires I::Item to implement Display, so we also include the constraint in our where clause.

When using traits that have blanket implementation, we can also go the other way and require I to implement Itertools instead of Iterator:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

fn format_iter<I>(mut items: I) -> String
where
    I: Itertools,
    I::Item: Display,
{
    items.join(", ")
}

assert_eq!(format_iter(vec![1, 2, 3].into_iter()), "1, 2, 3");
}

By doing so, we make it explicit of the intention that we only care that I implements Itertools, and hide the fact that we need I to also implement Iterator in order to implement Itertools.

Constraint Leaks

At this point, we have defined our generic function format_iter with two constraints in the where clause. When calling format_iter from another generic function, the constraint would also be propagated to the caller.

As a demonstration, supposed that we want to define another generic function that uses format_iter to format any type that implements IntoIterator, we would need to also include the constraints needed by format_iter as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

fn format_iter<I>(mut items: I) -> String
where
    I: Itertools,
    I::Item: Display,
{
    items.join(", ")
}

fn format_items<C>(items: C) -> String
where
    C: IntoIterator,
    C::IntoIter: Itertools,
    <C::IntoIter as Iterator>::Item: Display,
{
    format_iter(items.into_iter())
}

assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3");
}

When defining the generic function format_items above, we only really care that the generic type C implements IntoIterator, and then pass C::IntoIter to format_iter. However, because of the constraints specified by format_iter, Rust also forces us to specify the same constraints in format_items, even if we don't need the constraints directly.

As we can see, the constraints specified in the where clause of format_iter is a form of leaky abstraction, as it also forces generic consumers like format_items to also know about the internal details of how format_iter uses the iterator.

The leaking of where constraints also makes it challenging to write highly generic functions at a larger scale. The number of constraints could quickly become unmanageable, if a high level generic function calls many low-level generic functions that each has different constraints.

Furthermore, the repeatedly specified constraints become tightly coupled with the concrete implementation of the low-level functions. For example, if format_iter changed from using Itertools::join to other ways of formatting the iterator, the constraints would become outdated and need to be changed in format_items.

Hiding Constraints with Traits and Blanket Implementations

Using the techniques we learned from blanket implementations, there is a way to hide the where constraints by redefining our generic functions as traits with blanket implementations.

We would first rewrite format_iter into a trait CanFormatIter as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

assert_eq!(vec![1, 2, 3].into_iter().format_iter(), "1, 2, 3");
}

The trait CanFormatIter is defined with a single method, format_iter, which consumes self and return a String. The trait comes with a blanket implementation for any type I, with the constraints that I: Itertools and I::Item: Display. Following that, we have the same implementation as before, which calls Itertools::join to format the iterator as a comma-separated string. By having a blanket implementation, we signal that CanFormatIter is intended to be derived automatically, and that no explicit implementation is required.

It is worth noting that the constraints I: Itertools and I::Item: Display are only present at the impl block, but not at the trait definition of CanFormatIter. By doing so, we have effectively "hidden" the constraints inside the impl block, and prevent it from leaking to its consumers.

We can now refactor format_items to use CanFormatIter as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

fn format_items<C>(items: C) -> String
where
    C: IntoIterator,
    C::IntoIter: CanFormatIter,
{
    items.into_iter().format_iter()
}

assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3");
}

In the new version of format_items, our where constraints are now simplified to only require C::IntoIter to implement CanFormatIter. With that, we are able to make it explicit that format_items needs CanFormatIter to be implemented, but it doesn't matter how it is implemented.

The reason this technique works is similar to how we used Itertools in our previous examples. At the call site of the code that calls format_items, Rust would see that the generic function requires C::IntoIter to implement CanFormatIter. But at the same time, Rust also sees that CanFormatIter has a blanket implementation. So if the constraints specified at the blanket implementation are satisfied, Rust would automatically provide an implementation of CanFormatIter to format_items, without the caller needing to know how that is done.

Nested Constraints Hiding

Once we have seen in action how we can hide constraints behind the blanket impl blocks of traits, there is no stopping for us to define more traits with blanket implementations to hide even more constraints.

For instance, we could rewrite format_items into a CanFormatItems trait as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

pub trait CanFormatItems {
    fn format_items(&self) -> String;
}

impl<Context> CanFormatItems for Context
where
    for<'a> &'a Context: IntoIterator,
    for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter,
{
    fn format_items(&self) -> String
    {
        self.into_iter().format_iter()
    }
}

assert_eq!(vec![1, 2, 3].format_items(), "1, 2, 3");
}

We first define a CanFormatItems trait, with a method format_items(&self). Here, we make an improvement over the original function to allow a reference &self, instead of an owned value self. This allows a container such as Vec to not be consumed when we try to format its items, which would be unnecessarily inefficient.

Inside the blanket impl block for CanFormatItems, we define it to work with any Context type, given that the generic Context type implements some constraints with higher ranked trait bounds (HRTB). While HRTB is an advanced subject on its own, the general idea is that we require that any reference &'a Context with any lifetime 'a implements IntoIterator. This is so that when IntoIterator::into_iter is called, the Self type being consumed is the reference type &'a Context, which is implicitly copyable, and thus allow the same context to be reused later on at other places.

Additionally, we require that <&'a Context as IntoIterator>::IntoIter implements CanFormatIter, so that we can call its method on the produced iterator. Thanks to the hiding of constraints by CanFormatIter, we can avoid specifying an overly verbose constraint that the iterator item also needs to implement Display.

Individually, the constraints hidden by CanFormatIter and CanFormatItems may not look significant. But when combining together, we can see how isolating the constraints help us better organize our code and make them cleaner. In particular, we can now write generic functions that consume CanFormatIter without having to understand all the indirect constraints underneath.

To demonstrate, supposed that we want to compare two list of items and see whether they have the same string representation. We can now define a generic stringly_equals function as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

pub trait CanFormatItems {
    fn format_items(&self) -> String;
}

impl<Context> CanFormatItems for Context
where
    for<'a> &'a Context: IntoIterator,
    for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter,
{
    fn format_items(&self) -> String
    {
        self.into_iter().format_iter()
    }
}

fn stringly_equals<Context>(left: &Context, right: &Context) -> bool
where
    Context: CanFormatItems,
{
    left.format_items() == right.format_items()
}

assert_eq!(stringly_equals(&vec![1, 2, 3], &vec![1, 2, 4]), false);
}

Our generic function stringly_equals can now be defined cleanly to work over any Context type that implements CanFormatItems. In this case, the function does not even need to be aware that Context needs to produce an iterator, with its items implementing Display.

Furthermore, instead of defining a generic function, we could instead use the same programming technique and define a CanStringlyCompareItems trait that does the same thing:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

pub trait CanFormatItems {
    fn format_items(&self) -> String;
}

impl<Context> CanFormatItems for Context
where
    for<'a> &'a Context: IntoIterator,
    for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter,
{
    fn format_items(&self) -> String
    {
        self.into_iter().format_iter()
    }
}

pub trait CanStringlyCompareItems {
    fn stringly_equals(&self, other: &Self) -> bool;
}

impl<Context> CanStringlyCompareItems for Context
where
    Context: CanFormatItems,
{
    fn stringly_equals(&self, other: &Self) -> bool {
        self.format_items() == other.format_items()
    }
}

assert_eq!(vec![1, 2, 3].stringly_equals(&vec![1, 2, 4]), false);
}

For each new trait we layer on top, we can build higher level interfaces that hide away lower level implementation details. When CanStringlyCompareItems is used, the consumer is shielded away from knowing anything about the concrete context, other than that two values are be compared by first being formatted into strings.

The example here may seem a bit stupid, but there are some practical use cases of implementing comparing two values as strings. For instance, a serialization library may want to use it inside tests to check whether two different values are serialized into the same string. For such use case, we may want to define another trait to help make such assertion during tests:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

pub trait CanFormatItems {
    fn format_items(&self) -> String;
}

impl<Context> CanFormatItems for Context
where
    for<'a> &'a Context: IntoIterator,
    for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter,
{
    fn format_items(&self) -> String
    {
        self.into_iter().format_iter()
    }
}

pub trait CanStringlyCompareItems {
    fn stringly_equals(&self, other: &Self) -> bool;
}

impl<Context> CanStringlyCompareItems for Context
where
    Context: CanFormatItems,
{
    fn stringly_equals(&self, other: &Self) -> bool {
        self.format_items() == other.format_items()
    }
}

pub trait CanAssertEqualImpliesStringlyEqual {
    fn assert_equal_implies_stringly_equal(&self, other: &Self);
}

impl<Context> CanAssertEqualImpliesStringlyEqual for Context
where
    Context: Eq + CanStringlyCompareItems,
{
    fn assert_equal_implies_stringly_equal(&self, other: &Self) {
        assert_eq!(self == other, self.stringly_equals(other))
    }
}

vec![1, 2, 3].assert_equal_implies_stringly_equal(&vec![1, 2, 3]);
vec![1, 2, 3].assert_equal_implies_stringly_equal(&vec![1, 2, 4]);
}

The trait CanAssertEqualImpliesStringlyEqual provides a method that takes two contexts of the same type, and assert that if both context values are equal, then their string representation are also equal. Inside the blanket impl block, we require that Context implements CanStringlyCompareItems, as well as Eq.

Thanks to the hiding of constraints, the trait CanAssertEqualImpliesStringlyEqual can cleanly separate its direct dependencies, Eq, from the rest of the indirect dependencies.

Dependency Injection

The programming technique that we have introduced in this chapter is sometimes known as dependency injection in some other languages and programming paradigms. The general idea is that the impl blocks are able to specify the dependencies they need in the form of where constraints, and the Rust trait system automatically helps us to resolve the dependencies at compile time.

In context-generic programming, we think of constraints in the impl blocks not as constraints, but more generally dependencies that the concrete implementation needs. Each time a new trait is defined, it serves as an interface for consumers to include them as a dependency, but at the same time separates the declaration from the concrete implementations.

Provider Traits

In the previous chapters on blanket implementations and impl-side dependencies, we learned about the power of using blanket impl blocks to simplify and hide the dependencies required by each part of the implementation. However, one major limitation of blanket implementations is that there cannot be multiple potentially overlapping implementations, due to restrictions in Rust's trait system. In CGP, we can overcome this limitation by introducing the concept of provider traits.

The main idea behind provider traits is to define Rust traits that are dedicated for providers to define new implementations, and separate it from the consumer traits that are more suitable for consumers that use the traits. Consider a simple consumer trait CanFormatToString, which allows formatting a context into string:

#![allow(unused)]
fn main() {
pub trait CanFormatString {
    fn format_string(&self) -> String;
}
}

The trait we defined here is almost identical to the standard library's ToString trait. But we will duplicate the trait here to tweak how it is implemented. We first note that the original ToString trait has a blanket implementation for any type that implements Display:

#![allow(unused)]
fn main() {
use core::fmt::Display;

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

impl<Context> CanFormatString for Context
where
    Context: Display,
{
    fn format_string(&self) -> String {
        format!("{}", self)
    }
}
}

Although having this blanket implementation is convenient, it restricts us from being able to format the context in other ways, such as using Debug.

#![allow(unused)]
fn main() {
use core::fmt::{Display, Debug};

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

impl<Context> CanFormatString for Context
where
    Context: Display,
{
    fn format_string(&self) -> String {
        format!("{}", self)
    }
}

// Error: conflicting implementation
impl<Context> CanFormatString for Context
where
    Context: Debug,
{
    fn format_string(&self) -> String {
        format!("{:?}", self)
    }
}
}

To overcome this limitation, we can introduce a provider trait that we'd call StringFormatter, which we will then use for defining implementations:

#![allow(unused)]
fn main() {
pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}
}

Compared to CanFormatString, the trait StringFormatter replaces the implicit context type Self with an explicit context type Context, as defined in its type parameter. Following that, it replaces all occurrances of &self with context: &Context.

By avoiding the use of Self in provider traits, we can bypass the restrictions of Rust trait system, and have multiple implementations defined. Continuing the earlier example, we can define the Display and Debug implementations of CanFormatString as two separate providers of StringFormatter:

#![allow(unused)]
fn main() {
use core::fmt::{Display, Debug};

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

pub struct FormatStringWithDisplay;

pub struct FormatStringWithDebug;

impl<Context> StringFormatter<Context> for FormatStringWithDisplay
where
    Context: Display,
{
    fn format_string(context: &Context) -> String {
        format!("{}", context)
    }
}

impl<Context> StringFormatter<Context> for FormatStringWithDebug
where
    Context: Debug,
{
    fn format_string(context: &Context) -> String {
        format!("{:?}", context)
    }
}
}

With provider traits, we now have two named providers FormatStringWithDisplay and FormatStringWithDebug, which are defined as dummy structs. These structs are not meant to be used inside any code during run time. Rather, they are used as identifiers at the type level for us to refer to the providers during compile time.

Notice that inside the implementation of StringFormatter, the types FormatStringWithDisplay and FormatStringWithDebug are in the position that is typically used for Self, but we don't use Self anywhere in the implementation. Instead, the original Self type is now referred explicitly as the Context type, and we use &context instead of &self inside the implementation.

From the point of view of Rust's trait system, the rules for overlapping implementation only applies to the Self type. But because we have two distinct Self types here (FormatStringWithDisplay and FormatStringWithDebug), the two implementations are not considered overlapping, and we are able to define them without any compilation error.

Using Provider Traits Directly

Although provider traits allow us to define overlapping implementations, the main downside is that consumer code cannot make use of an implementation without explicitly choosing the implementation.

Consider the following Person context defined:

#![allow(unused)]
fn main() {
use core::fmt::{self, Display, Debug};

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

pub struct FormatStringWithDisplay;

pub struct FormatStringWithDebug;

impl<Context> StringFormatter<Context> for FormatStringWithDisplay
where
    Context: Display,
{
    fn format_string(context: &Context) -> String {
        format!("{}", context)
    }
}

impl<Context> StringFormatter<Context> for FormatStringWithDebug
where
    Context: Debug,
{
    fn format_string(context: &Context) -> String {
        format!("{:?}", context)
    }
}

#[derive(Debug)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

impl Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.first_name, self.last_name)
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };

assert_eq!(
    FormatStringWithDisplay::format_string(&person),
    "John Smith"
);

assert_eq!(
    FormatStringWithDebug::format_string(&person),
    "Person { first_name: \"John\", last_name: \"Smith\" }"
);
}

Our Person struct is defined with both Debug and Display implementations. When using format_string on a value person: Person, we cannot just call person.format_string(). Instead, we have to explicitly pick a provider Provider, and call it with Provider::format_string(&person). On the other hand, thanks to the explicit syntax, we can use both FormatStringWithDisplay and FormatStringWithDebug on Person without any issue.

Nevertheless, having to explicitly pick a provider can be problematic, especially if there are multiple providers to choose from. In the next chapter, we will look at how we can link a provider trait with a consumer trait, so that we can use back the simple person.format_string() syntax without needing to know which provider to choose from.

Beyond String Formatting

In this chapter, we make use of a very simplified example of formatting strings to demonstrate the use case of provider traits. Our example may seem a bit redundant, as it does not simplify the code much as compared to directly using format!() to format the string with either Debug or Display. However, the provider trait allows us to also define providers that format a context in other ways, such as by serializing it as JSON:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> Result<String, Error>;
}

use anyhow::Error;
use serde::Serialize;
use serde_json::to_string;

pub struct FormatAsJson;

impl<Context> StringFormatter<Context> for FormatAsJson
where
    Context: Serialize,
{
    fn format_string(context: &Context) -> Result<String, Error> {
        Ok(to_string(context)?)
    }
}
}

To allow for error handling, we update the method signature of format_string to return a Result, with anyhow::Error being used as a general error type. We will also be covering better ways to handle errors in a context-generic way in in later chapters.

If we recall from the previous chapter, the CanFormatIter trait in fact has the same method signature as StringFormatter. So we can refactor the code from the previous chapter, and turn it into a context-generic provider that works for any iterable context like Vec.

This use of provider traits can also be more useful in more complex use cases, such as implementing Serialize, or even the Display trait itself. If we were to implement these traits using CGP, we would also define provider traits such as follows:

#![allow(unused)]
fn main() {
extern crate serde;

use core::fmt;
use serde::Serializer;

pub trait ProvideSerialize<Context> {
    fn serialize<S: Serializer>(context: &Context, serializer: S) -> Result<S::Ok, S::Error>;
}

pub trait ProvideFormat<Context> {
    fn fmt(context: &Context, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>;
}
}

As we can see above, we can define provider traits for any existing traits by replacing the Self type with an explicit Context type. In this chapter, we would not be covering the details on how to use CGP and provider traits to simplify formatting and serialization implementations, as that is beyond the current scope. Suffice to say, as we go through later chapters, it will become clearer on how having provider traits can impact us on thinking about how to structure and implement modular code.

Linking Consumers with Providers

In the previous chapter, we learned about how provider traits allow multiple overlapping implementations to be defined. However, if everything is implemented only as provider traits, it would be much more tedious having to determine which provider to use, at every time when we need to use the trait. To overcome this, we would need have both provider traits and consumer traits, and have some ways to choose a provider when implementing a consumer trait.

Implementing Consumer Traits

The simplest way to link a consumer trait with a provider is by implementing the consumer trait to call a chosen provider. Consider the StringFormatter example of the previous chapter, we would implement CanFormatString for a Person context as follows:

#![allow(unused)]
fn main() {
use core::fmt::{self, Display};

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

#[derive(Debug)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

impl Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.first_name, self.last_name)
    }
}

impl CanFormatString for Person {
    fn format_string(&self) -> String {
        FormatStringWithDisplay::format_string(self)
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };

assert_eq!(person.format_string(), "John Smith");

pub struct FormatStringWithDisplay;

impl<Context> StringFormatter<Context> for FormatStringWithDisplay
where
    Context: Display,
{
    fn format_string(context: &Context) -> String {
        format!("{}", context)
    }
}
}

To recap the previous chapter, we have a consumer trait CanFormatString and a provider trait StringFormatter. There are two example providers that implemenent StringFormatter - FormatStringWithDisplay which formats strings using Display, and FormatStringWithDebug which formats strings using Debug. In addition to that, we implement CanFormatString for the Person context by forwarding the call to FormatStringWithDisplay.

By doing so, we effectively "bind" the StringFormatter provider for the Person context to FormatStringWithDisplay. With that, any time a consumer code calls person.format_string(), it would automatically format the context using Display.

Thanks to the decoupling of providers and consumers, a context like Person can freely choose between multiple providers, and link them with relative ease. Similarly, the provider trait allows multiple context-generic providers such as FormatStringWithDisplay and FormatStringWithDebug to co-exist.

Blanket Consumer Trait Implementation

In the previous section, we manually implemented CanFormatString for Person with an explicit call to FormatStringWithDisplay. Although the implementation is relatively short, it can become tedious if we make heavy use of provider traits, which would require us to repeat the same pattern for every trait.

To simplify this further, we can make use of blanket implementations to automatically delegate the implementation of all consumer traits to one chosen provider. We would define the blanket implementation for CanFormatString as follows:

#![allow(unused)]
fn main() {
pub trait HasComponents {
    type Components;
}

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

impl<Context> CanFormatString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_string(&self) -> String {
        Context::Components::format_string(self)
    }
}
}

First of all, we define a new HasComponents trait that contains an associated type Components. The Components type would be specified by a context to choose a provider that it would use to forward all implementations of consumer traits. Following that, we add a blanket implementation for CanFormatString, which would be implemented for any Context that implements HasComponents, provided that Context::Components implements StringFormatter<Context>.

To explain in simpler terms - if a context has a provider that implements a provider trait for that context, then the consumer trait for that context is also automatically implemented.

With the new blanket implementation in place, we can now implement HasComponents for the Person context, and it would now help us to implement CanFormatString for free:

#![allow(unused)]
fn main() {
use core::fmt::{self, Display};

#[derive(Debug)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

impl Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.first_name, self.last_name)
    }
}

impl HasComponents for Person {
    type Components = FormatStringWithDisplay;
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };

assert_eq!(person.format_string(), "John Smith");

pub trait HasComponents {
    type Components;
}

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

impl<Context> CanFormatString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_string(&self) -> String {
        Context::Components::format_string(self)
    }
}

pub struct FormatStringWithDisplay;

impl<Context> StringFormatter<Context> for FormatStringWithDisplay
where
    Context: Display,
{
    fn format_string(context: &Context) -> String {
        format!("{}", context)
    }
}
}

Compared to before, the implementation of HasComponents is much shorter than implementing CanFormatString directly, since we only need to specify the provider type without any function definition.

At the moment, because the Person context only implements one consumer trait, we can set FormatStringWithDisplay directly as Person::Components. However, if there are other consumer traits that we would like to use with Person, we would need to define Person::Components with a separate provider that implements multiple provider traits. This will be covered in the next chapter, which we would talk about how to link multiple providers of different provider traits together.

Component System

You may have noticed that the trait for specifying the provider for a context is called HasComponents instead of HasProviders. This is to generalize the idea of a pair of consumer trait and provider trait working together, forming a component.

In context-generic programming, we use the term component to refer to a consumer-provider trait pair. The consumer trait and the provider trait are linked together through blanket implementations and traits such as HasComponents. These constructs working together to form the basis for a component system for CGP.

Provider Delegation

In the previous chapter, we learned to make use of the HasComponents trait to define a blanket implementation for a consumer trait like CanFormatString, so that a context would automatically delegate the implementation to a provider trait like StringFormatter. However, because there can only be one Components type defined for HasComponents, this means that the given provider needs to implement all provider traits that we would like to use for the context.

In this chapter, we will learn to combine multiple providers that each implements a distinct provider trait, and turn them into a single provider that implements multiple provider traits.

Implementing Multiple Providers

Consider that instead of just formatting a context as string, we also want to parse the context from string. In CGP, we would define two separate traits to handle the functionalities separately:

#![allow(unused)]
fn main() {
extern crate anyhow;

use anyhow::Error;

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}
}

Similar to the previous chapter, we define CanFormatToString for formatting a context into string, and CanParseFromString for parsing a context from a string. Notice that CanParseFromString also has an additional Sized constraint, as by default the Self type in Rust traits do not implement Sized, to allow traits to be used in dyn trait objects. Compared to before, we also make the methods return a Result to handle errors during formatting and parsing. 1 2

Next, we also define the provider traits as follows:

#![allow(unused)]
fn main() {
extern crate anyhow;

use anyhow::Error;

pub trait HasComponents {
    type Components;
}

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

impl<Context> CanFormatToString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_to_string(&self) -> Result<String, Error> {
        Context::Components::format_to_string(self)
    }
}

impl<Context> CanParseFromString for Context
where
    Context: HasComponents,
    Context::Components: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Context::Components::parse_from_string(raw)
    }
}
}

Similar to the previous chapter, we make use of blanket implementations and HasComponents to link the consumer traits CanFormatToString and CanParseFromString with their respective provider traits, StringFormatter and StringParser.

We can then implement context-generic providers for the given provider traits, such as to format and parse the context as JSON if the context implements Serialize and Deserialize:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

use serde::{Serialize, Deserialize};

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}
}

The provider FormatAsJsonString implements StringFormatter for any Context type that implements Serialize, and uses serde_json::to_string to format the context as JSON. Similarly, the provider ParseFromJsonString implements StringParser for any Context that implements Deserialize, and parse the context from a JSON string.

1

A proper introduction to error handling using CGP will be covered in future chapters. But for now, we will use use anyhow::Error to handle errors in a more naive way.

2

There are more general terms the problem space of formatting and parsing a context, such as serialization or encoding. Instead of strings, a more general solution may use types such as bytes or buffers. Although it is possible to design a generalized solution for encoding in CGP, it would be too much to cover the topic in this chapter alone. As such, we use naive strings in this chapter so that we can focus on first understanding the basic concepts in CGP.

Linking Multiple Providers to a Concrete Context

With the providers implemented, we can now define a concrete context like Person, and link it with the given providers. However, since there are multiple providers, we need to first define an aggregated provider called PersonComponents, which would implement both StringFormatter and StringParser by delegating the call to the actual providers.

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;
use serde::{Serialize, Deserialize};

pub trait HasComponents {
    type Components;
}

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

impl<Context> CanFormatToString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_to_string(&self) -> Result<String, Error> {
        Context::Components::format_to_string(self)
    }
}

impl<Context> CanParseFromString for Context
where
    Context: HasComponents,
    Context::Components: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Context::Components::parse_from_string(raw)
    }
}

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

impl StringFormatter<Person> for PersonComponents {
    fn format_to_string(person: &Person) -> Result<String, Error> {
        FormatAsJsonString::format_to_string(person)
    }
}

impl StringParser<Person> for PersonComponents {
    fn parse_from_string(raw: &str) -> Result<Person, Error> {
        ParseFromJsonString::parse_from_string(raw)
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
let person_str = r#"{"first_name":"John","last_name":"Smith"}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

We first define Person struct with auto-derived implementations of Serialize and Deserialize. We also auto-derive Debug and Eq for use in tests later on.

We then define a dummy struct PersonComponents, which would be used to aggregate the providers for Person. Compared to the previous chapter, we implement HasComponents for Person with PersonComponents as the provider.

We then implement the provider traits StringFormatter and StringParser for PersonComponents, with the actual implementation forwarded to FormatAsJsonString and ParseFromJsonString.

Inside the test that follows, we verify that the wiring indeed automatically implements CanFormatToString and CanParseFromString for Person, with the JSON implementation used.

Blanket Provider Implementation

Although the previous example works, the boilerplate for forwarding multiple implementations by PersonComponents seems a bit tedious and redundant. The main differences between the two implementation boilerplate is that we want to choose FormatAsJsonString as the provider for StringFormatter, and ParseFromJsonString as the provider for StringParser.

Similar to how we can use HasComponents with blanket implementations to link a consumer with a provider, we can reduce the boilerplate required by using similar pattern to link a provider with another provider:

#![allow(unused)]
fn main() {
extern crate anyhow;

use anyhow::Error;

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

pub trait DelegateComponent<Name> {
    type Delegate;
}

pub struct StringFormatterComponent;

pub struct StringParserComponent;

impl<Context, Component> StringFormatter<Context> for Component
where
    Component: DelegateComponent<StringFormatterComponent>,
    Component::Delegate: StringFormatter<Context>,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Component::Delegate::format_to_string(context)
    }
}

impl<Context, Component> StringParser<Context> for Component
where
    Component: DelegateComponent<StringParserComponent>,
    Component::Delegate: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Component::Delegate::parse_from_string(raw)
    }
}
}

The DelegateComponent is similar to the HasComponents trait, but it is intended to be implemented by providers instead of concrete contexts. It also has an extra generic Name type that is used to differentiate which component the provider delegation is intended for.

To make use of the Name parameter, we first need to assign names to the CGP components that we have defined. We first define the dummy struct StringFormatterComponent to be used as the name for StringFormatter, and StringParserComponent to be used as the name for StringParser. In general, we can choose any type as the component name. However by convention, we choose to add a -Component postfix to the name of the provider trait to be used as the name of the component.

We then define a blanket implementation for StringFormatter, which is implemented for a provider type Component with the following conditions: if the provider implements DelegateComponent<StringFormatterComponent>, and if the associated type Delegate also implements StringFormatter<Context>, then Component also implements StringFormatter<Context> by delegating the implementation to Delegate.

Following the same pattern, we also define a blanket implementation for StringParser. The main difference here is that the name StringParserComponent is used as the type argument to DelegateComponent. In other words, different blanket provider implementations make use of different Name types for DelegateComponent, allowing different Delegate to be used depending on the Name.

Using DelegateComponent

It may take a while to fully understand how the blanket implementations with DelegateComponent and HasComponents work. But since the same pattern will be used everywhere, it would hopefully become clear as we see more examples. It would also help to see how the blanket implementation is used, by going back to the example of implementing the concrete context Person.

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;
use serde::{Serialize, Deserialize};

pub trait HasComponents {
    type Components;
}

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

impl<Context> CanFormatToString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_to_string(&self) -> Result<String, Error> {
        Context::Components::format_to_string(self)
    }
}

impl<Context> CanParseFromString for Context
where
    Context: HasComponents,
    Context::Components: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Context::Components::parse_from_string(raw)
    }
}

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

pub trait DelegateComponent<Name> {
    type Delegate;
}

pub struct StringFormatterComponent;

pub struct StringParserComponent;

impl<Context, Component> StringFormatter<Context> for Component
where
    Component: DelegateComponent<StringFormatterComponent>,
    Component::Delegate: StringFormatter<Context>,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Component::Delegate::format_to_string(context)
    }
}

impl<Context, Component> StringParser<Context> for Component
where
    Component: DelegateComponent<StringParserComponent>,
    Component::Delegate: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Component::Delegate::parse_from_string(raw)
    }
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

impl DelegateComponent<StringFormatterComponent> for PersonComponents {
    type Delegate = FormatAsJsonString;
}

impl DelegateComponent<StringParserComponent> for PersonComponents {
    type Delegate = ParseFromJsonString;
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
let person_str = r#"{"first_name":"John","last_name":"Smith"}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

Instead of implementing the provider traits, we now only need to implement DelegateComponent<StringFormatterComponent> and DelegateComponent<StringParserComponent> for PersonComponents. Rust's trait system would then automatically make use of the blanket implementations to implement CanFormatToString and CanParseFromString for Person.

As we will see in the next chapter, we can make use of macros to further simplify the component delegation, making it as simple as one line to implement such delegation.

Switching Provider Implementations

With the given examples, some readers may question why is there a need to define multiple providers for the JSON implementation, when we can just define one provider struct and implement both provider traits for it.

The use of two providers in this chapter is mainly used as demonstration on how to delegate and combine multiple providers. In practice, as the number of CGP components increase, we would also quickly run into the need have multiple provider implementations and choosing between different combination of providers.

Even with the simplified example here, we can demonstrate how a different provider for StringFormatter may be needed. Supposed that we want to format the context as prettified JSON string, we can define a separate provider FormatAsPrettifiedJsonString as follows:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;
use serde::{Serialize, Deserialize};

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub struct FormatAsPrettifiedJsonString;

impl<Context> StringFormatter<Context> for FormatAsPrettifiedJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string_pretty(context)?)
    }
}
}

In the StringFormatter implementation for FormatAsPrettifiedJsonString, we use serde_json::to_string_pretty instead of serde_json::to_string to pretty format the JSON string. With CGP, both FormatAsPrettifiedJsonString and FormatAsJsonString can co-exist peacefully, and we can easily choose which provider to use in each concrete context implementation.

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;
use serde::{Serialize, Deserialize};

pub trait HasComponents {
    type Components;
}

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

impl<Context> CanFormatToString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_to_string(&self) -> Result<String, Error> {
        Context::Components::format_to_string(self)
    }
}

impl<Context> CanParseFromString for Context
where
    Context: HasComponents,
    Context::Components: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Context::Components::parse_from_string(raw)
    }
}

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct FormatAsPrettifiedJsonString;

impl<Context> StringFormatter<Context> for FormatAsPrettifiedJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string_pretty(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

pub trait DelegateComponent<Name> {
    type Delegate;
}

pub struct StringFormatterComponent;

pub struct StringParserComponent;

impl<Context, Component> StringFormatter<Context> for Component
where
    Component: DelegateComponent<StringFormatterComponent>,
    Component::Delegate: StringFormatter<Context>,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Component::Delegate::format_to_string(context)
    }
}

impl<Context, Component> StringParser<Context> for Component
where
    Component: DelegateComponent<StringParserComponent>,
    Component::Delegate: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Component::Delegate::parse_from_string(raw)
    }
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

impl DelegateComponent<StringFormatterComponent> for PersonComponents {
    type Delegate = FormatAsPrettifiedJsonString;
}

impl DelegateComponent<StringParserComponent> for PersonComponents {
    type Delegate = ParseFromJsonString;
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
let person_str = r#"{
  "first_name": "John",
  "last_name": "Smith"
}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str,
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

Compared to before, the only line change is to set the Delegate of DelegateComponent<StringFormatterComponent> to FormatAsPrettifiedJsonString instead of FormatAsJsonString. With that, we can now easily choose between whether to pretty print a Person context as JSON.

Beyond having a prettified implementation, it is also easy to think of other kinds of generic implementations, such as using Debug or Display to format strings, or use different encodings such as XML to format the string. With CGP, we can define generalized component interfaces that are applicable to a wide range of implementations. We can then make use of DelegateComponent to easily choose which implementation we want to use for different concrete contexts.

Component Macros

At this point, we have covered all basic building blocks of defining CGP components. In summary, a CGP component is consist of the following building blocks:

  • A consumer trait.
  • A provider trait.
  • A component name type.
  • A blanket implementation of the consumer trait using HasComponents.
  • A blanket implementation of the provider trait using DelegateComponent.

Syntactically, all CGP components follow the same pattern. The pattern is roughly as follows:

// Consumer trait
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

// Provider trait
pub trait ActionPerformer<Context, GenericA, GenericB, ...>
where
    Context: ConstraintA + ConstraintB + ...,
{
    fn perform_action(
        context: &Context,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

// Component name
pub struct ActionPerformerComponent;

// Blanket implementation for consumer trait
impl<Context, GenericA, GenericB, ...>
    CanPerformAction<GenericA, GenericB, ...> for Context
where
    Context: HasComponents + ConstraintA + ConstraintB + ...,
    Context::Components: ActionPerformer<Context>,
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output {
        Context::Components::perform_action(self, arg_a, arg_b, ...)
    }
}

// Blanket implementation for provider trait
impl<Context, Component, GenericA, GenericB, ...>
    ActionPerformer<Context, GenericA, GenericB, ...>
    for Component
where
    Context: ConstraintA + ConstraintB + ...,
    Component: DelegateComponent<ActionPerformerComponent>,
    Component::Delegate: ActionPerformer<Context, GenericA, GenericB, ...>,
{
    fn perform_action(
        context: &Context,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output {
        Component::Delegate::perform_action(context, arg_a, arg_b, ...)
    }
}

cgp_component Macro

With the repetitive pattern, it makes sense that we should be able to just define the consumer trait, and make use of Rust macros to generate the remaining code. The author has published the cgp Rust crate that provides the cgp_component attribute macro that can be used for this purpose. Using the macro, the same code as above can be significantly simplified to the following:

use cgp::prelude::*;

#[cgp_component {
    name: ActionPerformerComponent,
    provider: ActionPerformer,
    context: Context,
}]
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

To use the macro, the bulk import statement use cgp::prelude::* has to be used to bring all CGP constructs into scope. This includes the HasComponents and DelegateComponent traits, which are also provided by the cgp crate.

We then use cgp_component as an attribute proc macro, with several key-value arguments given. The name field is used to define the component name type, which is called ActionPerformerComponent. The provider field ActionPerformer is used for the name for the provider trait. The context field Context is used for the generic type name of the context when used inside the provider trait.

The cgp_component macro allows the name and context field to be omited. When omitted, the context field will default to Context, and the name field will default to {provider}Component. So the same example above could be simplified to:

use cgp::prelude::*;

#[cgp_component {
    provider: ActionPerformer,
}]
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

delegate_components Macro

In addition to the cgp_component macro, cgp also provides the delegate_components! macro that can be used to automatically implement DelegateComponent for a provider type. The syntax is roughly as follows:

use cgp::prelude::*;

pub struct TargetProvider;

delegate_components! {
    TargetProvider {
        ComponentA: ProviderA,
        ComponentB: ProviderB,
        [
            ComponentC1,
            ComponentC2,
            ...
        ]: ProviderC,
    }
}

The above code would be desugared into the following:

impl DelegateComponent<ComponentA> for TargetProvider {
    type Delegate = ProviderA;
}

impl DelegateComponent<ComponentB> for TargetProvider {
    type Delegate = ProviderB;
}

impl DelegateComponent<ComponentC1> for TargetProvider {
    type Delegate = ProviderC;
}

impl DelegateComponent<ComponentC2> for TargetProvider {
    type Delegate = ProviderC;
}

The delegate_components! macro accepts an argument to an existing type, TargetProvider, which is expected to be defined outside of the macro. It is followed by an open brace, and contain entries that look like key-value pairs. For a key-value pair ComponentA: ProviderA, the type ComponentA is used as the component name, and ProviderA refers to the provider implementation. When multiple keys map to the same value, i.e. multiple components are delegated to the same provider implementation, the array syntax can be used to further simplify the mapping.

Example Use

To illustrate how cgp_component and delegate_components can be used, we revisit the code for CanFormatToString, CanParseFromString, and PersonContext from the previous chapter, and look at how the macros can simplify the same code.

Following is the full code after simplification using cgp:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;
extern crate cgp;

use cgp::prelude::*;
use anyhow::Error;
use serde::{Serialize, Deserialize};

// Component definitions

#[cgp_component {
    name: StringFormatterComponent,
    provider: StringFormatter,
    context: Context,
}]
pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

#[cgp_component {
    name: StringParserComponent,
    provider: StringParser,
    context: Context,
}]
pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

// Provider implementations

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

// Concrete context and wiring

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

delegate_components! {
    PersonComponents {
        StringFormatterComponent: FormatAsJsonString,
        StringParserComponent: ParseFromJsonString,
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
let person_str = r#"{"first_name":"John","last_name":"Smith"}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

As we can see, the new code is significantly simpler and more readable than before. Using cgp_component, we no longer need to explicitly define the provider traits StringFormatter and StringParser, and the blanket implementations can be omitted. We also make use of delegate_components! on PersonComponents to delegate StringFormatterComponent to FormatAsJsonString, and StringParserComponent to ParseFromJsonString.

CGP Macros as Language Extension

The use of cgp crate with its macros is essential in enabling the full power of context-generic programming in Rust. Without it, programming with CGP would become too verbose and full of boilerplate code.

On the other hand, the use of cgp macros makes CGP code look much more like programming in a domain-specific language (DSL) than in regular Rust. In fact, one could argue that CGP acts as a language extension to the base language Rust, and almost turn into its own programming language.

In a way, implementing CGP in Rust is slightly similar to implementing OOP in C. We could think of context-generic programming being as foundational as object-oriented programming, and may be integrated as a core language feature in future programming languages.

Perhaps one day, there might be an equivalent of C++ to replace CGP-on-Rust. Or perhaps more ideally, the core constructs of CGP would one day directly supported as a core language feature in Rust. But until that happens, the cgp crate serves as an experimental ground on how context-generic programming can be done in Rust, and how it can help build better Rust applications.

In the chapters that follow, we will make heavy use of cgp and its macros to dive further into the world of context-generic programming.

Debugging Techniques

By leveraging impl-side dependencies, CGP providers are able to include additional dependencies that are not specified in the provider trait. We have already seen this in action in the previous chapter, for example, where the provider FormatAsJsonString is able to require Context to implement Serialize, while that is not specified anywhere in the provider trait StringFormatter.

We have also went through how provider delegation can be done using DelegateComponent, which an aggregated provider like PersonComponents can use to delegate the implementation of StringFormatter to FormatAsJsonString. Within this delegation, we can also see that the requirement for Context to implement Serialize is not required in any part of the code.

In fact, because the provider constraints are not enforced in DelegateComponent, the delegation would always be successful, even if some provider constraints are not satisfied. In other words, the impl-side provider constraints are enforced lazily in CGP, and compile-time errors would only arise when we try to use a consumer trait against a concrete context.

Unsatisfied Dependency Errors

To demonstrate how such error would arise, we would reuse the same example Person context as the previous chapter. Consider if we made a mistake and forgot to implement Serialize for Person:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;
extern crate cgp;

use cgp::prelude::*;
use anyhow::Error;
use serde::{Serialize, Deserialize};

#[cgp_component {
   name: StringFormatterComponent,
   provider: StringFormatter,
   context: Context,
}]
pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

#[cgp_component {
   name: StringParserComponent,
   provider: StringParser,
   context: Context,
}]
pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

// Note: We forgot to derive Serialize here
#[derive(Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

delegate_components! {
    PersonComponents {
        StringFormatterComponent: FormatAsJsonString,
        StringParserComponent: ParseFromJsonString,
    }
}
}

We know that Person uses PersonComponents to implement CanFormatToString, and PersonComponents delegates the provider implementation to FormatAsJsonString. However, since FormatAsJsonString requires Person to implement Serialize, without it CanFormatToString cannot be implemented on PersonContext.

However, notice that the above code still compiles successfully. This is because we have not yet try to use CanFormatToString on person. We can try to add test code to call format_to_string, and check if it works:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;
extern crate cgp;

use cgp::prelude::*;
use anyhow::Error;
use serde::{Serialize, Deserialize};

#[cgp_component {
   name: StringFormatterComponent,
   provider: StringFormatter,
   context: Context,
}]
pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

#[cgp_component {
   name: StringParserComponent,
   provider: StringParser,
   context: Context,
}]
pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

// Note: We forgot to derive Serialize here
#[derive(Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

delegate_components! {
    PersonComponents {
        StringFormatterComponent: FormatAsJsonString,
        StringParserComponent: ParseFromJsonString,
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
println!("{}", person.format_to_string().unwrap());
}

The first time we try to call the method, our code would fail with a compile error that looks like follows:

error[E0599]: the method `format_to_string` exists for struct `Person`, but its trait bounds were not satisfied
  --> debugging-techniques.md:180:23
   |
54 | pub struct Person {
   | ----------------- method `format_to_string` not found for this struct because it doesn't satisfy `Person: CanFormatToString`
...
59 | pub struct PersonComponents;
   | --------------------------- doesn't satisfy `PersonComponents: StringFormatter<Person>`
...
73 | println!("{}", person.format_to_string().unwrap());
   |                -------^^^^^^^^^^^^^^^^--
   |                |      |
   |                |      this is an associated function, not a method
   |                help: use associated function syntax instead: `Person::format_to_string()`
   |
   = note: found the following associated functions; to be used as methods, functions must have a `self` parameter
note: the candidate is defined in the trait `StringFormatter`
  --> debugging-techniques.md:125:5
   |
18 |     fn format_to_string(&self) -> Result<String, Error>;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: trait bound `PersonComponents: StringFormatter<Person>` was not satisfied
  --> debugging-techniques.md:119:1
   |
12 | / #[cgp_component {
13 | |    name: StringFormatterComponent,
14 | |    provider: StringFormatter,
15 | |    context: Context,
   | |             ^^^^^^^
16 | | }]
   | |__^
note: the trait `StringFormatter` must be implemented
  --> debugging-techniques.md:124:1
   |
17 | / pub trait CanFormatToString {
18 | |     fn format_to_string(&self) -> Result<String, Error>;
19 | | }
   | |_^
   = help: items from traits can only be used if the trait is implemented and in scope
note: `CanFormatToString` defines an item `format_to_string`, perhaps you need to implement it
  --> debugging-techniques.md:124:1
   |
17 | pub trait CanFormatToString {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: this error originates in the attribute macro `cgp_component` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 1 previous error

Unfortunately, the error message returned from Rust is very confusing, and not helpful at all in guiding us to the root cause. For an inexperience developer, the main takeaway from the error message is just that CanFormatString is not implemented for Person, but the developer is left entirely on their own to find out how to fix it.

One main reason we get such obscured errors is because the implementation of CanFormatString is done through two indirect blanket implementations. As Rust was not originally designed for blanket implementations to be used this way, it does not follow through to explain why the blanket implementation is not implemented.

Technically, there is no reason why the Rust compiler cannot be improved to show more detailed errors to make using CGP easier. However, improving the compiler will take time, and we need to present strong argument on why such improvement is needed, e.g. through this book. Until then, we need temporary workarounds to make it easier to debug CGP errors in the meanwhile.

Check Traits

We have learned that CGP lazily resolve dependencies and implements consumer traits on a concrete context only when they are actively used. However, when defining a concrete context, we would like to be able to eagerly check that the consumer traits are implemented, so that no confusing error should arise when the context is being used.

By convention, the best approach for implementing such checks is to define a check trait, which asserts that a concrete context implements all consumer traits that we intended to implement. the check trait would be defined as follows:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;
extern crate cgp;

use cgp::prelude::*;
use anyhow::Error;
use serde::{Serialize, Deserialize};

#[cgp_component {
   name: StringFormatterComponent,
   provider: StringFormatter,
   context: Context,
}]
pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

#[cgp_component {
   name: StringParserComponent,
   provider: StringParser,
   context: Context,
}]
pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

// Note: We forgot to derive Serialize here
#[derive(Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

delegate_components! {
    PersonComponents {
        StringFormatterComponent: FormatAsJsonString,
        StringParserComponent: ParseFromJsonString,
    }
}

pub trait CanUsePerson:
    CanFormatToString
    + CanParseFromString
{}

impl CanUsePerson for Person {}
}

By convention, a check trait has the name starts with CanUse, followed by the name of the concrete context. We list all the consumer traits that the concrete context should implement as the super trait. The check trait has an empty body, followed by a blanket implementation for the target concrete context.

In the example above, we define the check trait CanUsePerson, which is used to check that the concrete context Person implements CanFormatToString and CanParseFromString. If we try to compile the check trait with the same example code as before, we would get the following error message:

error[E0277]: the trait bound `FormatAsJsonString: StringFormatter<Person>` is not satisfied
  --> debugging-techniques.md:330:23
   |
77 | impl CanUsePerson for Person {}
   |                       ^^^^^^ the trait `StringFormatter<Person>` is not implemented for `FormatAsJsonString`, which is required by `Person: CanFormatToString`
   |
   = help: the trait `StringFormatter<Context>` is implemented for `FormatAsJsonString`
note: required for `PersonComponents` to implement `StringFormatter<Person>`
  --> debugging-techniques.md:265:1
   |
12 | / #[cgp_component {
13 | |    name: StringFormatterComponent,
14 | |    provider: StringFormatter,
15 | |    context: Context,
16 | | }]
   | |__^
note: required for `Person` to implement `CanFormatToString`
  --> debugging-techniques.md:265:1
   |
12 | / #[cgp_component {
13 | |    name: StringFormatterComponent,
14 | |    provider: StringFormatter,
15 | |    context: Context,
   | |             ^^^^^^^
16 | | }]
   | |__^
note: required by a bound in `CanUsePerson`
  --> debugging-techniques.md:326:5
   |
72 | pub trait CanUsePerson:
   |           ------------ required by a bound in this trait
73 |     CanFormatToString
   |     ^^^^^^^^^^^^^^^^^ required by this bound in `CanUsePerson`
   = note: `CanUsePerson` is a "sealed trait", because to implement it you also need to implement `main::_doctest_main_debugging_techniques_md_255_0::CanFormatToString`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it
   = help: the following type implements the trait:
             Context
   = note: this error originates in the attribute macro `cgp_component` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 1 previous error

The error message is still pretty confusing, but it is slightly more informative than the previous error. Here, we can see that the top of the error says that StringFormatter<Person> is not implemented for FormatAsJsonString. Although it does not point to the root cause, at least we are guided to look into the implementation of FormatAsJsonString to find out what went wrong there.

At the moment, there is no better way to simplify debugging further, and we need to manually look into FormatAsJsonString to check why it could not implement StringFormatter for the Person context. Here, the important thing to look for is the additional constraints that FormatAsJsonString requires on the context, which in this case is Serialize.

We can make use of the check trait to further track down all the indirect dependencies of the providers that the concrete context uses. In this case, we determine that Serialize is needed for FormatAsJsonString, and Deserialize is needed for ParseFromJsonString. So we add them into the super trait of CanUsePerson as follows:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;
extern crate cgp;

use cgp::prelude::*;
use anyhow::Error;
use serde::{Serialize, Deserialize};

#[cgp_component {
   name: StringFormatterComponent,
   provider: StringFormatter,
   context: Context,
}]
pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

#[cgp_component {
   name: StringParserComponent,
   provider: StringParser,
   context: Context,
}]
pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub struct FormatAsJsonString;

impl<Context> StringFormatter<Context> for FormatAsJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string(context)?)
    }
}

pub struct ParseFromJsonString;

impl<Context> StringParser<Context> for ParseFromJsonString
where
    Context: for<'a> Deserialize<'a>,
{
    fn parse_from_string(json_str: &str) -> Result<Context, Error> {
        Ok(serde_json::from_str(json_str)?)
    }
}

// Note: We forgot to derive Serialize here
#[derive(Deserialize, Debug, Eq, PartialEq)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

pub struct PersonComponents;

impl HasComponents for Person {
    type Components = PersonComponents;
}

delegate_components! {
    PersonComponents {
        StringFormatterComponent: FormatAsJsonString,
        StringParserComponent: ParseFromJsonString,
    }
}

pub trait CanUsePerson:
    Serialize
    + for<'a> Deserialize<'a>
    + CanFormatToString
    + CanParseFromString
{}

impl CanUsePerson for Person {}
}

When we try to compile CanUsePerson again, we would see a different error message at the top:

error[E0277]: the trait bound `Person: Serialize` is not satisfied
   |
71 | impl CanUsePerson for Person {}
   |                       ^^^^^^ the trait `Serialize` is not implemented for `Person`
   |
   = note: for local types consider adding `#[derive(serde::Serialize)]` to your `Person` type
   = note: for types from other crates check whether the crate offers a `serde` feature flag

This tells us that we have forgotten to implement Serialize for Person. We can then take our action to properly fill in the missing dependencies.

Debugging Check Traits

Due to the need for check traits, the work for implementing a concrete context often involves wiring up the providers, and then checking that the providers and all their dependencies are implemented. As the number of components increase, the number of dependencies we need to check also increase accordingly.

When encountering errors in the check traits, it often helps to comment out a large portion of the dependencies, to focus on resolving the errors arise from a specific dependency. For example, in the check trait we can temporarily check for the implementation of CanFormatToString by commenting out all other constraints as follows:

pub trait CanUsePerson:
    Sized
    // + Serialize
    // + for<'a> Deserialize<'a>
    + CanFormatToString
    // + CanParseFromString
{}

We add a dummy constaint like Sized in the beginning of the super traits for CanUsePerson, so that we can easily comment out individual lines and not worry about whether it would lead to a dangling + sign. We can then pin point the error to a specific provider, and then continue tracing the missing dependencies from there. We would then notice that FormatAsJsonString requires Serialize, which we can then update the commented code to:

pub trait CanUsePerson:
    Sized
    + Serialize
    // + for<'a> Deserialize<'a>
    // + CanFormatToString
    // + CanParseFromString
{}

This technique can hopefully help speed up the debugging process, and determine which dependency is missing.

Improving The Compiler Error Message

The need of manual debugging using check traits is probably one of the major blockers for spreading CGP for wider adoption. However, it is possible to improve the error messages returned from the Rust compiler so that we can more easily find out what went wrong.

When Rust fails to resolve the constraints for Person: CanFormatString, it in fact knows that the reason for the failure is caused by unsatisfied indirect constraints such as Person: Serialize. So what we need to do is to make Rust prints out the unsatisfied constraints.

This has been reported as issue #134346 at the Rust project, with a pending fix available as pull request #134348. But until the patch is merged and stabilized, you can try to use our custom fork of the Rust compiler to debug any CGP error.

Following are the steps to use the modified Rust compiler:

  • Clone our fork of the Rust compiler at https://github.com/contextgeneric/rust.git, or add it as a secondary git remote.
  • Checkout the branch cgp, which applies the error reporting patch to the latest stable Rust.
  • Build the Rust compiler following the official guide. The steps should be something as follows:
    • Run ./x setup
    • Run ./x build
    • Run ./x build proc-macro-srv-cli, if you wan to use the forked compiler with Rust Analyzer.
    • Optionally, run ./x build --stage 2 and ./x build --stage 2 proc-macro-srv-cli, if you want to use the stage 2 compiler.
  • Link the compiled custom compiler using rustup link, aliasing it to a custom name like cgp. e.g. rustup toolchain link cgp build/host/stage1.
    • Link with build/host/stage2 if you want to use the stage 2 compiler.
  • Run your project using the custom compiler, e.g. cargo +cgp check.
  • To use this with Rust Analyzer, set channel = "cgp" inside your project's rust-toolchain.toml file.

If everything is working, you should see similar error messages as before, but with additional information included:

error[E0277]: the trait bound `FormatAsJsonString: StringFormatter<Person>` is not satisfied
  --> content/debugging-techniques.md:330:23
   |
77 | impl CanUsePerson for Person {}
   |                       ^^^^^^ the trait `StringFormatter<Person>` is not implemented for `FormatAsJsonString`
   |
   = help: the following constraint is not satisfied: `Person: Serialize`
   = help: the trait `StringFormatter<Context>` is implemented for `FormatAsJsonString`
note: required for `PersonComponents` to implement `StringFormatter<Person>`

After the main error is shown, we can also see an extra help hint that says: the following constraint is not satisfied: Person: Serialize. Thanks to that, we can now much more easily pin point the source of error, and proceed to fix our CGP program.

We hope that our patch will soon be accepted by the Rust project, so that future versions of Rust will be more accessible for CGP. When that happens, we will update this chapter to reflect the changes.

Associated Types

In the first part of this book, we explored how CGP leverages Rust's trait system to wire up components using blanket implementations. Because CGP operates within Rust's trait system, it allows us to incorporate advanced Rust features to create new design patterns. In this chapter, we will focus on using associated types with CGP to define context-generic providers that are generic over multiple abstract types.

Building Authentication Components

Suppose we want to build a simple authentication system using bearer tokens with an expiration time. To achieve this, we need to fetch the expiration time of a valid token and ensure that it is not in the past. A naive approach to implementing the authentication might look like the following:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

pub mod main {
pub mod traits {
    use anyhow::Error;
    use cgp::prelude::*;

    #[cgp_component {
        provider: AuthTokenValidator,
    }]
    pub trait CanValidateAuthToken {
        fn validate_auth_token(&self, auth_token: &str) -> Result<(), Error>;
    }

    #[cgp_component {
        provider: AuthTokenExpiryFetcher,
    }]
    pub trait CanFetchAuthTokenExpiry {
        fn fetch_auth_token_expiry(&self, auth_token: &str) -> Result<u64, Error>;
    }

    #[cgp_component {
        provider: CurrentTimeGetter,
    }]
    pub trait HasCurrentTime {
        fn current_time(&self) -> Result<u64, Error>;
    }
}

pub mod impls {
    use std::time::{SystemTime, UNIX_EPOCH};

    use anyhow::{anyhow, Error};

    use super::traits::*;

    pub struct ValidateTokenIsNotExpired;

    impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
    where
        Context: HasCurrentTime + CanFetchAuthTokenExpiry,
    {
        fn validate_auth_token(context: &Context, auth_token: &str) -> Result<(), Error> {
            let now = context.current_time()?;

            let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

            if token_expiry < now {
                Ok(())
            } else {
                Err(anyhow!("auth token has expired"))
            }
        }
    }

    pub struct GetSystemTimestamp;

    impl<Context> CurrentTimeGetter<Context> for GetSystemTimestamp {
        fn current_time(_context: &Context) -> Result<u64, Error> {
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)?
                .as_millis()
                .try_into()?;

            Ok(now)
        }
    }
}

pub mod contexts {
    use std::collections::BTreeMap;

    use anyhow::anyhow;
    use cgp::prelude::*;

    use super::impls::*;
    use super::traits::*;

    pub struct MockApp {
        pub auth_tokens_store: BTreeMap<String, u64>,
    }

    pub struct MockAppComponents;

    impl HasComponents for MockApp {
        type Components = MockAppComponents;
    }

    delegate_components! {
        MockAppComponents {
            CurrentTimeGetterComponent: GetSystemTimestamp,
            AuthTokenValidatorComponent: ValidateTokenIsNotExpired,
        }
    }

    impl AuthTokenExpiryFetcher<MockApp> for MockAppComponents {
        fn fetch_auth_token_expiry(
            context: &MockApp,
            auth_token: &str,
        ) -> Result<u64, anyhow::Error> {
            context
                .auth_tokens_store
                .get(auth_token)
                .cloned()
                .ok_or_else(|| anyhow!("invalid auth token"))
        }
    }

    pub trait CanUseMockApp: CanValidateAuthToken {}

    impl CanUseMockApp for MockApp {}
}

}
}

In this example, we first define the CanValidateAuthToken trait, which serves as the primary API for validating authentication tokens. To facilitate the implementation of the validator, we also define the CanFetchAuthTokenExpiry trait, which is responsible for fetching the expiration time of an authentication token — assuming the token is valid. Finally, the HasCurrentTime trait is introduced to retrieve the current time.

Next, we define a context-generic provider, ValidateTokenIsNotExpired, which validates authentication tokens by comparing their expiration time with the current time. The provider fetches both the token’s expiration time and the current time, and ensure that the token is still valid. Additionally, we define another context-generic provider, GetSystemTimestamp, which retrieves the current time using std::time::SystemTime::now().

For this demonstration, we introduce a concrete context, MockApp, which includes an auth_tokens_store field. This store is a mocked collection of authentication tokens with their respective expiration times, stored in a BTreeMap. We also implement the AuthTokenExpiryFetcher trait specifically for the MockApp context, which retrieves expiration times from the mocked auth_tokens_store. Lastly, we define the CanUseMockApp trait, ensuring that MockApp properly implements the CanValidateAuthToken trait through the provided wiring.

Abstract Types

The previous example demonstrates basic CGP techniques for implementing a reusable provider, ValidateTokenIsNotExpired, which can work with different concrete contexts. However, the method signatures are tied to specific types. For instance, we use String to represent the authentication token and u64 to represent the Unix timestamp in milliseconds.

Common practice suggests that we should use distinct types to differentiate values from different domains, reducing the chance of mixing them up. A common approach in Rust is to use the newtype pattern to define wrapper types, like so:

#![allow(unused)]
fn main() {
pub struct AuthToken {
    value: String,
}

pub struct Time {
    value: u64,
}
}

While the newtype pattern helps abstract over underlying values, it doesn't fully generalize the code to work with different types. For example, instead of defining our own Time type with Unix timestamp semantics, we may want to use a datetime library such as datetime or chrono. The choice of library could depend on the specific use case of a concrete application.

A more flexible approach is to define an abstract Time type that allows us to implement context-generic providers compatible with any Time type chosen by the concrete context. This can be achieved in CGP by defining type traits that contain associated types:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}
}

Here, we define the HasTimeType trait with an associated type Time, which is constrained to types that implement Eq and Ord so that they can be compared. Similarly, the HasAuthTokenType trait defines an associated type AuthToken, without any additional constraints.

Similar to regular trait methods, CGP allows us to auto-derive blanket implementations that delegate the associated types to providers using HasComponents and DelegateComponent. Therefore, we can use #[cgp_component] on traits containing associated types as well.

With these type traits in place, we can now update our authentication components to leverage abstract types within the trait methods:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use std::time::Instant;

use anyhow::Error;
use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType {
    fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}
}

Here, we modify the CanValidateAuthToken trait to include HasAuthTokenType as a supertrait, allowing it to accept the abstract type Self::AuthToken as a method parameter. Likewise, CanFetchAuthTokenExpiry requires both HasAuthTokenType and HasTimeType, while HasCurrentTime only requires HasTimeType.

With the abstract types defined, we can now update ValidateTokenIsNotExpired to work generically with any Time and AuthToken types:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use anyhow::{anyhow, Error};
use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType {
    fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}

pub struct ValidateTokenIsNotExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime + CanFetchAuthTokenExpiry,
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err(anyhow!("auth token has expired"))
        }
    }
}
}

This example shows how CGP enables us to define context-generic providers that are not just generic over the context itself, but also over its associated types. Unlike traditional generic programming, where all generic parameters are specified positionally, CGP allows us to parameterize abstract types using names via associated types.

Defining Abstract Type Traits with cgp_type!

The type traits HasTimeType and HasAuthTokenType share a similar structure, and as you define more abstract types, this boilerplate can become tedious. To streamline the process, the cgp crate provides the cgp_type! macro, which simplifies type trait definitions.

Here's how you can define the same types with cgp_type!:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( Time: Eq + Ord );
cgp_type!( AuthToken );
}

The cgp_type! macro accepts the name of an abstract type, $name, along with any applicable constraints for that type. It then automatically generates the same implementation as the cgp_component macro: a consumer trait named Has{$name}Type, a provider trait named Provide{$name}Type, and a component name type named ${name}TypeComponent. Each of the generated traits includes an associated type defined as type $name: $constraints;. In addition, cgp_type! also derives some other implementations, which we'll explore in later chapters.

Trait Minimalism

At first glance, it might seem overly verbose to define multiple type traits and require each to be explicitly included as a supertrait of a method interface. For instance, you might be tempted to consolidate the methods and types into a single trait, like this:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use cgp::prelude::*;
use anyhow::Error;

#[cgp_component {
    provider: AppImpl,
}]
pub trait AppTrait {
    type Time: Eq + Ord;

    type AuthToken;

    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;

    fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;

    fn current_time(&self) -> Result<Self::Time, Error>;
}
}

While this approach might seem simpler, it introduces unnecessary coupling between potentially unrelated types and methods. For example, an application implementing token validation might delegate this functionality to an external microservice. In such a case, it is redundant to require the application to specify a Time type that it doesn’t actually use.

In practice, we find the practical benefits of defining many minimal traits often outweight any theoretical advantages of combining multiple items into one trait. As we will demonstrate in later chapters, having traits that contain only one type or method would also enable more advanced CGP patterns to be applied, including the use of cgp_type! that we have just covered.

We encourage readers to embrace minimal traits without concern for theoretical overhead. However, during the early phases of a project, you might prefer to consolidate items to reduce cognitive overload while learning or prototyping. As the project matures, you can always refactor and decompose larger traits into smaller, more focused ones, following the techniques outlined in this book.

Impl-Side Associated Type Constraints

The minimalism philosophy of CGP extends to the constraints placed on associated types within type traits. Consider the earlier definition of HasTimeType:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( Time: Eq + Ord );
}

Here, the associated Time type is constrained by Eq + Ord. This means that all concrete implementations of Time must satisfy these constraints, regardless of whether they are actually required by the providers. In fact, if we revisit our previous examples, we notice that the Eq constraint isn’t used anywhere.

Such overly restrictive constraints can become a bottleneck as the application evolves. As complexity increases, it’s common to require additional traits on Time, such as Debug + Display + Clone + Hash + Serialize + Deserialize and so on. Imposing these constraints globally limits flexibility and makes it harder to adapt to changing requirements.

Fortunately, CGP allows us to apply the same principle of impl-side dependencies to associated type constraints. Consider the following example:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use anyhow::{anyhow, Error};
use cgp::prelude::*;

cgp_type!( Time );

cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType {
    fn fetch_auth_token_expiry(&self, auth_token: &Self::AuthToken) -> Result<Self::Time, Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}

pub struct ValidateTokenIsNotExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime + CanFetchAuthTokenExpiry,
    Context::Time: Ord,
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err(anyhow!("auth token has expired"))
        }
    }
}
}

In this example, we redefine HasTimeType::Time without any constraints. Instead, we specify the constraint Context::Time: Ord in the provider implementation for ValidateTokenIsNotExpired. This ensures that the ValidateTokenIsNotExpired provider can compare the token expiry time using Ord, while avoiding unnecessary global constraints on Time.

By applying constraints on the implementation side, we can conditionally require HasTimeType::Time to implement Ord, but only when the ValidateTokenIsNotExpired provider is in use. This approach allows abstract types to scale flexibly alongside generic context types, enabling the same CGP patterns to be applied to abstract types.

In some cases, it can still be convenient to include constraints (e.g., Debug) directly on an associated type, especially if those constraints are nearly universal across providers. Additionally, current Rust error reporting often produces clearer error messages when constraints are defined at the associated type level, as opposed to being deferred to the implementation.

As a guideline, we recommend that readers begin by defining type traits without placing constraints on associated types, relying instead on implementation-side constraints wherever possible. However, readers may choose to apply global constraints to associated types when appropriate, particularly for simple and widely applicable traits like Debug and Eq.

Type Providers

With type abstraction in place, we can define context-generic providers for the Time and AuthToken abstract types. For example, we can create a provider that uses std::time::Instant as the Time type:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use std::time::Instant;

use cgp::prelude::*;
use anyhow::Error;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}

pub struct UseInstant;

impl<Context> ProvideTimeType<Context> for UseInstant {
    type Time = Instant;
}

impl<Context> CurrentTimeGetter<Context> for UseInstant
where
    Context: HasTimeType<Time = Instant>,
{
    fn current_time(_context: &Context) -> Result<Instant, Error> {
        Ok(Instant::now())
    }
}
}

Here, the UseInstant provider implements ProvideTimeType for any Context type by setting the associated type Time to Instant. Additionally, it implements CurrentTimeGetter for any Context, provided that Context::Time is Instant. This type equality constraint works similarly to regular implementation-side dependencies and is frequently used for scope-limited access to a concrete type associated with an abstract type.

The type equality constraint is necessary because a given context might not always use UseInstant as the provider for ProvideTimeType. Instead, the context could choose a different provider that uses another type to represent Time. Consequently, UseInstant can only implement CurrentTimeGetter if the Context uses it or another provider that also uses Instant as its Time type.

Aside from Instant, we can also define alternative providers for Time, using other types like datetime::LocalDateTime:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;
extern crate datetime;

use cgp::prelude::*;
use anyhow::Error;
use datetime::LocalDateTime;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time: Eq + Ord;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType {
    fn current_time(&self) -> Result<Self::Time, Error>;
}

pub struct UseLocalDateTime;

impl<Context> ProvideTimeType<Context> for UseLocalDateTime {
    type Time = LocalDateTime;
}

impl<Context> CurrentTimeGetter<Context> for UseLocalDateTime
where
    Context: HasTimeType<Time = LocalDateTime>,
{
    fn current_time(_context: &Context) -> Result<LocalDateTime, Error> {
        Ok(LocalDateTime::now())
    }
}
}

Since our application only requires the Time type to implement Ord and the ability to retrieve the current time, we can easily swap between different time providers, as long as they meet these dependencies. As the application evolves, additional constraints might be introduced on the Time type, potentially limiting the available concrete time types. However, with CGP, we can incrementally introduce new dependencies based on the application’s needs, avoiding premature restrictions caused by unused requirements.

Similarly, for the abstract AuthToken type, we can define a context-generic provider ProvideAuthTokenType that uses String as its implementation:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct UseStringAuthToken;

impl<Context> ProvideAuthTokenType<Context> for UseStringAuthToken {
    type AuthToken = String;
}
}

Compared to the newtype pattern, we can use plain String values directly, without wrapping them in a newtype struct. Contrary to common wisdom, in CGP, we place less emphasis on wrapping every domain type in a newtype. This is particularly true when most of the application is written in a context-generic style. The rationale is that abstract types and their accompanying interfaces already fulfill the role of newtypes by encapsulating and "protecting" raw values, reducing the need for additional wrapping.

That said, readers are free to define newtypes and use them alongside abstract types. For beginners, this can be especially useful, as later chapters will explore methods to properly restrict access to underlying concrete types in context-generic code. Additionally, newtypes remain valuable when the raw values are also used in non-context-generic code, where access to the concrete types is unrestricted.

Throughout this book, we will primarily use plain types to implement abstract types, without additional newtype wrapping. However, we will revisit the comparison between newtypes and abstract types in later chapters, providing further guidance on when each approach is most appropriate.

The UseType Pattern

Implementing type providers can quickly become repetitive as the number of abstract types grows. For example, to use String as the AuthToken type, we first need to define a new struct, UseStringAuthToken, and then implement ProvideAuthTokenType for it. To streamline this process, the cgp_type! macro simplifies the implementation by automatically generating a provider using the UseType pattern. The generated implementation looks like this:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct UseType<Type>(pub PhantomData<Type>);

impl<Context, AuthToken> ProvideAuthTokenType<Context> for UseType<AuthToken> {
    type AuthToken = AuthToken;
}
}

Here, UseType is a marker type with a generic parameter Type, representing the type to be used for a given type trait. Since PhantomData is its only field, UseType is never intended to be used as a runtime value. The generic implementation of ProvideAuthTokenType for UseType ensures that the AuthToken type is directly set to the Type parameter of UseType.

With this generic implementation, we can redefine UseStringAuthToken as a simple type alias for UseType<String>:

#![allow(unused)]
fn main() {
use core::marker::PhantomData;

pub struct UseType<Type>(pub PhantomData<Type>);

type UseStringAuthToken = UseType<String>;
}

In fact, we can even skip defining type aliases altogether and use UseType directly in the delegate_components macro when wiring type providers.

The UseType struct is included in the cgp crate, and when you define an abstract type using the cgp_type! macro, the corresponding generic UseType implementation is automatically derived. This makes UseType a powerful tool for simplifying component wiring and reducing boilerplate in your code.

Putting It Altogether

With all the pieces in place, we can now apply what we've learned and refactor our naive authentication components to utilize abstract types, as shown below:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;
extern crate datetime;

pub mod main {
pub mod traits {
    use anyhow::Error;
    use cgp::prelude::*;

    cgp_type!( Time );
    cgp_type!( AuthToken );

    #[cgp_component {
        provider: AuthTokenValidator,
    }]
    pub trait CanValidateAuthToken: HasAuthTokenType {
        fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Error>;
    }

    #[cgp_component {
        provider: AuthTokenExpiryFetcher,
    }]
    pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType {
        fn fetch_auth_token_expiry(
            &self,
            auth_token: &Self::AuthToken,
        ) -> Result<Self::Time, Error>;
    }

    #[cgp_component {
        provider: CurrentTimeGetter,
    }]
    pub trait HasCurrentTime: HasTimeType {
        fn current_time(&self) -> Result<Self::Time, Error>;
    }
}

pub mod impls {
    use anyhow::{anyhow, Error};
    use cgp::prelude::*;
    use datetime::LocalDateTime;

    use super::traits::*;

    pub struct ValidateTokenIsNotExpired;

    impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
    where
        Context: HasCurrentTime + CanFetchAuthTokenExpiry,
        Context::Time: Ord,
    {
        fn validate_auth_token(
            context: &Context,
            auth_token: &Context::AuthToken,
        ) -> Result<(), Error> {
            let now = context.current_time()?;

            let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

            if token_expiry < now {
                Ok(())
            } else {
                Err(anyhow!("auth token has expired"))
            }
        }
    }

    pub struct UseLocalDateTime;

    impl<Context> ProvideTimeType<Context> for UseLocalDateTime {
        type Time = LocalDateTime;
    }

    impl<Context> CurrentTimeGetter<Context> for UseLocalDateTime
    where
        Context: HasTimeType<Time = LocalDateTime>,
    {
        fn current_time(_context: &Context) -> Result<LocalDateTime, Error> {
            Ok(LocalDateTime::now())
        }
    }
}

pub mod contexts {
    use std::collections::BTreeMap;

    use anyhow::anyhow;
    use cgp::prelude::*;
    use datetime::LocalDateTime;

    use super::impls::*;
    use super::traits::*;

    pub struct MockApp {
        pub auth_tokens_store: BTreeMap<String, LocalDateTime>,
    }

    pub struct MockAppComponents;

    impl HasComponents for MockApp {
        type Components = MockAppComponents;
    }

    delegate_components! {
        MockAppComponents {
            [
                TimeTypeComponent,
                CurrentTimeGetterComponent,
            ]: UseLocalDateTime,
            AuthTokenTypeComponent: UseType<String>,
            AuthTokenValidatorComponent: ValidateTokenIsNotExpired,
        }
    }

    impl AuthTokenExpiryFetcher<MockApp> for MockAppComponents {
        fn fetch_auth_token_expiry(
            context: &MockApp,
            auth_token: &String,
        ) -> Result<LocalDateTime, anyhow::Error> {
            context
                .auth_tokens_store
                .get(auth_token)
                .cloned()
                .ok_or_else(|| anyhow!("invalid auth token"))
        }
    }

    pub trait CanUseMockApp: CanValidateAuthToken {}

    impl CanUseMockApp for MockApp {}
}

}
}

Compared to our earlier approach, it is now much easier to update the MockApp context to use different time and auth token providers. If different use cases require distinct concrete types, we can easily define additional context types with different configurations, all without duplicating the core logic.

So far, we have applied abstract types to the Time and AuthToken types, but we are still relying on the concrete anyhow::Error type. In the next chapter, we will explore error handling in depth and learn how to use abstract error types to improve the way application errors are managed.

Error Handling

Rust introduces a modern approach to error handling through the use of the Result type, which explicitly represents errors. Unlike implicit exceptions commonly used in other mainstream languages, the Result type offers several advantages. It clearly indicates when errors may occur and specifies the type of errors that might be encountered when calling a function. However, the Rust community has yet to reach a consensus on the ideal error type to use within a Result.

Choosing an appropriate error type is challenging because different applications have distinct requirements. For instance, should the error include stack traces? Can it be compatible with no_std environments? How should the error message be presented? Should it include structured metadata for introspection or specialized logging? How can different errors be distinguished to determine whether an operation should be retried? How can error sources from various libraries be composed or flattened effectively? These and other concerns complicate the decision-making process.

Because of these cross-cutting concerns, discussions in the Rust community about finding a universally optimal error type are never ending. Currently, the ecosystem tends to favor libraries like anyhow that store error values using some form of dynamic typing. While convenient, these approaches sacrifice some benefits of static typing, such as the ability to determine at compile time whether a function cannot produce certain errors.

CGP offers an alternative approach to error handling: using abstract error types within Result alongside a context-generic mechanism for raising errors without requiring a specific error type. In this chapter, we will explore this new approach, demonstrating how it allows error handling to be tailored to an application's precise needs.

Abstract Error Type

In the previous chapter, we explored how to use associated types with CGP to define abstract types. Similarly to abstract types like Time and AuthToken, we can define an abstract Error type as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::fmt::Debug;

use cgp::prelude::*;

cgp_type!( Error: Debug );
}

The HasErrorType trait is particularly significant because it serves as a standard type API for all CGP components that involve abstract errors. Its definition is intentionally minimal, consisting of a single associated type, Error, constrained by Debug by default. This Debug constraint was chosen because many Rust APIs, such as Result::unwrap, rely on error types implementing Debug.

Given its ubiquity, the HasErrorType trait is included as part of the cgp crate and is available in the prelude. Therefore, we will use the version provided by cgp rather than redefining it locally in subsequent examples.

Building on the example from the previous chapter, we can update authentication components to leverage the abstract error type defined by HasErrorType:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( Time );
cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType {
    fn fetch_auth_token_expiry(
        &self,
        auth_token: &Self::AuthToken,
    ) -> Result<Self::Time, Self::Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType + HasErrorType {
    fn current_time(&self) -> Result<Self::Time, Self::Error>;
}
}

In these examples, each trait now includes HasErrorType as a supertrait, and methods return Self::Error in the Result type instead of relying on a concrete type like anyhow::Error. This abstraction allows greater flexibility and customization, enabling components to adapt their error handling to the specific needs of different contexts.

Raising Errors With From

After adopting abstract errors in our component interfaces, the next challenge is handling these abstract errors in context-generic providers. With CGP, this is achieved by leveraging impl-side dependencies and adding constraints to the Error type, such as requiring it to implement From. This allows for the conversion of a source error into an abstract error value.

For example, we can modify the ValidateTokenIsNotExpired provider to convert a source error, &'static str, into Context::Error when an authentication token has expired:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( Time );
cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType {
    fn fetch_auth_token_expiry(
        &self,
        auth_token: &Self::AuthToken,
    ) -> Result<Self::Time, Self::Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType + HasErrorType {
    fn current_time(&self) -> Result<Self::Time, Self::Error>;
}

pub struct ValidateTokenIsNotExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime + CanFetchAuthTokenExpiry + HasErrorType,
    Context::Time: Ord,
    Context::Error: From<&'static str>
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Context::Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err("auth token has expired".into())
        }
    }
}
}

This example demonstrates how CGP simplifies "stringy" error handling in context-generic providers by delegating the conversion from strings to concrete error values to the application. While using string errors is generally not a best practice, it is useful during the prototyping phase when precise error handling strategies are not yet established.

CGP encourages an iterative approach to error handling. Developers can begin with string errors for rapid prototyping and transition to structured error handling as the application matures. For example, we can replace the string error with a custom error type like ErrAuthTokenHasExpired:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::fmt::Display;

use cgp::prelude::*;

cgp_type!( Time );
cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType {
    fn fetch_auth_token_expiry(
        &self,
        auth_token: &Self::AuthToken,
    ) -> Result<Self::Time, Self::Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType + HasErrorType {
    fn current_time(&self) -> Result<Self::Time, Self::Error>;
}

pub struct ValidateTokenIsNotExpired;

#[derive(Debug)]
pub struct ErrAuthTokenHasExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime + CanFetchAuthTokenExpiry + HasErrorType,
    Context::Time: Ord,
    Context::Error: From<ErrAuthTokenHasExpired>
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Context::Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err(ErrAuthTokenHasExpired.into())
        }
    }
}

impl Display for ErrAuthTokenHasExpired {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "auth token has expired")
    }
}
}

In this example, we introduced the ErrAuthTokenHasExpired type to represent the specific error of an expired authentication token. The AuthTokenValidator implementation requires Context::Error to implement From<ErrAuthTokenHasExpired> for conversion to the abstract error type. Additionally, ErrAuthTokenHasExpired implements both Debug and Display, allowing applications to present and log the error meaningfully.

CGP facilitates defining provider-specific error types like ErrAuthTokenHasExpired without burdening the provider with embedding these errors into the application's overall error handling strategy. With impl-side dependencies, constraints like Context::Error: From<ErrAuthTokenHasExpired> apply only when the application uses a specific provider. If an application employs a different provider to implement AuthTokenValidator, it does not need to handle the ErrAuthTokenHasExpired error.

Raising Errors using CanRaiseError

In the previous section, we used the From constraint in the ValidateTokenIsNotExpired provider to raise errors such as &'static str or ErrAuthTokenHasExpired. While this approach is elegant, we quickly realize it doesn't work with common error types like anyhow::Error. This is because anyhow::Error only provides a blanket From implementation only for types that implement core::error::Error + Send + Sync + 'static.

This restriction is a common pain point when using error libraries like anyhow. The reason for this limitation is that without CGP, a type like anyhow::Error cannot provide multiple blanket From implementations without causing conflicts. As a result, using From can leak abstractions, forcing custom error types like ErrAuthTokenHasExpired to implement common traits like core::error::Error. Another challenge is that ownership rules prevent supporting custom From implementations for non-owned types like String and &str.

To address these issues, we recommend using a more flexible — though slightly more verbose—approach with CGP: the CanRaiseError trait, rather than relying on From for error conversion. Here's how we define it:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[cgp_component {
    provider: ErrorRaiser,
}]
pub trait CanRaiseError<SourceError>: HasErrorType {
    fn raise_error(e: SourceError) -> Self::Error;
}
}

The CanRaiseError trait has a generic parameter SourceError, representing the source error type that will be converted into the abstract error type HasErrorType::Error. By making it a generic parameter, this allows a context to raise multiple source error types and convert them into the abstract error.

Since raising errors is common in most CGP code, the CanRaiseError trait is included in the CGP prelude, so we don’t need to define it manually.

We can now update the ValidateTokenIsNotExpired provider to use CanRaiseError instead of From for error handling, raising a source error like &'static str:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( Time );
cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType {
    fn fetch_auth_token_expiry(
        &self,
        auth_token: &Self::AuthToken,
    ) -> Result<Self::Time, Self::Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType + HasErrorType {
    fn current_time(&self) -> Result<Self::Time, Self::Error>;
}

pub struct ValidateTokenIsNotExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime + CanFetchAuthTokenExpiry + CanRaiseError<&'static str>,
    Context::Time: Ord,
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Context::Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err(Context::raise_error("auth token has expired"))
        }
    }
}
}

In this updated implementation, we replace the Context: HasErrorType constraint with Context: CanRaiseError<&'static str>. Since HasErrorType is a supertrait of CanRaiseError, we only need to include CanRaiseError in the constraint to automatically include HasErrorType. We also use the Context::raise_error method to convert the string "auth token has expired" into Context::Error.

This approach avoids the limitations of From and offers greater flexibility for error handling in CGP, especially when working with third-party error types like anyhow::Error.

Context-Generic Error Raisers

By defining the CanRaiseError trait using CGP, we overcome the limitations of From and enable context-generic error raisers that work across various source error types. For instance, we can create a context-generic error raiser for anyhow::Error as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use cgp::core::error::{ErrorRaiser, HasErrorType};

pub struct RaiseAnyhowError;

impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseAnyhowError
where
    Context: HasErrorType<Error = anyhow::Error>,
    SourceError: core::error::Error + Send + Sync + 'static,
{
    fn raise_error(e: SourceError) -> anyhow::Error {
        e.into()
    }
}
}

Here, RaiseAnyhowError is a provider that implements the ErrorRaiser trait with generic Context and SourceError. The implementation is valid only if the Context implements HasErrorType and implements Context::Error as anyhow::Error. Additionally, the SourceError must satisfy core::error::Error + Send + Sync + 'static, which is necessary for the From implementation provided by anyhow::Error. Inside the method body, the source error is converted into anyhow::Error using e.into() since the required constraints are already satisfied.

For a more generalized approach, we can create a provider that works with any error type supporting From:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::core::error::{ErrorRaiser, HasErrorType};

pub struct RaiseFrom;

impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseFrom
where
    Context: HasErrorType,
    Context::Error: From<SourceError>,
{
    fn raise_error(e: SourceError) -> Context::Error {
        e.into()
    }
}
}

This implementation requires the Context to implement HasErrorType and the Context::Error type to implement From<SourceError>. With these constraints in place, this provider allows errors to be raised from any source type to Context::Error using From, without requiring explicit coupling in providers like ValidateTokenIsNotExpired.

The introduction of CanRaiseError might seem redundant when it ultimately relies on From in some cases. However, the purpose of this indirection is to enable alternative mechanisms for converting errors when From is insufficient or unavailable. For example, we can define an error raiser for anyhow::Error that uses the Debug trait instead of From:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use core::fmt::Debug;

use anyhow::anyhow;
use cgp::core::error::{ErrorRaiser, HasErrorType};

pub struct DebugAnyhowError;

impl<Context, SourceError> ErrorRaiser<Context, SourceError> for DebugAnyhowError
where
    Context: HasErrorType<Error = anyhow::Error>,
    SourceError: Debug,
{
    fn raise_error(e: SourceError) -> anyhow::Error {
        anyhow!("{e:?}")
    }
}
}

In this implementation, the DebugAnyhowError provider raises any source error into an anyhow::Error, as long as the source error implements Debug. The raise_error method uses the anyhow! macro and formats the source error using the Debug trait. This approach allows a concrete context to use providers like ValidateTokenIsNotExpired while relying on DebugAnyhowError to raise source errors such as &'static str or ErrAuthTokenHasExpired, which only implement Debug or Display.

The cgp-error-anyhow Crate

The CGP project provides the cgp-error-anyhow crate, which includes the anyhow-specific providers discussed in this chapter. These constructs are offered as a separate crate rather than being part of the core cgp crate to avoid adding anyhow as a mandatory dependency.

In addition, CGP offers other error crates tailored to different error handling libraries. The cgp-error-eyre crate supports eyre::Error, while the cgp-error-std crate works with Box<dyn core::error::Error>.

As demonstrated in this chapter, CGP allows projects to easily switch between error handling implementations without being tightly coupled to a specific error type. For instance, if the application needs to run in a resource-constrained environment, replacing cgp-error-anyhow with cgp-error-std in the component wiring enables the application to use the simpler Box<dyn Error> type for error handling.

Putting It Altogether

With the use of HasErrorType, CanRaiseError, and cgp-error-anyhow, we can now refactor the full example from the previous chapter, and make it generic over the error type:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate cgp_error_anyhow;
extern crate anyhow;
extern crate datetime;

pub mod main {
pub mod traits {
    use cgp::prelude::*;

    cgp_type!( Time );
    cgp_type!( AuthToken );

    #[cgp_component {
        provider: AuthTokenValidator,
    }]
    pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType {
        fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>;
    }

    #[cgp_component {
        provider: AuthTokenExpiryFetcher,
    }]
    pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType {
        fn fetch_auth_token_expiry(
            &self,
            auth_token: &Self::AuthToken,
        ) -> Result<Self::Time, Self::Error>;
    }

    #[cgp_component {
        provider: CurrentTimeGetter,
    }]
    pub trait HasCurrentTime: HasTimeType + HasErrorType {
        fn current_time(&self) -> Result<Self::Time, Self::Error>;
    }
}

pub mod impls {
    use core::fmt::Debug;

    use anyhow::anyhow;
    use cgp::core::error::{ErrorRaiser, ProvideErrorType};
    use cgp::prelude::{CanRaiseError, HasErrorType};
    use datetime::LocalDateTime;

    use super::traits::*;

    pub struct ValidateTokenIsNotExpired;

    #[derive(Debug)]
    pub struct ErrAuthTokenHasExpired;

    impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
    where
        Context: HasCurrentTime + CanFetchAuthTokenExpiry + CanRaiseError<ErrAuthTokenHasExpired>,
        Context::Time: Ord,
    {
        fn validate_auth_token(
            context: &Context,
            auth_token: &Context::AuthToken,
        ) -> Result<(), Context::Error> {
            let now = context.current_time()?;

            let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

            if token_expiry < now {
                Ok(())
            } else {
                Err(Context::raise_error(ErrAuthTokenHasExpired))
            }
        }
    }

    pub struct UseLocalDateTime;

    impl<Context> ProvideTimeType<Context> for UseLocalDateTime {
        type Time = LocalDateTime;
    }

    impl<Context> CurrentTimeGetter<Context> for UseLocalDateTime
    where
        Context: HasTimeType<Time = LocalDateTime> + HasErrorType,
    {
        fn current_time(_context: &Context) -> Result<LocalDateTime, Context::Error> {
            Ok(LocalDateTime::now())
        }
    }
}

pub mod contexts {
    use std::collections::BTreeMap;

    use anyhow::anyhow;
    use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent};
    use cgp::prelude::*;
    use cgp_error_anyhow::{UseAnyhowError, DebugAnyhowError};
    use datetime::LocalDateTime;

    use super::impls::*;
    use super::traits::*;

    pub struct MockApp {
        pub auth_tokens_store: BTreeMap<String, LocalDateTime>,
    }

    pub struct MockAppComponents;

    impl HasComponents for MockApp {
        type Components = MockAppComponents;
    }

    delegate_components! {
        MockAppComponents {
            ErrorTypeComponent: UseAnyhowError,
            ErrorRaiserComponent: DebugAnyhowError,
            [
                TimeTypeComponent,
                CurrentTimeGetterComponent,
            ]: UseLocalDateTime,
            AuthTokenTypeComponent: UseType<String>,
            AuthTokenValidatorComponent: ValidateTokenIsNotExpired,
        }
    }

    impl AuthTokenExpiryFetcher<MockApp> for MockAppComponents {
        fn fetch_auth_token_expiry(
            context: &MockApp,
            auth_token: &String,
        ) -> Result<LocalDateTime, anyhow::Error> {
            context
                .auth_tokens_store
                .get(auth_token)
                .cloned()
                .ok_or_else(|| anyhow!("invalid auth token"))
        }
    }

    pub trait CanUseMockApp: CanValidateAuthToken {}

    impl CanUseMockApp for MockApp {}
}

}
}

In the updated code, we refactored ValidateTokenIsNotExpired to use CanRaiseError<ErrAuthTokenHasExpired>, with ErrAuthTokenHasExpired implementing only Debug. Additionally, we use the provider UseAnyhowError from cgp-error-anyhow, which implements ProvideErrorType by setting Error to anyhow::Error.

In the component wiring for MockAppComponents, we wire up ErrorTypeComponent with UseAnyhowError and ErrorRaiserComponent with DebugAnyhowError. In the context-specific implementation AuthTokenExpiryFetcher<MockApp>, we can now use anyhow::Error directly, since Rust already knows that MockApp::Error is the same type as anyhow::Error.

Conclusion

In this chapter, we provided a high-level overview of how error handling in CGP differs significantly from traditional error handling done in Rust. By utilizing abstract error types with HasErrorType, we can create providers that are generic over the concrete error type used by an application. The CanRaiseError trait allows us to implement context-generic error raisers, overcoming the limitations of non-overlapping implementations and enabling us to work with source errors that only implement traits like Debug.

However, error handling is a complex subject, and CGP abstractions such as HasErrorType and CanRaiseError are just the foundation for addressing this complexity. There are additional details related to error handling that we will explore in the upcoming chapters, preparing us to handle errors effectively in real-world applications.

Delegated Error Raisers

In the previous chapter, we defined context-generic error raisers such as RaiseFrom and DebugAnyhowError, which can be used to raise any source error that satisfies certain constraints. However, in the main wiring for MockAppComponents, we could only select a specific provider for the ErrorRaiserComponent.

In more complex applications, we might want to handle different source errors in different ways, depending on the type of the source error. For example, we might use RaiseFrom when a From instance is available, and default to DebugAnyhowError for cases where the source error implements Debug.

In this chapter, we will introduce the UseDelegate pattern, which provides a declarative approach to handle errors differently based on the source error type.

Ad Hoc Error Raisers

One way to handle source errors differently is by defining an error raiser provider with explicit implementations for each source error. For example:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

#[derive(Debug)]
pub struct ErrAuthTokenHasExpired;

use core::convert::Infallible;
use core::num::ParseIntError;

use anyhow::anyhow;
use cgp::core::error::ErrorRaiser;
use cgp::prelude::*;

pub struct MyErrorRaiser;

impl<Context> ErrorRaiser<Context, anyhow::Error> for MyErrorRaiser
where
    Context: HasErrorType<Error = anyhow::Error>,
{
    fn raise_error(e: anyhow::Error) -> anyhow::Error {
        e
    }
}

impl<Context> ErrorRaiser<Context, Infallible> for MyErrorRaiser
where
    Context: HasErrorType,
{
    fn raise_error(e: Infallible) -> Context::Error {
        match e {}
    }
}

impl<Context> ErrorRaiser<Context, std::io::Error> for MyErrorRaiser
where
    Context: HasErrorType<Error = anyhow::Error>,
{
    fn raise_error(e: std::io::Error) -> anyhow::Error {
        e.into()
    }
}

impl<Context> ErrorRaiser<Context, ParseIntError> for MyErrorRaiser
where
    Context: HasErrorType<Error = anyhow::Error>,
{
    fn raise_error(e: ParseIntError) -> anyhow::Error {
        e.into()
    }
}

impl<Context> ErrorRaiser<Context, ErrAuthTokenHasExpired> for MyErrorRaiser
where
    Context: HasErrorType<Error = anyhow::Error>,
{
    fn raise_error(e: ErrAuthTokenHasExpired) -> anyhow::Error {
        anyhow!("{e:?}")
    }
}

impl<Context> ErrorRaiser<Context, String> for MyErrorRaiser
where
    Context: HasErrorType<Error = anyhow::Error>,
{
    fn raise_error(e: String) -> anyhow::Error {
        anyhow!("{e}")
    }
}

impl<'a, Context> ErrorRaiser<Context, &'a str> for MyErrorRaiser
where
    Context: HasErrorType<Error = anyhow::Error>,
{
    fn raise_error(e: &'a str) -> anyhow::Error {
        anyhow!("{e}")
    }
}
}

In this example, we define the provider MyErrorRaiser with explicit ErrorRaiser implementations for a set of source error types, assuming that the abstract Context::Error is anyhow::Error.

With explicit implementations, MyErrorRaiser handles different source errors in various ways. When raising a source error of type anyhow::Error, we simply return e because Context::Error is also anyhow::Error. For Infallible, we handle the error by matching the empty case. For std::io::Error and ParseIntError, we rely on the From instance, as they satisfy the constraint core::error::Error + Send + Sync + 'static. When raising ErrAuthTokenHasExpired, we use the anyhow! macro to format the error with the Debug instance. For String and &'a str, we use anyhow! to format the error with the Display instance.

While defining explicit ErrorRaiser implementations provides a high degree of flexibility, it also requires a significant amount of repetitive boilerplate. Since we’ve already defined various generic error raisers, it would be beneficial to find a way to delegate error handling to different error raisers based on the source error type.

UseDelegate Pattern

When examining the patterns for implementing custom error raisers, we notice similarities to the provider delegation pattern we covered in an earlier chapter. In fact, with a bit of indirection, we can reuse DelegateComponent to delegate the handling of source errors:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use core::marker::PhantomData;

use cgp::core::error::ErrorRaiser;
use cgp::prelude::*;

pub struct UseDelegate<Components>(pub PhantomData<Components>);

impl<Context, SourceError, Components> ErrorRaiser<Context, SourceError> for UseDelegate<Components>
where
    Context: HasErrorType,
    Components: DelegateComponent<SourceError>,
    Components::Delegate: ErrorRaiser<Context, SourceError>,
{
    fn raise_error(e: SourceError) -> Context::Error {
        Components::Delegate::raise_error(e)
    }
}
}

Let's walk through the code step by step. First, we define the UseDelegate struct with a phantom Components parameter. UseDelegate serves as a marker type for implementing the trait-specific component delegation pattern. Here, we implement ErrorRaiser for UseDelegate, allowing it to act as a context-generic provider for ErrorRaiser under specific conditions.

Within the implementation, we specify that for any context Context, source error SourceError, and error raiser provider Components, UseDelegate<Components> implements ErrorRaiser<Context, SourceError> if Components implements DelegateComponent<SourceError>. Additionally, the delegate Components::Delegate must also implement ErrorRaiser<Context, SourceError>. Inside the raise_error method, we delegate the implementation to Components::Delegate::raise_error.

In simpler terms, UseDelegate<Components> implements ErrorRaiser<Context, SourceError> if there is a delegated provider ErrorRaiser<Context, SourceError> from Components via SourceError.

We can better understand this by looking at a concrete example. Using UseDelegate, we can declaratively dispatch errors as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use cgp::core::component::UseDelegate;
use cgp::core::error::ErrorRaiser;
use cgp::prelude::*;

use core::fmt::Debug;
use core::num::ParseIntError;

use anyhow::anyhow;

#[derive(Debug)]
pub struct ErrAuthTokenHasExpired;

pub struct DebugAnyhowError;

impl<Context, E> ErrorRaiser<Context, E> for DebugAnyhowError
where
    Context: HasErrorType<Error = anyhow::Error>,
    E: Debug,
{
    fn raise_error(e: E) -> anyhow::Error {
        anyhow!("{e:?}")
    }
}

pub struct RaiseFrom;

impl<Context, E> ErrorRaiser<Context, E> for RaiseFrom
where
    Context: HasErrorType,
    Context::Error: From<E>,
{
    fn raise_error(e: E) -> Context::Error {
        e.into()
    }
}

pub struct MyErrorRaiserComponents;

delegate_components! {
    MyErrorRaiserComponents {
        [
            std::io::Error,
            ParseIntError,
        ]:
            RaiseFrom,
        [
            ErrAuthTokenHasExpired,
        ]:
            DebugAnyhowError,
    }
}

pub type MyErrorRaiser = UseDelegate<MyErrorRaiserComponents>;
}

In this example, we first define MyErrorRaiserComponents and use delegate_components! to map source error types to the error raiser providers we wish to use. Then, we redefine MyErrorRaiser to be UseDelegate<MyErrorRaiserComponents>. This allows us to implement ErrorRaiser for source errors such as std::io::Error, ParseIntError, and ErrAuthTokenHasExpired.

We can also trace the ErrorRaiser implementation for UseDelegate and see how errors like std::io::Error are handled. First, UseDelegate implements ErrorRaiser because MyErrorRaiserComponents implements DelegateComponent<std::io::Error>. From there, we observe that the delegate is RaiseFrom, and for the case where Context::Error is anyhow::Error, a From instance exists for converting std::io::Error into anyhow::Error. Thus, the chain of dependencies is satisfied, and ErrorRaiser is implemented successfully.

As seen above, the DelegateComponent and delegate_components! constructs are not only useful for wiring up CGP providers but can also be used to dispatch providers based on the generic parameters of specific traits. In fact, we will see the same pattern applied in other contexts throughout CGP.

For this reason, the UseDelegate type is included in the cgp crate, along with the ErrorRaiser implementation, so that readers can easily identify when delegation is being used every time they encounter a trait implemented for UseDelegate.

Forwarding Error Raiser

In addition to the delegation pattern, it can be useful to implement generic error raisers that perform a transformation on the source error and then forward the handling to another error raiser. For instance, when implementing a generic error raiser that formats the source error using Debug, we could first format it as a string and then forward the handling as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::core::error::{CanRaiseError, ErrorRaiser};
use core::fmt::Debug;

pub struct DebugError;

impl<Context, SourceError> ErrorRaiser<Context, SourceError> for DebugError
where
    Context: CanRaiseError<String>,
    SourceError: Debug,
{
    fn raise_error(e: SourceError) -> Context::Error {
        Context::raise_error(format!("{e:?}"))
    }
}
}

In the example above, we define a generic error raiser DebugError that implements ErrorRaiser for any SourceError that implements Debug. Additionally, we require that Context also implements CanRaiseError<String>. Inside the implementation of raise_error, we format the source error as a string and then invoke Context::raise_error with the formatted string.

A forwarding error raiser like DebugError is designed to be used with UseDelegate, ensuring that the ErrorRaiser implementation for String is handled by a separate error raiser. Without this, an incorrect wiring could result in a stack overflow if DebugError were to call itself recursively when handling the String error.

The key advantage of this approach is that it remains generic over the abstract Context::Error type. When used correctly, this allows for a large portion of error handling to remain fully context-generic, promoting flexibility and reusability.

Full Example

Now that we have learned how to use UseDelegate, we can rewrite the naive error raiser from the beginning of this chapter and use delegate_components! to simplify our error handling.

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

pub mod main {
pub mod impls {
    use core::convert::Infallible;
    use core::fmt::{Debug, Display};

    use anyhow::anyhow;
    use cgp::core::error::{CanRaiseError, ErrorRaiser, ProvideErrorType};
    use cgp::prelude::HasErrorType;

    #[derive(Debug)]
    pub struct ErrAuthTokenHasExpired;

    pub struct ReturnError;

    impl<Context, Error> ErrorRaiser<Context, Error> for ReturnError
    where
        Context: HasErrorType<Error = Error>,
    {
        fn raise_error(e: Error) -> Error {
            e
        }
    }

    pub struct RaiseFrom;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseFrom
    where
        Context: HasErrorType,
        Context::Error: From<SourceError>,
    {
        fn raise_error(e: SourceError) -> Context::Error {
            e.into()
        }
    }

    pub struct RaiseInfallible;

    impl<Context> ErrorRaiser<Context, Infallible> for RaiseInfallible
    where
        Context: HasErrorType,
    {
        fn raise_error(e: Infallible) -> Context::Error {
            match e {}
        }
    }

    pub struct DebugError;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for DebugError
    where
        Context: CanRaiseError<String>,
        SourceError: Debug,
    {
        fn raise_error(e: SourceError) -> Context::Error {
            Context::raise_error(format!("{e:?}"))
        }
    }

    pub struct UseAnyhow;

    impl<Context> ProvideErrorType<Context> for UseAnyhow {
        type Error = anyhow::Error;
    }

    pub struct DisplayAnyhowError;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for DisplayAnyhowError
    where
        Context: HasErrorType<Error = anyhow::Error>,
        SourceError: Display,
    {
        fn raise_error(e: SourceError) -> anyhow::Error {
            anyhow!("{e}")
        }
    }
}

pub mod contexts {
    use core::convert::Infallible;
    use core::num::ParseIntError;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent};
    use cgp::prelude::*;

    use super::impls::*;

    pub struct MyApp;

    pub struct MyAppComponents;

    pub struct MyErrorRaiserComponents;

    impl HasComponents for MyApp {
        type Components = MyAppComponents;
    }

    delegate_components! {
        MyAppComponents {
            ErrorTypeComponent: UseAnyhow,
            ErrorRaiserComponent: UseDelegate<MyErrorRaiserComponents>,
        }
    }

    delegate_components! {
        MyErrorRaiserComponents {
            anyhow::Error: ReturnError,
            Infallible: RaiseInfallible,
            [
                std::io::Error,
                ParseIntError,
            ]:
                RaiseFrom,
            [
                ErrAuthTokenHasExpired,
            ]:
                DebugError,
            [
                String,
                <'a> &'a str,
            ]:
                DisplayAnyhowError,
        }
    }

    pub trait CanRaiseMyAppErrors:
        CanRaiseError<anyhow::Error>
        + CanRaiseError<Infallible>
        + CanRaiseError<std::io::Error>
        + CanRaiseError<ParseIntError>
        + CanRaiseError<ErrAuthTokenHasExpired>
        + CanRaiseError<String>
        + for<'a> CanRaiseError<&'a str>
    {
    }

    impl CanRaiseMyAppErrors for MyApp {}
}
}
}

In the first part of the example, we define various context-generic error raisers that are useful not only for our specific application but can also be reused later for other applications. We have ReturnError, which simply returns the source error as-is, RaiseFrom for converting the source error using From, RaiseInfallible for handling Infallible errors, and DebugError for formatting and re-raising the error as a string. We also define UseAnyhow to implement ProvideErrorType, and DisplayAnyhowError to convert any SourceError implementing Display into anyhow::Error.

In the second part of the example, we define a dummy context, MyApp, to illustrate how it can handle various source errors. We define MyErrorRaiserComponents and use delegate_components! to map various source error types to the corresponding error raiser providers. We then use UseDelegate<MyErrorRaiserComponents> as the provider for ErrorRaiserComponent. Finally, we define the trait CanRaiseMyAppErrors to verify that all the error raisers are wired correctly.

Wiring Checks

As seen in the example, the use of UseDelegate with ErrorRaiser acts as a form of top-level error handler for an application. The main difference is that the "handling" of errors is done entirely at compile-time, enabling us to customize how each source error is handled without incurring any runtime performance overhead.

However, it's important to note that the wiring for delegated error raisers is done lazily, similar to how CGP provider wiring works. This means that an error could be wired incorrectly, with constraints that are not satisfied, and the issue will only manifest as a compile-time error when the error raiser is used in another provider.

Misconfigured wiring of error raisers can often lead to common CGP errors, especially for beginners. We encourage readers to refer back to the chapter on debugging techniques and utilize check traits to ensure all source errors are wired correctly. It's also helpful to use a forked Rust compiler to display unsatisfied constraints arising from incomplete error raiser implementations.

Conclusion

In this chapter, we explored the UseDelegate pattern and how it allows us to declaratively handle error raisers in various ways. This pattern simplifies error handling and can be extended to other problem domains within CGP, as we'll see in future chapters. Additionally, the UseDelegate pattern serves as a foundation for more advanced error handling techniques, which will be covered in the next chapter.

Error Reporting

In the previous chapter on error handling, we implemented AuthTokenValidator to raise the error string "auth token has expired", when a given auth token has expired. Even after we defined a custom error type ErrAuthTokenHasExpired, it is still a dummy struct that has a Debug implementation that outputs the same string "auth token has expired". In real world applications, we know that it is good engineering practice to include as much details to an error, so that developers and end users can more easily diagnose the source of the problem. On the other hand, it takes a lot of effort to properly design and show good error messages. When doing initial development, we don't necessary want to spend too much effort on formatting error messages, when we don't even know if the code would survive the initial iteration.

To resolve the dilemma, developers are often forced to choose a comprehensive error library that can do everything from error handling to error reporting. Once the library is chosen, implementation code often becomes tightly coupled with the error library. If there is any detail missing in the error report, it may be challenging to include more details without diving deep into the impementation.

CGP offers better ways to resolve this dilemma, by allowing us to decouple the logic of error handling from actual error reporting. In this chapter, we will go into detail of how we can use CGP to improve the error report to show more information about an expired auth token.

Reporting Errors with Abstract Types

One challenge that CGP introduces is that with abstract types, it may be challenging to produce good error report without knowledge about the underlying type. We can workaround this in a naive way by using impl-side dependencies to require the abstract types Context::AuthToken and Context::Time to implement Debug, and then format them as a string before raising it as an error:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::fmt::Debug;

use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType {
    fn fetch_auth_token_expiry(
        &self,
        auth_token: &Self::AuthToken,
    ) -> Result<Self::Time, Self::Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType + HasErrorType {
    fn current_time(&self) -> Result<Self::Time, Self::Error>;
}

pub struct ValidateTokenIsNotExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime + CanFetchAuthTokenExpiry + for<'a> CanRaiseError<String>,
    Context::Time: Debug + Ord,
    Context::AuthToken: Debug,
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Context::Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err(Context::raise_error(
                format!(
                    "the auth token {:?} has expired at {:?}, which is earlier than the current time {:?}",
                    auth_token, token_expiry, now,
                )))
        }
    }
}
}

The example above now shows better error message. But our provider ValidateTokenIsNotExpired is now tightly coupled with how the token expiry error is reported. We are now forced to implement Debug for any AuthToken and Time types that we want to use. It is also not possible to customize the error report to instead use the Display instance, without directly modifying the implementation for ValidateTokenIsNotExpired. Similarly, we cannot easily customize how the message content is formatted, or add additional details to the report.

Source Error Types with Abstract Fields

To better report the error message, we would first re-introduce the ErrAuthTokenHasExpired source error type that we have used in earlier examples. But now, we would also add fields with abstract types into the struct, so that it contains all values that may be essential for generating a good error report:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::fmt::Debug;

use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct ErrAuthTokenHasExpired<'a, Context>
where
    Context: HasAuthTokenType + HasTimeType,
{
    pub context: &'a Context,
    pub auth_token: &'a Context::AuthToken,
    pub current_time: &'a Context::Time,
    pub expiry_time: &'a Context::Time,
}
}

The ErrAuthTokenHasExpired struct is now parameterized by a generic lifetime 'a and a generic context Context. Inside the struct, all fields are in the form of reference &'a, so that we don't perform any copy to construct the error value. The struct has a where clause to require Context to implement HasAuthTokenType and HasTimeType, since we need to hold their values inside the struct. In addition to auth_token, current_time, and expiry_time, we also include a context field with a reference to the main context, so that additional error details may be provided through Context.

In addition to the struct, we also manually implement a Debug instance as a default way to format ErrAuthTokenHasExpired as string:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::fmt::Debug;

use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct ErrAuthTokenHasExpired<'a, Context>
where
    Context: HasAuthTokenType + HasTimeType,
{
    pub context: &'a Context,
    pub auth_token: &'a Context::AuthToken,
    pub current_time: &'a Context::Time,
    pub expiry_time: &'a Context::Time,
}

impl<'a, Context> Debug for ErrAuthTokenHasExpired<'a, Context>
where
    Context: HasAuthTokenType + HasTimeType,
    Context::AuthToken: Debug,
    Context::Time: Debug,
{
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "the auth token {:?} has expired at {:?}, which is earlier than the current time {:?}",
            self.auth_token, self.expiry_time, self.current_time,
        )
    }
}
}

Inside the Debug instance for ErrAuthTokenHasExpired, we make use of impl-side dependencies to require Context::AuthToken and Context::Time to implement Debug. We then use Debug to format the values and show the error message.

Notice that even though ErrAuthTokenHasExpired contains a context field, it is not used in the Debug implementation. Also, since the Debug constraint for Context::AuthToken and Context::Time are only present in the Debug implementation, it is possible for the concrete types to not implement Debug, if the application do not use Debug with ErrAuthTokenHasExpired.

This design is intentional, as we only provide the Debug implementation as a convenience for quickly formatting the error message without further customization. On the other hand, a better error reporting strategy may be present elsewhere and provided by the application. The main purpose of this design is so that at the time ErrAuthTokenHasExpired and ValidateTokenIsNotExpired are defined, we don't need to concern about where and how this error reporting strategy is implemented.

Using the new ErrAuthTokenHasExpired, we can now re-implement ValidateTokenIsNotExpired as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::fmt::Debug;

use cgp::prelude::*;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

#[cgp_component {
    provider: AuthTokenValidator,
}]
pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType {
    fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>;
}

#[cgp_component {
    provider: AuthTokenExpiryFetcher,
}]
pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType {
    fn fetch_auth_token_expiry(
        &self,
        auth_token: &Self::AuthToken,
    ) -> Result<Self::Time, Self::Error>;
}

#[cgp_component {
    provider: CurrentTimeGetter,
}]
pub trait HasCurrentTime: HasTimeType + HasErrorType {
    fn current_time(&self) -> Result<Self::Time, Self::Error>;
}

pub struct ErrAuthTokenHasExpired<'a, Context>
where
    Context: HasAuthTokenType + HasTimeType,
{
    pub context: &'a Context,
    pub auth_token: &'a Context::AuthToken,
    pub current_time: &'a Context::Time,
    pub expiry_time: &'a Context::Time,
}

impl<'a, Context> Debug for ErrAuthTokenHasExpired<'a, Context>
where
    Context: HasAuthTokenType + HasTimeType,
    Context::AuthToken: Debug,
    Context::Time: Debug,
{
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "the auth token {:?} has expired at {:?}, which is earlier than the current time {:?}",
            self.auth_token, self.expiry_time, self.current_time,
        )
    }
}

pub struct ValidateTokenIsNotExpired;

impl<Context> AuthTokenValidator<Context> for ValidateTokenIsNotExpired
where
    Context: HasCurrentTime
        + CanFetchAuthTokenExpiry
        + for<'a> CanRaiseError<ErrAuthTokenHasExpired<'a, Context>>,
    Context::Time: Ord,
{
    fn validate_auth_token(
        context: &Context,
        auth_token: &Context::AuthToken,
    ) -> Result<(), Context::Error> {
        let now = context.current_time()?;

        let token_expiry = context.fetch_auth_token_expiry(auth_token)?;

        if token_expiry < now {
            Ok(())
        } else {
            Err(Context::raise_error(ErrAuthTokenHasExpired {
                context,
                auth_token,
                current_time: &now,
                expiry_time: &token_expiry,
            }))
        }
    }
}
}

In the new implementation, we include the constraint for<'a> CanRaiseError<ErrAuthTokenHasExpired<'a, Context>> with higher ranked trait bound, so that we can raise ErrAuthTokenHasExpired parameterized with any lifetime. Notice that inside the where constraints, we no longer require the Debug bound on Context::AuthToken and Context::Time.

With this approach, we have made use of ErrAuthTokenHasExpired to fully decouple ValidateTokenIsNotExpired provider from the problem of how to report the token expiry error.

Error Report Raisers

In the previous chapter, we have learned about how to define custom error raisers and then dispatch them using the UseDelegate pattern. With that in mind, we can easily define error raisers for ErrAuthTokenHasExpired to format it in different ways.

One thing to note is that since ErrAuthTokenHasExpired contains a lifetime parameter with borrowed values, any error raiser that handles it would likely have to make use of the borrowed value to construct an owned value for Context::Error.

The simplest way to raise ErrAuthTokenHasExpired is to make use of its Debug implementation to and raise it using DebugError:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::core::error::{CanRaiseError, ErrorRaiser};
use core::fmt::Debug;

pub struct DebugError;

impl<Context, SourceError> ErrorRaiser<Context, SourceError> for DebugError
where
    Context: CanRaiseError<String>,
    SourceError: Debug,
{
    fn raise_error(e: SourceError) -> Context::Error {
        Context::raise_error(format!("{e:?}"))
    }
}
}

As we discussed in the previous chapter, DebugError would implement ErrorRaiser if ErrAuthTokenHasExpired implements Debug. But recall that the Debug implementation for ErrAuthTokenHasExpired requires both Context::AuthToken and Context::Time to implement Debug. So in a way, the use of impl-side dependencies here is deeply nested, but nevertheless still works thanks to Rust's trait system.

Now supposed that instead of using Debug, we want to use the Display instance of Context::AuthToken and Context::Time to format the error. Even if we are in a crate that do not own ErrAuthTokenHasExpired, we can still implement a custom ErrorRaiser instance as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::fmt::Display;

use cgp::prelude::*;
use cgp::core::error::ErrorRaiser;

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct ErrAuthTokenHasExpired<'a, Context>
where
    Context: HasAuthTokenType + HasTimeType,
{
    pub context: &'a Context,
    pub auth_token: &'a Context::AuthToken,
    pub current_time: &'a Context::Time,
    pub expiry_time: &'a Context::Time,
}

pub struct DisplayAuthTokenExpiredError;

impl<'a, Context> ErrorRaiser<Context, ErrAuthTokenHasExpired<'a, Context>>
    for DisplayAuthTokenExpiredError
where
    Context: HasAuthTokenType + HasTimeType + CanRaiseError<String>,
    Context::AuthToken: Display,
    Context::Time: Display,
{
    fn raise_error(e: ErrAuthTokenHasExpired<'a, Context>) -> Context::Error {
        Context::raise_error(format!(
            "the auth token {} has expired at {}, which is earlier than the current time {}",
            e.auth_token, e.expiry_time, e.current_time,
        ))
    }
}
}

With this approach, we can now use DisplayAuthTokenExpiredError if Context::AuthToken and Context::Time implement Display. But even if they don't, we are still free to choose alternative strategies for our application.

One possible way to improve the error message is to obfuscate the auth token, so that the reader of the error message cannot know about the actual auth token. This may have already been done, if the concrete AuthToken type implements a custom Display that does so. But in case if it does not, we can still do something similar using a customized error raiser:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate sha1;

use core::fmt::Display;

use cgp::prelude::*;
use cgp::core::error::ErrorRaiser;
use sha1::{Digest, Sha1};

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct ErrAuthTokenHasExpired<'a, Context>
where
    Context: HasAuthTokenType + HasTimeType,
{
    pub context: &'a Context,
    pub auth_token: &'a Context::AuthToken,
    pub current_time: &'a Context::Time,
    pub expiry_time: &'a Context::Time,
}

pub struct ShowAuthTokenExpiredError;

impl<'a, Context> ErrorRaiser<Context, ErrAuthTokenHasExpired<'a, Context>>
    for ShowAuthTokenExpiredError
where
    Context: HasAuthTokenType + HasTimeType + CanRaiseError<String>,
    Context::AuthToken: Display,
    Context::Time: Display,
{
    fn raise_error(e: ErrAuthTokenHasExpired<'a, Context>) -> Context::Error {
        let auth_token_hash = Sha1::new_with_prefix(e.auth_token.to_string()).finalize();

        Context::raise_error(format!(
            "the auth token {:x} has expired at {}, which is earlier than the current time {}",
            auth_token_hash, e.expiry_time, e.current_time,
        ))
    }
}
}

By decoupling the error reporting from the provider, we can now customize the error reporting as we see fit, without needing to access or modify the original provider ValidateTokenIsNotExpired.

Context-Specific Error Details

Previously, we included the context field in ErrAuthTokenHasExpired but never used it in the error reporting. But with the ability to define custom error raisers, we can also define one that extracts additional details from the context, so that it can be included in the error message.

Supposed that we are using CanValidateAuthToken in an application that serves sensitive documents. When an expired auth token is used, we may want to also include the document ID being accessed, so that we can identify the attack patterns of any potential attacker. If the application context holds the document ID, we can now access it within the error raiser as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate sha1;

use core::fmt::Display;

use cgp::prelude::*;
use cgp::core::error::ErrorRaiser;
use sha1::{Digest, Sha1};

#[cgp_component {
    name: TimeTypeComponent,
    provider: ProvideTimeType,
}]
pub trait HasTimeType {
    type Time;
}

#[cgp_component {
    name: AuthTokenTypeComponent,
    provider: ProvideAuthTokenType,
}]
pub trait HasAuthTokenType {
    type AuthToken;
}

pub struct ErrAuthTokenHasExpired<'a, Context>
where
    Context: HasAuthTokenType + HasTimeType,
{
    pub context: &'a Context,
    pub auth_token: &'a Context::AuthToken,
    pub current_time: &'a Context::Time,
    pub expiry_time: &'a Context::Time,
}

#[cgp_component {
    provider: DocumentIdGetter,
}]
pub trait HasDocumentId {
    fn document_id(&self) -> u64;
}

pub struct ShowAuthTokenExpiredError;

impl<'a, Context> ErrorRaiser<Context, ErrAuthTokenHasExpired<'a, Context>>
    for ShowAuthTokenExpiredError
where
    Context: HasAuthTokenType + HasTimeType + CanRaiseError<String> + HasDocumentId,
    Context::AuthToken: Display,
    Context::Time: Display,
{
    fn raise_error(e: ErrAuthTokenHasExpired<'a, Context>) -> Context::Error {
        let document_id = e.context.document_id();
        let auth_token_hash = Sha1::new_with_prefix(e.auth_token.to_string()).finalize();

        Context::raise_error(format!(
            "failed to access highly sensitive document {} at time {}, using the auth token {:x} which was expired at {}",
            document_id, e.current_time, auth_token_hash, e.expiry_time,
        ))
    }
}
}

With this, even though the provider ValidateTokenIsNotExpired did not know that Context contains a document ID, by including the context value in ErrAuthTokenHasExpired, we can still implement a custom error raiser that produce a custom error message that includes the document ID.

Conclusion

In this chapter, we have learned about some advanced CGP techniques that can be used to decouple providers from the burden of producing good error reports. With that, we are able to define custom error raisers that produce highly detailed error reports, without needing to modify the original provider implementation. The use of source error types with abstract fields and borrowed values serves as a cheap interface to decouple the producer of an error (the provider) from the handler of an error (the error raiser).

Still, even with CGP, learning all the best practices of properly raising and handling errors can be overwhelming, especially for beginners. Furthermore, even if we can decouple and customize the handling of all possible error cases, extra effort is still needed for every customization, which can still takes a lot of time.

As a result, we do not encourage readers to try and define custom error structs for all possible errors. Instead, readers should start with simple error types like strings, and slowly add more structures to common errors that occur in the application. But readers should keep in mind the techniques introduced in this chapter, so that by the time we need to customize and produce good error reports for our applications, we know about how this can be done using CGP.

Error Wrapping

When programming in Rust, there is a common need to not only raise new errors, but also attach additional details to an error that has previously been raised. This is mainly to allow a caller to attach additional details about which higher-level operations are being performed, so that better error report and diagnostics can be presented to the user.

Error libraries such as anyhow and eyre provide methods such as context and wrap_err to allow wrapping of additional details to their error type. In this chapter, we will discuss about how to implement context-generic error wrapping with CGP, and how to integrate them with existing error libraries.

Example: Config Loader

Supposed that we want to build an application with the functionality to load and parse some application configuration from a config path. Using the CGP patterns that we have learned so far, we may implement a context-generic config loader as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate serde;
extern crate serde_json;

pub mod main {
pub mod traits {
    use std::path::PathBuf;

    use cgp::prelude::*;

    #[cgp_component {
        name: ConfigTypeComponent,
        provider: ProvideConfigType,
    }]
    pub trait HasConfigType {
        type Config;
    }

    #[cgp_component {
        provider: ConfigLoader,
    }]
    pub trait CanLoadConfig: HasConfigType + HasErrorType {
        fn load_config(&self) -> Result<Self::Config, Self::Error>;
    }

    #[cgp_component {
        provider: ConfigPathGetter,
    }]
    pub trait HasConfigPath {
        fn config_path(&self) -> &PathBuf;
    }
}

pub mod impls {
    use std::{fs, io};

    use cgp::core::error::{ErrorRaiser, ProvideErrorType};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::traits::*;

    pub struct LoadJsonConfig;

    impl<Context> ConfigLoader<Context> for LoadJsonConfig
    where
        Context: HasConfigType
            + HasConfigPath
            + CanRaiseError<io::Error>
            + CanRaiseError<serde_json::Error>,
        Context::Config: for<'a> Deserialize<'a>,
    {
        fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
            let config_path = context.config_path();

            let config_bytes = fs::read(config_path).map_err(Context::raise_error)?;

            let config = serde_json::from_slice(&config_bytes).map_err(Context::raise_error)?;

            Ok(config)
        }
    }
}
}
}

We first define the HasConfigType trait, which provides an abstract Config type to represent the application's config. We then define a CanLoadConfig trait, which provides an interface for loading the application config. To help with the implementation, we also implement a HasConfigPath trait, which allows a provider to get the file path to the config file from the context.

Using the config traits, we then implement LoadJsonConfig as a context-generic provider for ConfigLoader, which would read a JSON config file as bytes from the filesystem using std::fs::read, and then parse the config using serde_json. With CGP, LoadJsonConfig can work with any Config type that implements Deserialize.

We can then define an example application context that makes use of LoadJsonConfig to load its config as follows:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate cgp;
extern crate serde;
extern crate serde_json;

pub mod main {
pub mod traits {
    use std::path::PathBuf;

    use cgp::prelude::*;

    #[cgp_component {
        name: ConfigTypeComponent,
        provider: ProvideConfigType,
    }]
    pub trait HasConfigType {
        type Config;
    }

    #[cgp_component {
        provider: ConfigLoader,
    }]
    pub trait CanLoadConfig: HasConfigType + HasErrorType {
        fn load_config(&self) -> Result<Self::Config, Self::Error>;
    }

    #[cgp_component {
        provider: ConfigPathGetter,
    }]
    pub trait HasConfigPath {
        fn config_path(&self) -> &PathBuf;
    }
}

pub mod impls {
    use std::{fs, io};

    use cgp::core::error::{ErrorRaiser, ProvideErrorType};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::traits::*;

    pub struct LoadJsonConfig;

    impl<Context> ConfigLoader<Context> for LoadJsonConfig
    where
        Context: HasConfigType
            + HasConfigPath
            + CanRaiseError<io::Error>
            + CanRaiseError<serde_json::Error>,
        Context::Config: for<'a> Deserialize<'a>,
    {
        fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
            let config_path = context.config_path();

            let config_bytes = fs::read(config_path).map_err(Context::raise_error)?;

            let config = serde_json::from_slice(&config_bytes).map_err(Context::raise_error)?;

            Ok(config)
        }
    }

    pub struct UseAnyhowError;

    impl<Context> ProvideErrorType<Context> for UseAnyhowError {
        type Error = anyhow::Error;
    }

    pub struct RaiseFrom;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseFrom
    where
        Context: HasErrorType,
        Context::Error: From<SourceError>,
    {
        fn raise_error(e: SourceError) -> Context::Error {
            e.into()
        }
    }
}

pub mod contexts {
    use std::io;
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::impls::*;
    use super::traits::*;

    pub struct App {
        pub config_path: PathBuf,
    }

    #[derive(Deserialize)]
    pub struct AppConfig {
        pub api_secret: String,
    }

    pub struct AppComponents;

    pub struct RaiseAppErrors;

    impl HasComponents for App {
        type Components = AppComponents;
    }

    delegate_components! {
        AppComponents {
            ErrorTypeComponent: UseAnyhowError,
            ErrorRaiserComponent: UseDelegate<RaiseAppErrors>,
            ConfigLoaderComponent: LoadJsonConfig,
        }
    }

    delegate_components! {
        RaiseAppErrors {
            [
                io::Error,
                serde_json::Error,
            ]:
                RaiseFrom,
        }
    }

    impl ProvideConfigType<App> for AppComponents {
        type Config = AppConfig;
    }

    impl ConfigPathGetter<App> for AppComponents {
        fn config_path(app: &App) -> &PathBuf {
            &app.config_path
        }
    }

    pub trait CanUseApp: CanLoadConfig {}

    impl CanUseApp for App {}
}
}
}

The App context has a config_path field to store the path to the JSON config. We also define an example AppConfig type, which implements Deserialize and has an api_secret string field that can be used by further implementation.

Inside the component wiring for AppComponents, we make use of UseAnyhowError that we have defined in earlier chapter to provide the anyhow::Error type, and we use UseDelegate<RaiseAppErrors> to implement the error raiser. Inside of RaiseAppErrors, we make use of RaiseFrom to convert std::io::Error and serde_json::Error to anyhow::Error using the From instance.

We also provide context-specific implementations of ProvideConfigType and ConfigPathGetter for the App context. Following that, we define a check trait CanUseApp to check that the wiring is done correctly and that App implements CanLoadConfig.

Even though the example implementation for LoadJsonConfig works, we would quickly find out that the error message returned from it is not very helpful. For example, if the file does not exist, we would get the following error message:

No such file or directory (os error 2)

Similarly, if the config file is not in JSON format, we would get an error message like the following:

expected value at line 1 column 2

Error messages like above make it very difficult for users to figure out what went wrong, and what action needs to be taken to resolve them. To improve the error messages, we need to wrap around source errors like std::io::Error, and provide additional details so that the user knows that the error occured when trying to load the app config. Next, we will learn about how to wrap around these errors in CGP.

Error Wrapper

With the same motivation described in the previous chapter, we would like to make use of CGP to also enable modular error reporting for the error details that is being wrapped. This would mean that we want to define a generic Detail type that can include structured data inside the error details. We can do that by introduce an error wrapper trait as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[cgp_component {
    provider: ErrorWrapper,
}]
pub trait CanWrapError<Detail>: HasErrorType {
    fn wrap_error(error: Self::Error, detail: Detail) -> Self::Error;
}
}

The CanWrapError trait is parameterized by a generic Detail type, and has HasErrorType as its supertrait. Inside the wrap_error method, it first accepts a context error Self::Error and also a Detail value. It then wraps the detail inside the context error, and return Self::Error.

To see how CanWrapError works in practice, we can redefine LoadJsonConfig to use CanWrapError as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate serde;
extern crate serde_json;

use std::path::PathBuf;
use core::fmt::Display;
use std::{fs, io};

use cgp::prelude::*;
use serde::Deserialize;

#[cgp_component {
    name: ConfigTypeComponent,
    provider: ProvideConfigType,
}]
pub trait HasConfigType {
    type Config;
}

#[cgp_component {
    provider: ConfigLoader,
}]
pub trait CanLoadConfig: HasConfigType + HasErrorType {
    fn load_config(&self) -> Result<Self::Config, Self::Error>;
}

#[cgp_component {
    provider: ConfigPathGetter,
}]
pub trait HasConfigPath {
    fn config_path(&self) -> &PathBuf;
}

pub struct LoadJsonConfig;

impl<Context> ConfigLoader<Context> for LoadJsonConfig
where
    Context: HasConfigType
        + HasConfigPath
        + CanWrapError<String>
        + CanRaiseError<io::Error>
        + CanRaiseError<serde_json::Error>,
    Context::Config: for<'a> Deserialize<'a>,
{
    fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
        let config_path = context.config_path();

        let config_bytes = fs::read(config_path).map_err(|e| {
            Context::wrap_error(
                Context::raise_error(e),
                format!(
                    "error when reading config file at path {}",
                    config_path.display()
                ),
            )
        })?;

        let config = serde_json::from_slice(&config_bytes).map_err(|e| {
            Context::wrap_error(
                Context::raise_error(e),
                format!(
                    "error when parsing JSON config file at path {}",
                    config_path.display()
                ),
            )
        })?;

        Ok(config)
    }
}
}

Inside the new implementation of LoadJsonConfig, we add a CanWrapError<String> constraint so that we can add stringly error details inside the provider. When mapping the errors returned from std::fs::read and serde_json::from_slice, we pass in a closure instead of directly calling Context::raise_error. Since the first argument of wrap_error expects a Context::Error, we would still first use Context::raise_error to raise std::io::Error and serde_json::Error into Context::Error. In the second argument, we use format! to add additional details that the errors occured when we are trying to read and parse the given config file.

By looking only at the example, it may seem redundant that we have to first raise a concrete source error like std::io::Error into Context::Error, before wrapping it again using Context::wrap_error. If the reader prefers, you can also use a constraint like CanRaiseError<(String, std::io::Error)> to raise the I/O error with additional string detail.

However, the interface for CanWrapError is more applicable generally, especially when we combine the use with other abstractions. For example, we may want to define a trait like CanReadFile to try reading a file, and returning a general Context::Error when the read fails. In that case, we can still use wrap_error without knowing about whether we are dealing with concrete errors or abstract errors.

Next, we would need to implement a provider for CanWrapError to handle how to wrap additional details into the error value. In the case when the context error type is anyhow::Error, we can simply call the context method. So we can implement an error wrapper provider for anyhow::Error as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use core::fmt::Display;

use cgp::prelude::*;
use cgp::core::error::ErrorWrapper;

pub struct WrapWithAnyhowContext;

impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowContext
where
    Context: HasErrorType<Error = anyhow::Error>,
    Detail: Display + Send + Sync + 'static,
{
    fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
        error.context(detail)
    }
}
}

We implement WrapWithAnyhowContext as a context-generic provider for anyhow::Error. It is implemented for any context type Context with Context::Error being the same as anyhow::Error. Additionally, it is implemented for any Detail type that implements Display + Send + Sync + 'static, as those are the required trait bounds to use anyhow::Error::context. Inside the wrap_error implementation, we simply call error.context(detail) to wrap the error detail using anyhow.

After rewiring the application with the new providers, if we run the application again with missing file, it would show the following error instead:

error when reading config file at path config.json

Caused by:
    No such file or directory (os error 2)

Similarly, when encountering error parsing the config JSON, the application now shows the error message:

error when parsing JSON config file at path config.toml

Caused by:
    expected value at line 1 column 2

As we can see, the error messages are now much more informative, allowing the user to diagnose what went wrong and fix the problem.

Structured Error Wrapping

Similar to the reasons for using structured error reporting from the previous chapter, using structured error details would make it possible to decouple how to format the wrapped error detail from the provider. For the case of LoadJsonConfig, we can define and use a structured error detail type as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate serde;
extern crate serde_json;

use std::path::PathBuf;
use core::fmt::Debug;
use std::{fs, io};

use cgp::prelude::*;
use serde::Deserialize;

#[cgp_component {
    name: ConfigTypeComponent,
    provider: ProvideConfigType,
}]
pub trait HasConfigType {
    type Config;
}

#[cgp_component {
    provider: ConfigLoader,
}]
pub trait CanLoadConfig: HasConfigType + HasErrorType {
    fn load_config(&self) -> Result<Self::Config, Self::Error>;
}

#[cgp_component {
    provider: ConfigPathGetter,
}]
pub trait HasConfigPath {
    fn config_path(&self) -> &PathBuf;
}

pub struct LoadJsonConfig;

pub struct ErrLoadJsonConfig<'a, Context> {
    pub context: &'a Context,
    pub config_path: &'a PathBuf,
    pub action: LoadJsonConfigAction,
}

pub enum LoadJsonConfigAction {
    ReadFile,
    ParseFile,
}

impl<Context> ConfigLoader<Context> for LoadJsonConfig
where
    Context: HasConfigType
        + HasConfigPath
        + CanRaiseError<io::Error>
        + CanRaiseError<serde_json::Error>
        + for<'a> CanWrapError<ErrLoadJsonConfig<'a, Context>>,
    Context::Config: for<'a> Deserialize<'a>,
{
    fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
        let config_path = context.config_path();

        let config_bytes = fs::read(config_path).map_err(|e| {
            Context::wrap_error(
                Context::raise_error(e),
                ErrLoadJsonConfig {
                    context,
                    config_path,
                    action: LoadJsonConfigAction::ReadFile,
                },
            )
        })?;

        let config = serde_json::from_slice(&config_bytes).map_err(|e| {
            Context::wrap_error(
                Context::raise_error(e),
                ErrLoadJsonConfig {
                    context,
                    config_path,
                    action: LoadJsonConfigAction::ParseFile,
                },
            )
        })?;

        Ok(config)
    }
}

impl<'a, Context> Debug for ErrLoadJsonConfig<'a, Context> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self.action {
            LoadJsonConfigAction::ReadFile => {
                write!(
                    f,
                    "error when reading config file at path {}",
                    self.config_path.display()
                )
            }
            LoadJsonConfigAction::ParseFile => {
                write!(
                    f,
                    "error when parsing JSON config file at path {}",
                    self.config_path.display()
                )
            }
        }
    }
}
}

We first define an error detail struct ErrLoadJsonConfig that is parameterized by a lifetime 'a and a context type Context. Inside the struct, we include the Context field to allow potential extra details to be included from the concrete context. We also include the config_path to show the path of the config file that cause the error. Lastly, we also include a LoadJsonConfigAction field to indicate whether the error happened when reading or parsing the config file.

We also implement a Debug instance for ErrLoadJsonConfig, so that it can be used by default when there is no need to customize the display of the error detail. The Debug implementation ignores the context field, and shows the same error messages as we did before.

To make use of the Debug implementation with anyhow, we can implement a separate provider that wraps any Detail type that implements Debug as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate anyhow;

use core::fmt::Debug;

use cgp::prelude::*;
use cgp::core::error::ErrorWrapper;

pub struct WrapWithAnyhowDebug;

impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowDebug
where
    Context: HasErrorType<Error = anyhow::Error>,
    Detail: Debug,
{
    fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
        error.context(format!("{detail:?}"))
    }
}
}

To wrap the error, we first use Debug to format the error detail into string, and then call error.context with the string.

Full Example

With everything that we have learned so far, we can rewrite the config loader example in the beginning of this chapter, and make use of CanWrapError to decouple the error wrapping details from the provider LoadJsonConfig:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate cgp;
extern crate serde;
extern crate serde_json;

pub mod main {
pub mod traits {
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::prelude::*;

    #[cgp_component {
        name: ConfigTypeComponent,
        provider: ProvideConfigType,
    }]
    pub trait HasConfigType {
        type Config;
    }

    #[cgp_component {
        provider: ConfigLoader,
    }]
    pub trait CanLoadConfig: HasConfigType + HasErrorType {
        fn load_config(&self) -> Result<Self::Config, Self::Error>;
    }

    #[cgp_component {
        provider: ConfigPathGetter,
    }]
    pub trait HasConfigPath {
        fn config_path(&self) -> &PathBuf;
    }
}

pub mod impls {
    use core::fmt::{Debug, Display};
    use std::path::PathBuf;
    use std::{fs, io};

    use cgp::core::error::{ErrorRaiser, ErrorWrapper,ProvideErrorType};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::traits::*;

    pub struct LoadJsonConfig;

    pub struct ErrLoadJsonConfig<'a, Context> {
        pub context: &'a Context,
        pub config_path: &'a PathBuf,
        pub action: LoadJsonConfigAction,
    }

    pub enum LoadJsonConfigAction {
        ReadFile,
        ParseFile,
    }

    impl<Context> ConfigLoader<Context> for LoadJsonConfig
    where
        Context: HasConfigType
            + HasConfigPath
            + CanRaiseError<io::Error>
            + CanRaiseError<serde_json::Error>
            + for<'a> CanWrapError<ErrLoadJsonConfig<'a, Context>>,
        Context::Config: for<'a> Deserialize<'a>,
    {
        fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
            let config_path = context.config_path();

            let config_bytes = fs::read(config_path).map_err(|e| {
                Context::wrap_error(
                    Context::raise_error(e),
                    ErrLoadJsonConfig {
                        context,
                        config_path,
                        action: LoadJsonConfigAction::ReadFile,
                    },
                )
            })?;

            let config = serde_json::from_slice(&config_bytes).map_err(|e| {
                Context::wrap_error(
                    Context::raise_error(e),
                    ErrLoadJsonConfig {
                        context,
                        config_path,
                        action: LoadJsonConfigAction::ParseFile,
                    },
                )
            })?;

            Ok(config)
        }
    }

    impl<'a, Context> Debug for ErrLoadJsonConfig<'a, Context> {
        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
            match self.action {
                LoadJsonConfigAction::ReadFile => {
                    write!(
                        f,
                        "error when reading config file at path {}",
                        self.config_path.display()
                    )
                }
                LoadJsonConfigAction::ParseFile => {
                    write!(
                        f,
                        "error when parsing JSON config file at path {}",
                        self.config_path.display()
                    )
                }
            }
        }
    }

    pub struct UseAnyhowError;

    impl<Context> ProvideErrorType<Context> for UseAnyhowError {
        type Error = anyhow::Error;
    }

    pub struct RaiseFrom;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseFrom
    where
        Context: HasErrorType,
        Context::Error: From<SourceError>,
    {
        fn raise_error(e: SourceError) -> Context::Error {
            e.into()
        }
    }

    pub struct WrapWithAnyhowDebug;

    impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowDebug
    where
        Context: HasErrorType<Error = anyhow::Error>,
        Detail: Debug,
    {
        fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
            error.context(format!("{detail:?}"))
        }
    }
}

pub mod contexts {
    use std::io;
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::{ErrorRaiserComponent, ErrorWrapperComponent, ErrorTypeComponent};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::impls::*;
    use super::traits::*;

    pub struct App {
        pub config_path: PathBuf,
    }

    #[derive(Deserialize)]
    pub struct AppConfig {
        pub secret: String,
    }

    pub struct AppComponents;

    pub struct RaiseAppErrors;

    impl HasComponents for App {
        type Components = AppComponents;
    }

    delegate_components! {
        AppComponents {
            ErrorTypeComponent: UseAnyhowError,
            ErrorRaiserComponent: UseDelegate<RaiseAppErrors>,
            ErrorWrapperComponent: WrapWithAnyhowDebug,
            ConfigLoaderComponent: LoadJsonConfig,
        }
    }

    delegate_components! {
        RaiseAppErrors {
            [
                io::Error,
                serde_json::Error,
            ]:
                RaiseFrom,
        }
    }

    impl ProvideConfigType<App> for AppComponents {
        type Config = AppConfig;
    }

    impl ConfigPathGetter<App> for AppComponents {
        fn config_path(app: &App) -> &PathBuf {
            &app.config_path
        }
    }

    pub trait CanUseApp: CanLoadConfig {}

    impl CanUseApp for App {}
}
}
}

Delegated Error Wrapping

Similar to the previous chapter on delegated error raisers, we can also make use of the UseDelegate pattern to implement delegated error wrapping as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;
use cgp::core::error::ErrorWrapper;

pub struct UseDelegate<Components>(pub PhantomData<Components>);

impl<Context, Detail, Components> ErrorWrapper<Context, Detail> for UseDelegate<Components>
where
    Context: HasErrorType,
    Components: DelegateComponent<Detail>,
    Components::Delegate: ErrorWrapper<Context, Detail>,
{
    fn wrap_error(error: Context::Error, detail: Detail) -> Context::Error {
        Components::Delegate::wrap_error(error, detail)
    }
}
}

With this implementation, we can dispatch the handling of different error Detail type to different error wrappers, similar to how we dispatch the error raisers based on the SourceError type:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate cgp;
extern crate serde;
extern crate serde_json;

pub mod main {
pub mod traits {
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::ErrorWrapper;
    use cgp::prelude::*;

    #[cgp_component {
        name: ConfigTypeComponent,
        provider: ProvideConfigType,
    }]
    pub trait HasConfigType {
        type Config;
    }

    #[cgp_component {
        provider: ConfigLoader,
    }]
    pub trait CanLoadConfig: HasConfigType + HasErrorType {
        fn load_config(&self) -> Result<Self::Config, Self::Error>;
    }

    #[cgp_component {
        provider: ConfigPathGetter,
    }]
    pub trait HasConfigPath {
        fn config_path(&self) -> &PathBuf;
    }
}

pub mod impls {
    use core::fmt::{Debug, Display};
    use std::path::PathBuf;
    use std::{fs, io};

    use cgp::core::error::{ErrorRaiser, ErrorWrapper, ProvideErrorType};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::traits::*;

    pub struct LoadJsonConfig;

    pub struct ErrLoadJsonConfig<'a, Context> {
        pub context: &'a Context,
        pub config_path: &'a PathBuf,
        pub action: LoadJsonConfigAction,
    }

    pub enum LoadJsonConfigAction {
        ReadFile,
        ParseFile,
    }

    impl<Context> ConfigLoader<Context> for LoadJsonConfig
    where
        Context: HasConfigType
            + HasConfigPath
            + CanRaiseError<io::Error>
            + CanRaiseError<serde_json::Error>
            + for<'a> CanWrapError<ErrLoadJsonConfig<'a, Context>>,
        Context::Config: for<'a> Deserialize<'a>,
    {
        fn load_config(context: &Context) -> Result<Context::Config, Context::Error> {
            let config_path = context.config_path();

            let config_bytes = fs::read(config_path).map_err(|e| {
                Context::wrap_error(
                    Context::raise_error(e),
                    ErrLoadJsonConfig {
                        context,
                        config_path,
                        action: LoadJsonConfigAction::ReadFile,
                    },
                )
            })?;

            let config = serde_json::from_slice(&config_bytes).map_err(|e| {
                Context::wrap_error(
                    Context::raise_error(e),
                    ErrLoadJsonConfig {
                        context,
                        config_path,
                        action: LoadJsonConfigAction::ParseFile,
                    },
                )
            })?;

            Ok(config)
        }
    }

    impl<'a, Context> Debug for ErrLoadJsonConfig<'a, Context> {
        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
            match self.action {
                LoadJsonConfigAction::ReadFile => {
                    write!(
                        f,
                        "error when reading config file at path {}",
                        self.config_path.display()
                    )
                }
                LoadJsonConfigAction::ParseFile => {
                    write!(
                        f,
                        "error when parsing JSON config file at path {}",
                        self.config_path.display()
                    )
                }
            }
        }
    }

    pub struct UseAnyhowError;

    impl<Context> ProvideErrorType<Context> for UseAnyhowError {
        type Error = anyhow::Error;
    }

    pub struct RaiseFrom;

    impl<Context, SourceError> ErrorRaiser<Context, SourceError> for RaiseFrom
    where
        Context: HasErrorType,
        Context::Error: From<SourceError>,
    {
        fn raise_error(e: SourceError) -> Context::Error {
            e.into()
        }
    }

    pub struct WrapWithAnyhowContext;

    impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowContext
    where
        Context: HasErrorType<Error = anyhow::Error>,
        Detail: Display + Send + Sync + 'static,
    {
        fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
            error.context(detail)
        }
    }

    pub struct WrapWithAnyhowDebug;

    impl<Context, Detail> ErrorWrapper<Context, Detail> for WrapWithAnyhowDebug
    where
        Context: HasErrorType<Error = anyhow::Error>,
        Detail: Debug,
    {
        fn wrap_error(error: anyhow::Error, detail: Detail) -> anyhow::Error {
            error.context(format!("{detail:?}"))
        }
    }
}

pub mod contexts {
    use std::io;
    use std::path::PathBuf;

    use cgp::core::component::UseDelegate;
    use cgp::core::error::{ErrorRaiserComponent, ErrorWrapperComponent, ErrorTypeComponent};
    use cgp::prelude::*;
    use serde::Deserialize;

    use super::impls::*;
    use super::traits::*;

pub struct App {
    pub config_path: PathBuf,
}

#[derive(Deserialize)]
pub struct AppConfig {
    pub secret: String,
}

pub struct AppComponents;

pub struct RaiseAppErrors;

pub struct WrapAppErrors;

impl HasComponents for App {
    type Components = AppComponents;
}

delegate_components! {
    AppComponents {
        ErrorTypeComponent: UseAnyhowError,
        ErrorRaiserComponent: UseDelegate<RaiseAppErrors>,
        ErrorWrapperComponent: UseDelegate<WrapAppErrors>,
        ConfigLoaderComponent: LoadJsonConfig,
    }
}

delegate_components! {
    RaiseAppErrors {
        [
            io::Error,
            serde_json::Error,
        ]:
            RaiseFrom,
    }
}

delegate_components! {
    WrapAppErrors {
        String: WrapWithAnyhowContext,
        <'a, Context> ErrLoadJsonConfig<'a, Context>:
            WrapWithAnyhowDebug,
        // add other error wrappers here
    }
}

    impl ProvideConfigType<App> for AppComponents {
        type Config = AppConfig;
    }

    impl ConfigPathGetter<App> for AppComponents {
        fn config_path(app: &App) -> &PathBuf {
            &app.config_path
        }
    }

    pub trait CanUseApp: CanLoadConfig {}

    impl CanUseApp for App {}
}
}
}

The above example shows the addition of a new WrapAppErrors type, which we use with delegate_components! to map the handling of String detail to WrapWithAnyhowContext, and ErrLoadJsonConfig detail to WrapWithAnyhowDebug. Following the same pattern, we will be able to customize how exactly each error detail is wrapped, by updating the mapping for WrapAppErrors.

Conclusion

In this chapter, we learned about how to perform abstract error wrapping to wrap additional details to an abstract error. The pattern for using CanWrapError is very similar to the patterns that we have previously learned for CanRaiseError. So this is mostly a recap of the same patterns, and also show readers how you can expect the same CGP pattern to be applied in many different places.

Similar to the advice from the previous chapters, it could be overwhelming for beginners to try to use the full structured error wrapping patterns introduced in this chapter. As a result, we encourage readers to start with using only String as the error detail when wrapping errors inside practice applications.

The need for structured error wrapping typically would only arise in large-scale applications, or when one wants to publish CGP-based library crates for others to build modular applications. As such, you can always revisit this chapter at a later time, and refactor your providers to make use of structured error details when you really need them.

Field Accessors

With impl-side dependencies, CGP offers a way to inject dependencies into providers without cluttering the public interfaces with extra constraints. One common use of this dependency injection is for a provider to retrieve values from the context. This pattern is often referred to as a field accessor or getter, since it involves accessing field values from the context. In this chapter, we'll explore how to define and use field accessors effectively with CGP.

Example: API Call

Suppose our application needs to make API calls to an external service to read messages by their message ID. To abstract away the details of the API call, we can define CGP traits as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( Message );
cgp_type!( MessageId );

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}
}

Following the patterns for associated types, we define the type traits HasMessageIdType and HasMessageType to abstract away the details of the message ID and message structures. Additionally, the CanQueryMessage trait accepts an abstract MessageId and returns either an abstract Message or an abstract Error, following the patterns for error handling.

With the interfaces defined, we now implement a simple API client provider that queries the message via an HTTP request.

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate reqwest;
extern crate serde;

use cgp::prelude::*;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;

cgp_type!( Message );
cgp_type!( MessageId );

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}

pub struct ReadMessageFromApi;

#[derive(Debug)]
pub struct ErrStatusCode {
    pub status_code: StatusCode,
}

#[derive(Deserialize)]
pub struct ApiMessageResponse {
    pub message: String,
}

impl<Context> MessageQuerier<Context> for ReadMessageFromApi
where
    Context: HasMessageIdType<MessageId = u64>
        + HasMessageType<Message = String>
        + CanRaiseError<reqwest::Error>
        + CanRaiseError<ErrStatusCode>,
{
    fn query_message(_context: &Context, message_id: &u64) -> Result<String, Context::Error> {
        let client = Client::new();

        let response = client
            .get(format!("http://localhost:8000/api/messages/{message_id}"))
            .send()
            .map_err(Context::raise_error)?;

        let status_code = response.status();

        if !status_code.is_success() {
            return Err(Context::raise_error(ErrStatusCode { status_code }));
        }

        let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?;

        Ok(message_response.message)
    }
}
}

For the purposes of the examples in this chapter, we will use the reqwest library to make HTTP calls. We will also use the blocking version of the API in this chapter, as asynchronous programming in CGP will be covered in later chapters.

In the example above, we implement MessageQuerier for the ReadMessageFromApi provider. For simplicity, we add the constraint that MessageId must be of type u64 and the Message type is a basic String.

We also use the context to handle errors. Specifically, we raise the reqwest::Error returned by the reqwest methods, as well as a custom ErrStatusCode error if the server responds with an error HTTP status.

Within the method, we first create a reqwest::Client, and then use it to send an HTTP GET request to the URL "http://localhost:8000/api/messages/{message_id}". If the returned HTTP status is unsuccessful, we raise the ErrStatusCode. Otherwise, we parse the response body as JSON into the ApiMessageResponse struct, which expects the response to contain a message field.

It's clear that the naive provider has some hard-coded values. For instance, the API base URL http://localhost:8000 is fixed, but it should be configurable. In the next section, we will explore how to define accessor traits to retrieve these configurable values from the context.

Getting the Base API URL

In CGP, defining an accessor trait to retrieve values from the context is straightforward. To make the base API URL configurable, we define a HasApiBaseUrl trait as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}
}

The HasApiBaseUrl trait defines a method, api_base_url, which returns a reference to a String from the context. In production applications, you might prefer to return a url::Url or even an abstract Url type instead of a String. However, for simplicity, we use a String in this example.

Next, we can include the HasApiBaseUrl trait within ReadMessageFromApi, allowing us to construct the HTTP request using the base API URL provided by the context:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate reqwest;
extern crate serde;

use cgp::prelude::*;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;

cgp_type!( Message );
cgp_type!( MessageId );

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

pub struct ReadMessageFromApi;

#[derive(Debug)]
pub struct ErrStatusCode {
    pub status_code: StatusCode,
}

#[derive(Deserialize)]
pub struct ApiMessageResponse {
    pub message: String,
}

impl<Context> MessageQuerier<Context> for ReadMessageFromApi
where
    Context: HasMessageIdType<MessageId = u64>
        + HasMessageType<Message = String>
        + HasApiBaseUrl
        + CanRaiseError<reqwest::Error>
        + CanRaiseError<ErrStatusCode>,
{
    fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> {
        let client = Client::new();

        let url = format!("{}/api/messages/{}", context.api_base_url(), message_id);

        let response = client.get(url).send().map_err(Context::raise_error)?;

        let status_code = response.status();

        if !status_code.is_success() {
            return Err(Context::raise_error(ErrStatusCode { status_code }));
        }

        let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?;

        Ok(message_response.message)
    }
}
}

Getting the Auth Token

In addition to the base API URL, many API services require authentication to protect their resources from unauthorized access. For this example, we’ll use simple bearer tokens for API access.

Just as we did with HasApiBaseUrl, we can define a HasAuthToken trait to retrieve the authentication token as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}
}

Similar to the pattern used in the earlier chapter, we first define HasAuthTokenType to keep the AuthToken type abstract. In fact, this HasAuthTokenType trait and its associated providers can be reused across different chapters or applications. This demonstrates how minimal CGP traits facilitate the reuse of interfaces in multiple contexts.

Next, we define a getter trait, HasAuthToken, which provides access to an abstract AuthToken value from the context. With this in place, we can now update ReadMessageFromApi to include the authentication token in the Authorization HTTP header:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate reqwest;
extern crate serde;

use core::fmt::Display;

use cgp::prelude::*;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;

cgp_type!( Message );
cgp_type!( MessageId );
cgp_type!( AuthToken );

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

#[cgp_component {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}

pub struct ReadMessageFromApi;

#[derive(Debug)]
pub struct ErrStatusCode {
    pub status_code: StatusCode,
}

#[derive(Deserialize)]
pub struct ApiMessageResponse {
    pub message: String,
}

impl<Context> MessageQuerier<Context> for ReadMessageFromApi
where
    Context: HasMessageIdType<MessageId = u64>
        + HasMessageType<Message = String>
        + HasApiBaseUrl
        + HasAuthToken
        + CanRaiseError<reqwest::Error>
        + CanRaiseError<ErrStatusCode>,
    Context::AuthToken: Display,
{
    fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> {
        let client = Client::new();

        let url = format!("{}/api/messages/{}", context.api_base_url(), message_id);

        let response = client
            .get(url)
            .bearer_auth(context.auth_token())
            .send()
            .map_err(Context::raise_error)?;

        let status_code = response.status();

        if !status_code.is_success() {
            return Err(Context::raise_error(ErrStatusCode { status_code }));
        }

        let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?;

        Ok(message_response.message)
    }
}
}

In this updated code, we use the bearer_auth method from the reqwest library to include the authentication token in the HTTP header. In this case, the provider only requires that Context::AuthToken implement the Display trait, allowing it to work with custom AuthToken types, not limited to String.

Accessor Method Minimalism

When creating providers like ReadMessageFromApi, which often need to use both HasApiBaseUrl and HasAuthToken, it might seem tempting to combine these two traits into a single one, containing both accessor methods:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

cgp_type!( AuthToken );

#[cgp_component {
    provider: ApiClientFieldsGetter,
}]
pub trait HasApiClientFields: HasAuthTokenType {
    fn api_base_url(&self) -> &String;

    fn auth_token(&self) -> &Self::AuthToken;
}
}

While this approach works, it introduces unnecessary coupling between the api_base_url and auth_token fields. If a provider only requires api_base_url but not auth_token, it would still need to include the unnecessary auth_token dependency. Additionally, this design prevents us from implementing separate providers that could provide the api_base_url and auth_token fields independently, each with its own logic.

This coupling also makes future changes more challenging. For example, if we switch to a different authentication method, like public key cryptography, we would need to remove the auth_token method and replace it with a new one. This change would affect all code dependent on HasApiClientFields. Instead, it's much easier to add a new getter trait and gradually transition providers to the new trait while keeping the old one intact.

As applications grow in complexity, it’s common to need many accessor methods. A trait like HasApiClientFields, with dozens of methods, could quickly become a bottleneck, making the application harder to evolve. Moreover, it's often unclear upfront which accessor methods are related, and trying to theorize about logical groupings can be a distraction.

From real-world experience using CGP, we’ve found that defining one accessor method per trait is the most effective approach for rapidly iterating on application development. This method simplifies the process of adding or removing accessor methods and reduces cognitive overload, as developers don’t need to spend time deciding or debating which method should belong to which trait. Over time, it's almost inevitable that a multi-method accessor trait will need to be broken up as some methods become irrelevant to parts of the application.

In future chapters, we’ll explore how breaking accessor methods down into individual traits can enable new design patterns that work well with single-method traits.

However, CGP doesn’t prevent developers from creating accessor traits with multiple methods and types. For those new to CGP, it might feel more comfortable to define non-minimal traits, as this has been a mainstream practice in programming for decades. So, feel free to experiment and include as many types and methods in a CGP trait as you prefer.

As an alternative to defining multiple accessor methods, you could define an inner struct containing all the common fields you’ll use across most providers:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

pub struct ApiClientFields {
    pub api_base_url: String,
    pub auth_token: String,
}

#[cgp_component {
    provider: ApiClientFieldsGetter,
}]
pub trait HasApiClientFields {
    fn api_client_fields(&self) -> &ApiClientFields;
}
}

In this example, we define an ApiClientFields struct that groups both the api_base_url and auth_token fields. The HasApiClientFields trait now only needs one getter method, returning the ApiClientFields struct.

One downside to this approach is that we can no longer use abstract types within the struct. For instance, the ApiClientFields struct stores the auth_token as a concrete String rather than as an abstract AuthToken type. As a result, this approach works best when your providers don’t rely on abstract types for their fields.

For the purposes of this book, we will continue to use minimal traits, as this encourages best practices and provides readers with a clear reference for idiomatic CGP usage.

Implementing Accessor Providers

Now that we have implemented the provider, we would look at how to implement a concrete context that uses ReadMessageFromApi and implement the accessors. We can implement an ApiClient context that makes use of all providers as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate cgp_error_anyhow;
extern crate reqwest;
extern crate serde;

use core::fmt::Display;

use cgp::core::component::UseDelegate;
use cgp::extra::error::RaiseFrom;
use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent};
use cgp::prelude::*;
use cgp_error_anyhow::{DebugAnyhowError, UseAnyhowError};
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;

cgp_type!( Message );
cgp_type!( MessageId );
cgp_type!( AuthToken );

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

#[cgp_component {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}

pub struct ReadMessageFromApi;

#[derive(Debug)]
pub struct ErrStatusCode {
    pub status_code: StatusCode,
}

#[derive(Deserialize)]
pub struct ApiMessageResponse {
    pub message: String,
}

impl<Context> MessageQuerier<Context> for ReadMessageFromApi
where
    Context: HasMessageIdType<MessageId = u64>
        + HasMessageType<Message = String>
        + HasApiBaseUrl
        + HasAuthToken
        + CanRaiseError<reqwest::Error>
        + CanRaiseError<ErrStatusCode>,
    Context::AuthToken: Display,
{
    fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> {
        let client = Client::new();

        let url = format!("{}/api/messages/{}", context.api_base_url(), message_id);

        let response = client
            .get(url)
            .bearer_auth(context.auth_token())
            .send()
            .map_err(Context::raise_error)?;

        let status_code = response.status();

        if !status_code.is_success() {
            return Err(Context::raise_error(ErrStatusCode { status_code }));
        }

        let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?;

        Ok(message_response.message)
    }
}

pub struct ApiClient {
    pub api_base_url: String,
    pub auth_token: String,
}

pub struct ApiClientComponents;

pub struct RaiseApiErrors;

impl HasComponents for ApiClient {
    type Components = ApiClientComponents;
}

delegate_components! {
    ApiClientComponents {
        ErrorTypeComponent: UseAnyhowError,
        ErrorRaiserComponent: UseDelegate<RaiseApiErrors>,
        MessageIdTypeComponent: UseType<u64>,
        MessageTypeComponent: UseType<String>,
        AuthTokenTypeComponent: UseType<String>,
        MessageQuerierComponent: ReadMessageFromApi,
    }
}

delegate_components! {
    RaiseApiErrors {
        reqwest::Error: RaiseFrom,
        ErrStatusCode: DebugAnyhowError,
    }
}

impl ApiBaseUrlGetter<ApiClient> for ApiClientComponents {
    fn api_base_url(api_client: &ApiClient) -> &String {
        &api_client.api_base_url
    }
}

impl AuthTokenGetter<ApiClient> for ApiClientComponents {
    fn auth_token(api_client: &ApiClient) -> &String {
        &api_client.auth_token
    }
}

pub trait CanUseApiClient: CanQueryMessage {}

impl CanUseApiClient for ApiClient {}
}

The ApiClient context is defined with the fields that we need to implement the accessor traits. We then have context-specific implementation of ApiBaseUrlGetter and AuthTokenGetter to work directly with ApiClient. With that, our wiring is completed, and we can check that ApiClient implements CanQueryMessage.

Context-Generic Accessor Providers

While the previous accessor implementation for ApiClient works, it requires explicit and concrete access to the ApiClient context to implement the accessors. While this approach is manageable with only a couple of accessor methods, it can quickly become cumbersome as the application grows and requires numerous accessors across multiple contexts. A more efficient approach would be to implement context-generic providers for field accessors, allowing us to reuse them across any context that contains the relevant field.

To enable the implementation of context-generic accessors, the cgp crate provides a derivable HasField trait. This trait acts as a proxy, allowing access to fields in a concrete context. The trait is defined as follows:

#![allow(unused)]
fn main() {
use core::marker::PhantomData;

pub trait HasField<Tag> {
    type Value;

    fn get_field(&self, tag: PhantomData<Tag>) -> &Self::Value;
}
}

For each field within a concrete context, we can implement a HasField instance by associating a Tag type with the field's name and an associated type Value representing the field's type. Additionally, the HasField trait includes a get_field method, which retrieves a reference to the field value from the context. The get_field method accepts an additional tag parameter, which is a PhantomData type parameter tied to the field's name Tag. This phantom parameter helps with type inference in Rust, as without it, Rust would not be able to deduce which field associated with Tag is being accessed.

We can automatically derive HasField instances for a context like ApiClient using the derive macro, as shown below:

#![allow(unused)]
fn main() {
extern crate cgp;

use cgp::prelude::*;

#[derive(HasField)]
pub struct ApiClient {
    pub api_base_url: String,
    pub auth_token: String,
}
}

The derive macro would then generate the corresponding HasField instances for ApiClient:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

pub struct ApiClient {
    pub api_base_url: String,
    pub auth_token: String,
}
impl HasField<symbol!("api_base_url")> for ApiClient {
    type Value = String;

    fn get_field(&self, _tag: PhantomData<symbol!("api_base_url")>) -> &String {
        &self.api_base_url
    }
}

impl HasField<symbol!("auth_token")> for ApiClient {
    type Value = String;

    fn get_field(&self, _tag: PhantomData<symbol!("auth_token")>) -> &String {
        &self.auth_token
    }
}
}

Symbols

In the derived HasField instances, we observe the use of symbol!("api_base_url") and symbol!("auth_token") for the Tag generic type. While a string like "api_base_url" is a value of type &str, we need to use it as a type within the Tag parameter. To achieve this, we use the symbol! macro to "lift" a string value into a unique type, which allows us to treat the string "api_base_url" as a type. Essentially, this means that if the string content is the same across two uses of symbol!, the types will be treated as equivalent.

Behind the scenes, the symbol! macro first uses the Char type to "lift" individual characters into types. The Char type is defined as follows:

#![allow(unused)]
fn main() {
pub struct Char<const CHAR: char>;
}

This makes use of Rust's const generics feature to parameterize Char with a constant CHAR of type char. The Char struct itself is empty, as we only use it for type-level manipulation.

Although we can use const generics to lift individual characters, we currently cannot use a type like String or &str within const generics. As a workaround, we construct a type-level list of characters. For example, symbol!("abc") is desugared to a type-level list of characters like:

(Char<'a'>, (Char<'b'>, (Char<'c'>, ())))

In cgp, instead of using Rust’s native tuple, we define the Cons and Nil types to represent type-level lists:

#![allow(unused)]
fn main() {
pub struct Nil;

pub struct Cons<Head, Tail>(pub Head, pub Tail);
}

The Nil type represents an empty type-level list, while Cons is used to prepend an element to the front of the list, similar to how linked lists work in Lisp.

Thus, the actual desugaring of symbol!("abc") looks like this:

Cons<Char<'a'>, Cons<Char<'b'>, Cons<Char<'c'>, Nil>>>

While this type may seem complex, it has a compact representation from the perspective of the Rust compiler. Furthermore, since we don’t construct values from symbol types at runtime, there is no runtime overhead associated with them. The use of HasField to implement context-generic accessors introduces negligible compile-time overhead, even in large codebases.

It’s important to note that the current representation of symbols is a temporary workaround. Once Rust supports using strings in const generics, we can simplify the desugaring process and adjust our implementation accordingly.

If the explanation here still feels unclear, think of symbols as strings being used as types rather than values. In later sections, we’ll explore how cgp provides additional abstractions that abstract away the use of symbol! and HasField. These abstractions simplify the process, so you won’t need to worry about these details in simple cases.

Auto Accessor Traits

The process of defining and wiring many CGP components can be overwhelming for developers who are new to CGP. In the early stages of a project, there is typically not much need for customizing how fields are accessed. As a result, some developers may find the full use of field accessors introduced in this chapter unnecessarily complex.

To simplify the use of accessor traits, one approach is to define them not as CGP components, but as regular Rust traits with blanket implementations that leverage HasField. For example, we can redefine the HasApiBaseUrl trait as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

impl<Context> HasApiBaseUrl for Context
where
    Context: HasField<symbol!("api_base_url"), Value = String>,
{
    fn api_base_url(&self) -> &String {
        self.get_field(PhantomData)
    }
}
}

With this approach, the HasApiBaseUrl trait will be automatically implemented for any context that derives HasField and contains the relevant field. There is no longer need for explicit wiring of the ApiBaseUrlGetterComponent within the context components.

This approach allows providers, such as ReadMessageFromApi, to still use accessor traits like HasApiBaseUrl to simplify field access. Meanwhile, context implementers can simply use #[derive(HasField)] without having to worry about manual wiring.

The main drawback of this approach is that the context cannot easily override the implementation of HasApiBaseUrl, unless it opts not to implement HasField. However, it would be straightforward to refactor the trait in the future to convert it into a full CGP component.

Overall, this approach may be an appealing option for developers who want a simpler experience with CGP without fully utilizing its advanced features.

The #[cgp_auto_getter] Macro

To streamline the creation of auto accessor traits, the cgp crate provides the #[cgp_auto_getter] macro, which derives blanket implementations for accessor traits. For instance, the earlier example can be rewritten as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

cgp_type!( AuthToken );

#[cgp_auto_getter]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

#[cgp_auto_getter]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}
}

Since #[cgp_auto_getter] generates a blanket implementation leveraging HasField directly, there is no corresponding provider trait being derived in this case.

The #[cgp_auto_getter] attribute can also be applied to accessor traits that define multiple getter methods. For instance, we can combine two accessor traits into one, as shown below:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

cgp_type!( AuthToken );

#[cgp_auto_getter]
pub trait HasApiClientFields: HasAuthTokenType {
    fn api_base_url(&self) -> &String;

    fn auth_token(&self) -> &Self::AuthToken;
}
}

By using #[cgp_auto_getter], accessor traits are automatically implemented for contexts that use #[derive(HasField)] and include fields matching the names and return types of the accessor methods. This approach encapsulates the use of HasField and symbol!, providing well-typed and idiomatic interfaces for field access while abstracting the underlying mechanics.

Using HasField in Accessor Providers

With HasField, we can implement context-generic providers like ApiUrlGetter. Here's an example:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

pub struct GetApiUrl;

impl<Context> ApiBaseUrlGetter<Context> for GetApiUrl
where
    Context: HasField<symbol!("api_url"), Value = String>,
{
    fn api_base_url(context: &Context) -> &String {
        context.get_field(PhantomData)
    }
}
}

In this implementation, GetApiUrl is defined for any Context type that implements HasField<symbol!("api_url"), Value = String>. This means that as long as the context uses #[derive(HasField)], and has a field named api_url of type String, the GetApiUrl provider can be used with it.

Similarly, we can implement a context-generic provider for AuthTokenGetter as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}

pub struct GetAuthToken;

impl<Context> AuthTokenGetter<Context> for GetAuthToken
where
    Context: HasAuthTokenType + HasField<symbol!("auth_token"), Value = Context::AuthToken>,
{
    fn auth_token(context: &Context) -> &Context::AuthToken {
        context.get_field(PhantomData)
    }
}
}

The GetAuthToken provider is slightly more complex since the auth_token method returns an abstract Context::AuthToken type. To handle this, we require the Context to implement HasAuthTokenType and for the Value associated type to match Context::AuthToken. This ensures that GetAuthToken can be used with any context that has an auth_token field of the same type as the AuthToken defined in HasAuthTokenType.

The UseFields Pattern

The providers GetAuthToken and GetApiUrl share a common characteristic: they implement accessor traits for any context type by utilizing HasField, with the field name corresponding to the accessor method name. To streamline this pattern, cgp provides the UseFields marker struct, which simplifies the implementation of such providers:

#![allow(unused)]
fn main() {
struct UseFields;
}

With UseFields, we can bypass the need to define custom provider structs and implement the logic directly on UseFields, as shown below:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

cgp_type!( AuthToken );

#[cgp_component {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}

impl<Context> ApiBaseUrlGetter<Context> for UseFields
where
    Context: HasField<symbol!("api_url"), Value = String>,
{
    fn api_base_url(context: &Context) -> &String {
        context.get_field(PhantomData)
    }
}

impl<Context> AuthTokenGetter<Context> for UseFields
where
    Context: HasAuthTokenType + HasField<symbol!("auth_token"), Value = Context::AuthToken>,
{
    fn auth_token(context: &Context) -> &Context::AuthToken {
        context.get_field(PhantomData)
    }
}
}

The #[cgp_getter] Macro

The cgp crate offers the #[cgp_getter] macro, which automatically derives implementations like UseFields. As an extension of #[cgp_component], it provides the same interface and generates the same CGP component traits and blanket implementations.

With #[cgp_getter], you can define accessor traits and seamlessly use UseFields directly in the component wiring, eliminating the need for manual implementations:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate cgp_error_anyhow;
extern crate reqwest;
extern crate serde;

use core::fmt::Display;

use cgp::core::component::UseDelegate;
use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent};
use cgp::core::field::UseField;
use cgp::extra::error::RaiseFrom;
use cgp::prelude::*;
use cgp_error_anyhow::{DebugAnyhowError, UseAnyhowError};
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;

cgp_type!(Message);
cgp_type!(MessageId);
cgp_type!(AuthToken);

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}

pub struct ReadMessageFromApi;

#[derive(Debug)]
pub struct ErrStatusCode {
    pub status_code: StatusCode,
}

#[derive(Deserialize)]
pub struct ApiMessageResponse {
    pub message: String,
}

impl<Context> MessageQuerier<Context> for ReadMessageFromApi
where
    Context: HasMessageIdType<MessageId = u64>
        + HasMessageType<Message = String>
        + HasApiBaseUrl
        + HasAuthToken
        + CanRaiseError<reqwest::Error>
        + CanRaiseError<ErrStatusCode>,
    Context::AuthToken: Display,
{
    fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> {
        let client = Client::new();

        let url = format!("{}/api/messages/{}", context.api_base_url(), message_id);

        let response = client
            .get(url)
            .bearer_auth(context.auth_token())
            .send()
            .map_err(Context::raise_error)?;

        let status_code = response.status();

        if !status_code.is_success() {
            return Err(Context::raise_error(ErrStatusCode { status_code }));
        }

        let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?;

        Ok(message_response.message)
    }
}

#[cgp_getter {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

#[cgp_getter {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}

#[derive(HasField)]
pub struct ApiClient {
    pub api_base_url: String,
    pub auth_token: String,
}

pub struct ApiClientComponents;

impl HasComponents for ApiClient {
    type Components = ApiClientComponents;
}

delegate_components! {
    ApiClientComponents {
        ErrorTypeComponent: UseAnyhowError,
        ErrorRaiserComponent: UseDelegate<RaiseApiErrors>,
        MessageTypeComponent: UseType<String>,
        MessageIdTypeComponent: UseType<u64>,
        AuthTokenTypeComponent: UseType<String>,
        [
            ApiBaseUrlGetterComponent,
            AuthTokenGetterComponent,
        ]: UseFields,
        MessageQuerierComponent: ReadMessageFromApi,
    }
}

pub struct RaiseApiErrors;

delegate_components! {
    RaiseApiErrors {
        reqwest::Error: RaiseFrom,
        ErrStatusCode: DebugAnyhowError,
    }
}

pub trait CanUseApiClient: CanQueryMessage {}

impl CanUseApiClient for ApiClient {}
}

Compared to #[cgp_auto_getter], #[cgp_getter] follows the same wiring process as other CGP components. To achieve the same outcome as #[cgp_auto_getter], the only additional step required is delegating the getter component to UseFields within delegate_components!.

The primary advantage of using #[cgp_getter] is the ability to define custom accessor providers that can retrieve fields from the context in various ways, as we will explore in the next section.

Like #[cgp_auto_getter], #[cgp_getter] can also be used with accessor traits containing multiple methods. This makes it easy to upgrade a trait, such as HasApiClientFields, to use #[cgp_getter] if custom accessor providers are needed in the future:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;

cgp_type!( AuthToken );

#[cgp_getter {
    provider: ApiClientFieldsGetter,
}]
pub trait HasApiClientFields: HasAuthTokenType {
    fn api_base_url(&self) -> &String;

    fn auth_token(&self) -> &Self::AuthToken;
}
}

Static Accessors

One advantage of defining minimal accessor traits is that it allows the implementation of custom accessor providers that do not necessarily read field values from the context. For instance, we can create static accessor providers that always return a global constant value.

Static accessors are useful when we want to hard-code values for a specific context. For example, we might define a production ApiClient context that always uses a fixed API URL:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;
use std::sync::OnceLock;

use cgp::prelude::*;

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

pub struct UseProductionApiUrl;

impl<Context> ApiBaseUrlGetter<Context> for UseProductionApiUrl {
    fn api_base_url(_context: &Context) -> &String {
        static BASE_URL: OnceLock<String> = OnceLock::new();

        BASE_URL.get_or_init(|| "https://api.example.com".into())
    }
}
}

In this example, the UseProductionApiUrl provider implements ApiBaseUrlGetter for any context type. Inside the api_base_url method, we define a static variable BASE_URL using OnceLock<String>. This allows us to initialize the global variable exactly once, and it remains constant throughout the application.

OnceLock is especially useful since constructors like String::from are not const fn in Rust. By using OnceLock::get_or_init, we can run non-const constructors at runtime while still benefiting from compile-time guarantees. The static variable is scoped within the method, so it is only accessible and initialized by the provider.

With UseProductionApiUrl, we can now define a production ApiClient context, as shown below:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate cgp_error_anyhow;
extern crate reqwest;
extern crate serde;

use core::fmt::Display;
use core::marker::PhantomData;
use std::sync::OnceLock;

use cgp::core::component::UseDelegate;
use cgp::extra::error::RaiseFrom;
use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent};
use cgp::core::field::UseField;
use cgp::prelude::*;
use cgp_error_anyhow::{DebugAnyhowError, UseAnyhowError};
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;

cgp_type!( Message );
cgp_type!( MessageId );
cgp_type!( AuthToken );

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

#[cgp_component {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}

pub struct ReadMessageFromApi;

#[derive(Debug)]
pub struct ErrStatusCode {
    pub status_code: StatusCode,
}

#[derive(Deserialize)]
pub struct ApiMessageResponse {
    pub message: String,
}

impl<Context> MessageQuerier<Context> for ReadMessageFromApi
where
    Context: HasMessageIdType<MessageId = u64>
        + HasMessageType<Message = String>
        + HasApiBaseUrl
        + HasAuthToken
        + CanRaiseError<reqwest::Error>
        + CanRaiseError<ErrStatusCode>,
    Context::AuthToken: Display,
{
    fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> {
        let client = Client::new();

        let url = format!("{}/api/messages/{}", context.api_base_url(), message_id);

        let response = client
            .get(url)
            .bearer_auth(context.auth_token())
            .send()
            .map_err(Context::raise_error)?;

        let status_code = response.status();

        if !status_code.is_success() {
            return Err(Context::raise_error(ErrStatusCode { status_code }));
        }

        let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?;

        Ok(message_response.message)
    }
}

pub struct UseProductionApiUrl;

impl<Context> ApiBaseUrlGetter<Context> for UseProductionApiUrl {
    fn api_base_url(_context: &Context) -> &String {
        static BASE_URL: OnceLock<String> = OnceLock::new();

        BASE_URL.get_or_init(|| "https://api.example.com".into())
    }
}

pub struct GetAuthToken;

impl<Context> AuthTokenGetter<Context> for GetAuthToken
where
    Context: HasAuthTokenType + HasField<symbol!("auth_token"), Value = Context::AuthToken>,
{
    fn auth_token(context: &Context) -> &Context::AuthToken {
        context.get_field(PhantomData)
    }
}

#[derive(HasField)]
pub struct ApiClient {
    pub auth_token: String,
}

pub struct ApiClientComponents;

pub struct RaiseApiErrors;

impl HasComponents for ApiClient {
    type Components = ApiClientComponents;
}

delegate_components! {
    ApiClientComponents {
        ErrorTypeComponent: UseAnyhowError,
        ErrorRaiserComponent: UseDelegate<RaiseApiErrors>,
        MessageIdTypeComponent: UseType<u64>,
        MessageTypeComponent: UseType<String>,
        AuthTokenTypeComponent: UseType<String>,
        ApiBaseUrlGetterComponent: UseProductionApiUrl,
        AuthTokenGetterComponent: GetAuthToken,
        MessageQuerierComponent: ReadMessageFromApi,
    }
}

delegate_components! {
    RaiseApiErrors {
        reqwest::Error: RaiseFrom,
        ErrStatusCode: DebugAnyhowError,
    }
}

pub trait CanUseApiClient: CanQueryMessage {}

impl CanUseApiClient for ApiClient {}
}

In the component wiring, we specify UseProductionApiUrl as the provider for ApiBaseUrlGetterComponent. Notably, the ApiClient context no longer contains the api_base_url field.

Static accessors are particularly useful for implementing specialized contexts where certain fields must remain constant. With this approach, constant values don't need to be passed around as part of the context during runtime, and there's no concern about incorrect values being assigned at runtime. Additionally, because of the compile-time wiring, this method may offer performance benefits compared to passing dynamic values during execution.

Using HasField Directly Inside Providers

Since the HasField trait can be automatically derived by contexts, some developers may be tempted to forgo defining accessor traits and instead use HasField directly within the providers. For example, one could remove HasApiBaseUrl and HasAuthToken and implement ReadMessageFromApi as follows:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate reqwest;
extern crate serde;

use core::fmt::Display;
use core::marker::PhantomData;

use cgp::prelude::*;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;

cgp_type!( Message );
cgp_type!( MessageId );
cgp_type!( AuthToken );

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}

pub struct ReadMessageFromApi;

#[derive(Debug)]
pub struct ErrStatusCode {
    pub status_code: StatusCode,
}

#[derive(Deserialize)]
pub struct ApiMessageResponse {
    pub message: String,
}

impl<Context> MessageQuerier<Context> for ReadMessageFromApi
where
    Context: HasMessageIdType<MessageId = u64>
        + HasMessageType<Message = String>
        + HasAuthTokenType
        + HasField<symbol!("api_base_url"), Value = String>
        + HasField<symbol!("auth_token"), Value = Context::AuthToken>
        + CanRaiseError<reqwest::Error>
        + CanRaiseError<ErrStatusCode>,
    Context::AuthToken: Display,
{
    fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> {
        let client = Client::new();

        let url = format!(
            "{}/api/messages/{}",
            context.get_field(PhantomData::<symbol!("api_base_url")>),
            message_id
        );

        let response = client
            .get(url)
            .bearer_auth(context.get_field(PhantomData::<symbol!("auth_token")>))
            .send()
            .map_err(Context::raise_error)?;

        let status_code = response.status();

        if !status_code.is_success() {
            return Err(Context::raise_error(ErrStatusCode { status_code }));
        }

        let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?;

        Ok(message_response.message)
    }
}
}

In the example above, the provider ReadMessageFromApi requires the context to implement HasField<symbol!("api_base_url")> and HasField<symbol!("auth_token")>. To preserve the original behavior, we add constraints ensuring that the api_base_url field is of type String and that the auth_token field matches the type of Context::AuthToken.

When using get_field, since there are multiple HasField instances in scope, we need to fully qualify the field access to specify which field we want to retrieve. For example, we call context.get_field(PhantomData::<symbol!("api_base_url")>) to access the api_base_url field.

However, while the direct use of HasField is possible, it does not necessarily simplify the code. In fact, it often requires more verbose specifications for each field. Additionally, using HasField directly necessitates explicitly defining the field types. In contrast, with custom accessor traits like HasAuthToken, we can specify that a method returns an abstract type like `Self::AuthToken, which prevents accidental access to fields with the same underlying concrete type.

Using HasField directly also makes the provider less flexible if the context requires custom access methods. For instance, if we wanted to put the api_base_url field inside a separate ApiConfig struct, we would run into difficulties with HasField:

#![allow(unused)]
fn main() {
pub struct Config {
    pub api_base_url: String,
    // other fields
}

pub struct ApiClient {
    pub config: Config,
    pub auth_token: String,
    // other fields
}
}

In this case, an accessor trait like HasApiUrl would allow the context to easily use a custom accessor provider. With direct use of HasField, however, indirect access would be more cumbersome to implement.

That said, using HasField directly can be convenient during the initial development stages, as it reduces the number of traits a developer needs to manage. Therefore, we encourage readers to use HasField where appropriate and gradually migrate to more specific accessor traits when necessary.

The UseField Pattern

In the previous chapter, we were able to implement context-generic accessor providers like GetApiUrl and UseFields without directly referencing the concrete context. However, the field names, such as api_url and auth_token, were hardcoded into the provider implementation. This means that a concrete context cannot choose different field names for these specific fields unless it manually re-implements the accessors.

There are various reasons why a context might want to use different names for the field values. For instance, two independent accessor providers might choose the same field name for different types, or a context might have multiple similar fields with slightly different names. In these cases, it would be beneficial to allow the context to customize the field names instead of having the providers pick fixed field names.

To address this, the cgp crate provides the UseField marker type (note the lack of s, making it different from UseFields), which we can leverage to implement flexible accessor providers:

#![allow(unused)]
fn main() {
use core::marker::PhantomData;

pub struct UseField<Tag>(pub PhantomData<Tag>);
}

Similar to the UseDelegate pattern, the UseField type acts as a marker for accessor implementations that follow the UseField pattern. Using UseField, we can define the providers as follows:

#![allow(unused)]
fn main() {
extern crate cgp;

use core::marker::PhantomData;

use cgp::prelude::*;
use cgp::core::field::UseField;

#[cgp_component {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

cgp_type!( AuthToken );
#[cgp_component {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}

impl<Context, Tag> ApiBaseUrlGetter<Context> for UseField<Tag>
where
    Context: HasField<Tag, Value = String>,
{
    fn api_base_url(context: &Context) -> &String {
        context.get_field(PhantomData)
    }
}

impl<Context, Tag> AuthTokenGetter<Context> for UseField<Tag>
where
    Context: HasAuthTokenType + HasField<Tag, Value = Context::AuthToken>,
{
    fn auth_token(context: &Context) -> &Context::AuthToken {
        context.get_field(PhantomData)
    }
}
}

Compared to UseFields, the implementation of UseField is parameterized by an additional Tag type, which represents the field name we want to access. The structure of the implementation is almost the same as before, but instead of using symbol! to directly reference the field names, we rely on the Tag type to abstract the field names.

Deriving UseField from #[cgp_getter]

The implementation of UseField on accessor traits can be automatically derived when the trait is defined with #[cgp_getter]. However, the derivation will only occur if the accessor trait contains exactly one accessor method. This is because, in cases with multiple methods, there is no clear way to determine which accessor method should utilize the Tag type specified in UseField.

By combining #[cgp_getter] with UseField, we can streamline the implementation of ApiClient and directly wire the accessor components within delegate_components!:

#![allow(unused)]
fn main() {
extern crate cgp;
extern crate cgp_error_anyhow;
extern crate reqwest;
extern crate serde;

use core::fmt::Display;
use core::marker::PhantomData;

use cgp::core::component::UseDelegate;
use cgp::extra::error::RaiseFrom;
use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent};
use cgp::core::field::UseField;
use cgp::prelude::*;
use cgp_error_anyhow::{DebugAnyhowError, UseAnyhowError};
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Deserialize;

cgp_type!( Message );
cgp_type!( MessageId );
cgp_type!( AuthToken );

#[cgp_component {
    provider: MessageQuerier,
}]
pub trait CanQueryMessage: HasMessageIdType + HasMessageType + HasErrorType {
    fn query_message(&self, message_id: &Self::MessageId) -> Result<Self::Message, Self::Error>;
}

#[cgp_getter {
    provider: ApiBaseUrlGetter,
}]
pub trait HasApiBaseUrl {
    fn api_base_url(&self) -> &String;
}

#[cgp_getter {
    provider: AuthTokenGetter,
}]
pub trait HasAuthToken: HasAuthTokenType {
    fn auth_token(&self) -> &Self::AuthToken;
}

pub struct ReadMessageFromApi;

#[derive(Debug)]
pub struct ErrStatusCode {
    pub status_code: StatusCode,
}

#[derive(Deserialize)]
pub struct ApiMessageResponse {
    pub message: String,
}

impl<Context> MessageQuerier<Context> for ReadMessageFromApi
where
    Context: HasMessageIdType<MessageId = u64>
        + HasMessageType<Message = String>
        + HasApiBaseUrl
        + HasAuthToken
        + CanRaiseError<reqwest::Error>
        + CanRaiseError<ErrStatusCode>,
    Context::AuthToken: Display,
{
    fn query_message(context: &Context, message_id: &u64) -> Result<String, Context::Error> {
        let client = Client::new();

        let url = format!("{}/api/messages/{}", context.api_base_url(), message_id);

        let response = client
            .get(url)
            .bearer_auth(context.auth_token())
            .send()
            .map_err(Context::raise_error)?;

        let status_code = response.status();

        if !status_code.is_success() {
            return Err(Context::raise_error(ErrStatusCode { status_code }));
        }

        let message_response: ApiMessageResponse = response.json().map_err(Context::raise_error)?;

        Ok(message_response.message)
    }
}

#[derive(HasField)]
pub struct ApiClient {
    pub api_base_url: String,
    pub auth_token: String,
}

pub struct ApiClientComponents;

pub struct RaiseApiErrors;

impl HasComponents for ApiClient {
    type Components = ApiClientComponents;
}

delegate_components! {
    ApiClientComponents {
        ErrorTypeComponent: UseAnyhowError,
        ErrorRaiserComponent: UseDelegate<RaiseApiErrors>,
        MessageIdTypeComponent: UseType<u64>,
        MessageTypeComponent: UseType<String>,
        AuthTokenTypeComponent: UseType<String>,
        ApiBaseUrlGetterComponent: UseField<symbol!("api_base_url")>,
        AuthTokenGetterComponent: UseField<symbol!("auth_token")>,
        MessageQuerierComponent: ReadMessageFromApi,
    }
}

delegate_components! {
    RaiseApiErrors {
        reqwest::Error: RaiseFrom,
        ErrStatusCode: DebugAnyhowError,
    }
}

pub trait CanUseApiClient: CanQueryMessage {}

impl CanUseApiClient for ApiClient {}
}

In this wiring example, UseField<symbol!("api_base_url")> is used to implement the ApiBaseUrlGetterComponent, and UseField<symbol!("auth_token")> is used for the AuthTokenGetterComponent. By explicitly specifying the field names in the wiring, we can easily change the field names in the ApiClient context and update the wiring accordingly.

Conclusion

In this chapter, we explored various ways to define accessor traits and implement accessor providers. The HasField trait, being derivable, offers a way to create context-generic accessor providers without directly accessing the context's concrete fields. The UseField pattern standardizes how field accessors are implemented, enabling contexts to customize field names for the accessors.

As we will see in later chapters, context-generic accessor providers allow us to implement a wide range of functionality without tying code to specific concrete contexts. This approach makes it possible to maintain flexibility and reusability across different contexts.