Debugging Support
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; use anyhow::Error; use serde::{Serialize, Deserialize}; pub trait HasCgpProvider { type CgpProvider; } 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: HasCgpProvider, Context::CgpProvider: StringFormatter<Context>, { fn format_to_string(&self) -> Result<String, Error> { Context::CgpProvider::format_to_string(self) } } impl<Context> CanParseFromString for Context where Context: HasCgpProvider, Context::CgpProvider: StringParser<Context>, { fn parse_from_string(raw: &str) -> Result<Context, Error> { Context::CgpProvider::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) } } // Note: We pretend to 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 HasCgpProvider for Person { type CgpProvider = PersonComponents; } impl DelegateComponent<StringFormatterComponent> for PersonComponents { type Delegate = FormatAsJsonString; } impl DelegateComponent<StringParserComponent> for PersonComponents { type Delegate = 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; use anyhow::Error; use serde::{Serialize, Deserialize}; pub trait HasCgpProvider { type CgpProvider; } 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: HasCgpProvider, Context::CgpProvider: StringFormatter<Context>, { fn format_to_string(&self) -> Result<String, Error> { Context::CgpProvider::format_to_string(self) } } impl<Context> CanParseFromString for Context where Context: HasCgpProvider, Context::CgpProvider: StringParser<Context>, { fn parse_from_string(raw: &str) -> Result<Context, Error> { Context::CgpProvider::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) } } // Note: We pretend to 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 HasCgpProvider for Person { type CgpProvider = 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() }; 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 workarounds to make it easier to debug CGP errors in the meanwhile.
IsProviderFor
Trait
Since cgp
v0.4, we have developed a technique to explicitly propagate the constraint
requirements, and "trick" Rust to show us the error messages that we need.
We will first define a IsProviderFor
trait as follows:
#![allow(unused)] fn main() { pub trait IsProviderFor<Component, Context, Params = ()> {} }
The IsProviderFor
trait is a marker trait that can be trivially implemented.
It is intended to be implemented by provider structs, and is parameterized by 3 generic parameters:
Component
- The component name type that corresponds to the provider implementation.Context
- The context type that is implemented by the provider.Params
- Any additional generic parameters present in the provider trait, combined as a tuple.
Even though the IsProviderFor
trait can be trivially implemented, we intentionally include
additional constraints that are exactly the same as the original constraints specified in
the corresponding provider trait implementation.
For example, the provider FormatAsJsonString
in the earlier example would implement
IsProviderFor
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 IsProviderFor<Component, Context, Params = ()> {} pub struct StringFormatterComponent; pub trait StringFormatter<Context> { fn format_to_string(context: &Context) -> Result<String, 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)?) } } impl<Context> IsProviderFor<StringFormatterComponent, Context> for FormatAsJsonString where Context: Serialize, { } }
The way to understand the trait implementation is follows: FormatAsJsonString
has a provider implementation for StringFormatterComponent
with the context Context
,
given that Context: Serialize
.
We can think of IsProviderFor
trait to act as a "carrier" for the hidden constraints
of provider traits. With it, instead of trying to check each provider trait implementation,
we only need to check for the implementation of one trait, IsProviderFor
.
Propagating IsProviderFor
Constraints
Now that we have captured our constraints using IsProviderFor
, we need to somehow propagate the
constraint upward through the delegation chain, so that the context provider PersonComponents
also implements IsProviderFor
with the same constraints as its delegate, FormatAsJsonString
.
We would do that by modifying the provider trait definition, so that IsProviderFor
becomes a
supertrait of the provider trait:
#![allow(unused)] fn main() { extern crate anyhow; extern crate serde; extern crate serde_json; use anyhow::Error; use serde::{Serialize, Deserialize}; pub trait IsProviderFor<Component, Context, Params = ()> {} pub trait DelegateComponent<Name> { type Delegate; } pub struct StringFormatterComponent; pub trait StringFormatter<Context>: IsProviderFor<StringFormatterComponent, Context> { fn format_to_string(context: &Context) -> Result<String, Error>; } impl<Context, Component> StringFormatter<Context> for Component where Component: DelegateComponent<StringFormatterComponent> + IsProviderFor<StringFormatterComponent, Context>, Component::Delegate: StringFormatter<Context>, { fn format_to_string(context: &Context) -> Result<String, Error> { Component::Delegate::format_to_string(context) } } }
We make it a requirement that in order for a provider to implement StringFormatter<Context>
, the provider
also needs to implement IsProviderFor<StringFormatterComponent, Context>
.
The supertrait constraint not only makes sure that we don't forget to always implement IsProviderFor
,
but also triggers Rust to show any unsatisfied constraints that were hidden previously.
Additionally, we also modify the blanket implementation of StringFormatter
, so that when a provider like
PersonComponents
delegates the implementation, it would need to explicitly implement
IsProviderFor<StringFormatterComponent, Context>
in addition to implementing DelegateComponent<StringFormatterComponent>
.
With the new requirements in place, when we delegate PersonComponents
's provider implementation of StringFormatterComponent
,
we would also write an implementation of IsProviderFor
for it as follows:
impl DelegateComponent<StringFormatterComponent> for PersonComponents {
type Delegate = FormatAsJsonString;
}
impl<Context> IsProviderFor<StringFormatterComponent, Context>
for PersonComponents
where
FormatAsJsonString: IsProviderFor<StringFormatterComponent, Context>
{
}
Notice here that when implementing IsProviderFor
for PersonComponents
, we add a constraint that
directly requires FormatAsJsonString
to also implement IsProviderFor
with the same generic parameters.
By doing so, we resurface the constraints to Rust, so that it would recursively look into the
IsProviderFor
trait bounds, and print out any unsatisfied constraints in the error messages.
At this point, you may wonder why not link the provider trait directly within the delegated implementation of IsProviderFor
, such as:
impl<Context> IsProviderFor<StringFormatterComponent, Context>
for PersonComponents
where
FormatAsJsonString: StringFormatter<Context>,
{
}
The main reason to not do this is that it requires direct access to the provider trait, which is not as simple dealing with simple types. Furthermore, the provider trait may contain additional where clauses, which would also need to be propagated explicitly.
By making use of IsProviderFor
, we are essentially "erasing" everything at the trait level, and use a single trait to represent all other provider traits. After all, the only thing that we are interested here is to propagate the constraints for the purpose of showing better error messages.
Check Traits
Now that we have the wirings for IsProviderFor
in place, we can implement check traits to check
on whether the provider traits that we want to use with Person
are implemented by its provider,
PersonComponents
.
pub trait CanUsePersonComponents:
IsProviderFor<StringFormatterComponent, Person>
+ IsProviderFor<StringParserComponent, Person>
{
}
impl CanUsePersonComponents for PersonComponents {}
To put it simply, a check trait is defined just for checking whether a type implements other traits that we are
interested to use with that type. For our case, we want to check that
PersonComponents: IsProviderFor<StringFormatterComponent, Person>
and IsProviderFor<StringParserComponent, Person>
, since we expect PersonComponents
to implement
StringFormatter<Person>
and StringParser<Person>
. So we define a check trait called CanUsePersonComponents
,
and put the IsProviderFor
constraints as the supertrait.
The check trait is then followed by an implementation of CanUsePersonComponents
for PersonComponents
,
which acts as an assertion that PersonComponents
implements all the supertraits that we specified.
With the checks in place, we now get a compile error message that shows us exactly what we need to fix,
which is to implement Serialize
for Person
:
error[E0277]: the trait bound `Person: Serialize` is not satisfied
--> src/lib.rs:155:33
|
155 | impl CanUsePersonComponents for PersonComponents {}
| ^^^^^^^^^^^^^^^^ 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
= help: the following other types implement trait `Serialize`:
&'a T
&'a mut T
()
(T,)
(T0, T1)
(T0, T1, T2)
(T0, T1, T2, T3)
(T0, T1, T2, T3, T4)
and 131 others
note: required for `FormatAsJsonString` to implement `IsProviderFor<StringFormatterComponent, Person>`
--> src/lib.rs:91:15
|
91 | impl<Context> IsProviderFor<StringFormatterComponent, Context>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
92 | for FormatAsJsonString
| ^^^^^^^^^^^^^^^^^^
93 | where
94 | Context: Serialize,
| --------- unsatisfied trait bound introduced here
= note: 1 redundant requirement hidden
= note: required for `PersonComponents` to implement `IsProviderFor<StringFormatterComponent, Person>`
note: required by a bound in `CanUsePersonComponents`
--> src/lib.rs:150:5
|
149 | pub trait CanUsePersonComponents:
| ---------------------- required by a bound in this trait
150 | IsProviderFor<StringFormatterComponent, Person>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `CanUsePersonComponents`
As we can see, the use of IsProviderFor
helps make it possible for us to debug our CGP programs again.
CanUseComponent
Trait
Although we can directly check for the implementation of IsProviderFor
, the definition is not as straightforward as
defining checks directly with the Person
context. Furthermore, the context type Person
needs to be repeatedly
specified for each supertrait, which becomes tedious and adds noise.
We can improve the check definitions by introducing a helper CanUseComponent
trait that is defined as follows:
#![allow(unused)] fn main() { pub trait HasCgpProvider { type CgpProvider; } pub trait IsProviderFor<Component, Context, Params = ()> {} pub trait CanUseComponent<Component, Params = ()> {} impl<Context, Component, Params> CanUseComponent<Component, Params> for Context where Context: HasCgpProvider, Context::CgpProvider: IsProviderFor<Component, Context, Params>, { } }
Instead of being implemented by a provider, CanUseComponent
is intended to be implemented by a context type that
implements HasCgpProvider
. Compared to IsProviderFor
, there is no need to specify an explicit Context
parameter.
CanUseComponent
also has a blanket implementation, making it automatically implemented for any Context
type
that implements HasCgpProvider
, with Context::CgpProvider
implementing IsProviderFor
.
In other words, we are using CanUseComponent
as a trait alias to IsProviderFor
, with improved ergonomics
that we can check directly against a context type.
Using CanUseComponent
, we can re-define our check trait much simply as follows:
pub trait CanUsePerson:
CanUseComponent<StringFormatterComponent>
+ CanUseComponent<StringParserComponent>
{
}
impl CanUsePerson for Person {}
Compared to before, we define the check trait CanUsePerson
directly on Person
instead of PersonComponents
.
When we try compiling, we would still get the same error message that shows us the required information
to fix the error:
error[E0277]: the trait bound `Person: CanUseComponent<StringFormatterComponent>` is not satisfied
--> src/lib.rs:164:23
|
164 | impl CanUsePerson for Person {}
| ^^^^^^ the trait `Serialize` is not implemented for `Person`
...
Limitations
The use of IsProviderFor
significantly improves the developer experience for CGP, which was significantly harder
to debug prior to the introduction of this technique in v0.4.0.
At this point, you might be concerned of the additional boilerplate required to implement and propagate the constraints for IsProviderFor
. However, as we will see in the next chapter, the CGP macros will automate the bulk of the implementation of IsProviderFor
, and only require lightweight attribute macros to be applied to enable the code generation.
It is worth noting that the IsProviderFor
trait is introduced as a workaround to improve the error messages of CGP code in Rust. Hypothetically, if Rust could provide better support of showing the relevant error messages, we could entirely remove the use of IsProviderFor
in future versions of CGP.
On the other hand, the use of IsProviderFor
can serve as a great showcase to the Rust compiler team on what error messages should have been shown by default, and make it easier to evaluate what should be the official fix in the Rust compiler.
That said, the error messages shown via IsProviderFor
can sometimes be too verbose, especially when an application contains many providers with deeply nested dependencies. The reason is that any unsatisfied constraint from deeply nested dependencies can propagate all the way up through chains of IsProviderFor
implementations. Although this can help us pinpoint the root cause, it could generate too much noise when showing all errors from the intermediary layers.
When encountering heap of error messages generated from IsProviderFor
, a useful tip is that the relevant error message may be hiding near the bottom. So it may be useful to read from the bottom up instead of top down.
Interactive Debugging with Argus
A potential improvement that we are currently exploring is to make use of Argus to help navigate error messages generated from CGP code. Argus provides an interactive debugger for trait-related errors, which may be well suited to be used for debugging CGP code.
We will add further details in the future to share potential integration with Argus. For now, interested readers are encouraged to check out the project, and perhaps make contribution to make such integration possible.
Conclusion
In this chapter, we have learned how CGP makes use of the IsProviderFor
trait to help show relevant error messages when encountering unsatisfied constraints. In the next chapter, we will walk through how to automate the generation of all the relevant boilerplates that we have learned so far, and write succint CGP code using macros.
We will show the full example that we have walked through earlier, with the addition of IsProviderFor
into the code:
#![allow(unused)] fn main() { extern crate anyhow; extern crate serde; extern crate serde_json; use anyhow::Error; use serde::{Deserialize, Serialize}; pub trait HasCgpProvider { type CgpProvider; } pub trait IsProviderFor<Component, Context, Params = ()> {} pub trait DelegateComponent<Name> { type Delegate; } pub trait CanUseComponent<Component, Params = ()> {} impl<Context, Component, Params> CanUseComponent<Component, Params> for Context where Context: HasCgpProvider, Context::CgpProvider: IsProviderFor<Component, Context, Params>, { } pub struct StringFormatterComponent; pub struct StringParserComponent; 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>: IsProviderFor<StringFormatterComponent, Context> { fn format_to_string(context: &Context) -> Result<String, Error>; } pub trait StringParser<Context>: IsProviderFor<StringParserComponent, Context> { fn parse_from_string(raw: &str) -> Result<Context, Error>; } impl<Context> CanFormatToString for Context where Context: HasCgpProvider, Context::CgpProvider: StringFormatter<Context>, { fn format_to_string(&self) -> Result<String, Error> { Context::CgpProvider::format_to_string(self) } } impl<Context> CanParseFromString for Context where Context: HasCgpProvider, Context::CgpProvider: StringParser<Context>, { fn parse_from_string(raw: &str) -> Result<Context, Error> { Context::CgpProvider::parse_from_string(raw) } } impl<Context, Component> StringFormatter<Context> for Component where Component: DelegateComponent<StringFormatterComponent> + IsProviderFor<StringFormatterComponent, Context>, 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> + IsProviderFor<StringParserComponent, Context>, Component::Delegate: StringParser<Context>, { fn parse_from_string(raw: &str) -> Result<Context, Error> { Component::Delegate::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)?) } } impl<Context> IsProviderFor<StringFormatterComponent, Context> for FormatAsJsonString where Context: Serialize, { } 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)?) } } impl<Context> IsProviderFor<StringParserComponent, Context> for ParseFromJsonString where Context: for<'a> Deserialize<'a>, { } // Note: We pretend to 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 HasCgpProvider for Person { type CgpProvider = PersonComponents; } impl DelegateComponent<StringFormatterComponent> for PersonComponents { type Delegate = FormatAsJsonString; } impl<Context> IsProviderFor<StringFormatterComponent, Context> for PersonComponents where FormatAsJsonString: IsProviderFor<StringFormatterComponent, Context>, { } impl DelegateComponent<StringParserComponent> for PersonComponents { type Delegate = ParseFromJsonString; } impl<Context> IsProviderFor<StringParserComponent, Context> for PersonComponents where ParseFromJsonString: IsProviderFor<StringParserComponent, Context> { } pub trait CanUsePerson: CanUseComponent<StringFormatterComponent> + CanUseComponent<StringParserComponent> { } impl CanUsePerson for Person {} }