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.