Overview
CGP v0.5.0 has been released, bringing a range of new features and some breaking changes as part of the ongoing effort toward stabilization. This version introduces several improvements to make CGP more practical, expressive, and easier to use in real-world Rust projects.
The highlights of this major release are summarized below. Also check out the changelog for the full list of changes.
New Features
#[derive(CgpData)]
Macro
The new #[derive(CgpData)]
macro provides a unified way to turn any struct or enum into an extensible data type.
For example, given:
#[derive(CgpData)]
pub struct Person {
pub name: String,
pub age: u8,
}
#[derive(CgpData)]
pub enum User {
Known(Person),
Anonymous(u64),
}
This macro automatically derives all the extensible data traits for you, including HasField
, FromVariant
, HasFields
, BuildField
, and ExtractField
.
Before v0.5.0, you had to derive these traits separately, as shown below:
#[derive(HasField, HasFields, BuildField)]
pub struct Person {
pub name: String,
pub age: u8,
}
#[derive(FromVariant, HasFields, ExtractField)]
pub enum User {
Known(Person),
Anonymous(u64),
}
With #[derive(CgpData)]
, the process is now simpler, cleaner, and less tedious.
#[cgp_auto_dispatch]
Macro
The new #[cgp_auto_dispatch]
macro allows automatic dispatch of trait implementations for enums when all their variants implement the same trait.
Consider the following example with a Shape
enum:
#[derive(CgpData)]
pub enum Shape {
Circle(Circle),
Rectangle(Rectangle),
}
pub struct Circle {
pub radius: f64,
}
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
Suppose we want to define a trait HasArea
for computing the area of shapes. We can define and implement it as follows:
#[cgp_auto_dispatch]
pub trait HasArea {
fn area(&self) -> f64;
}
impl HasArea for Circle {
fn area(&self) -> f64 {
PI * self.radius * self.radius
}
}
impl HasArea for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
With this setup, HasArea
is now automatically implemented for Shape
without the need to manually write any additional impl
blocks.
Behind the scenes, the #[cgp_auto_dispatch]
macro generates a blanket implementation of HasArea
using extensible visitors to dispatch calls to the appropriate variant. Because Shape
uses #[derive(CgpData)]
, it already includes the extensible variant constructs needed for the blanket implementation.
An important detail is that #[derive(CgpData)]
and #[cgp_auto_dispatch]
work seamlessly across crate boundaries. The trait and the enum do not need to know about each other for the blanket implementation to take effect. Everything just works.
A deeper explanation of how #[cgp_auto_dispatch]
operates will be covered in a future blog post. For now, you can experiment with it directly in your projects. Even if you are not yet using the rest of CGP, this macro can simplify your code right away.
UpdateField
Trait
A new UpdateField
trait has been introduced to generalize the process of updating extensible records:
pub trait UpdateField<Tag, M: MapType> {
type Value;
type Mapper: MapType;
type Output;
fn update_field(
self,
_tag: PhantomData<Tag>,
value: M::Map<Self::Value>,
) -> (<Self::Mapper as MapType>::Map<Self::Value>, Self::Output);
}
The UpdateField
trait allows you to replace a wrapped value within a partial record with another wrapped value. It is automatically derived by #[derive(CgpData)]
, which means that field update operations can rely on UpdateField
without requiring any manual derivation.
For instance, the BuildField
trait is now implemented as a blanket implementation that builds upon UpdateField
:
pub trait BuildField<Tag> {
type Value;
type Output;
fn build_field(self, _tag: PhantomData<Tag>, value: Self::Value) -> Self::Output;
}
impl<Context, Tag> BuildField<Tag> for Context
where
Context: UpdateField<Tag, IsPresent, Mapper = IsNothing>,
{
type Value = Context::Value;
type Output = Context::Output;
fn build_field(self, tag: PhantomData<Tag>, value: Self::Value) -> Self::Output {
self.update_field(tag, value).1
}
}
In essence, BuildField
is implemented by transforming a field that is initially IsNothing
in a partial record into an IsPresent
field, using UpdateField
as the underlying mechanism.
Finalize extensible builder with default values
The extensible builder pattern now supports finalizing a partial record by filling in any uninitialized fields using their Default
values. This functionality is provided by the new finalize_with_default
method.
For example, consider the following extensible record:
#[derive(CgpData)]
pub struct FooBar {
pub foo: String,
pub bar: u64,
}
You can now construct a FooBar
instance while letting the bar
field fall back to its default value of 0
:
let foo_bar = FooBar::builder() // __PartialFooBar<IsNothing, IsNothing>
.build_field(PhantomData::<Symbol!("foo")>, "foo".to_owned()) // __PartialFooBar<IsPresent, IsNothing>
.finalize_with_default(); // FooBar
Behind the scene, finalize_with_default
works by using the UpdateField
trait to perform a natural transformation on each field modifier. It applies the Default
implementation to convert IsNothing
fields into IsPresent
, completing the record automatically.
Extensible builder with optional field status
The original design of the extensible builder uses the typestate pattern to track whether each field in a partial record has been set. In this pattern, IsPresent
indicates that a field has been assigned a value, while IsNothing
indicates that it has not.
Although this approach provides strong compile-time guarantees, it also means that each state of the partial record has a distinct type. For example, __PartialFooBar<IsPresent, IsNothing>
and __PartialFooBar<IsNothing, IsPresent>
are considered different types. This can be inconvenient in scenarios where the builder needs to maintain a single type throughout the build process, such as when dynamically deserializing JSON data into a partial record.
To address this limitation, a new IsOptional
field state has been introduced. It allows a partial record to retain the same type even as fields are updated. This is achieved by mapping field values to Option
, so that the status of each field is determined at runtime. Using this approach, a partial record like __PartialFooBar<IsOptional, IsOptional>
can serve as an optional builder.
You can create and use an optional builder with the optional_builder
, set
, and finalize_optional
methods as shown below:
let foo_bar = FooBar::optional_builder() // __PartialFooBar<IsOptional, IsOptional>
.set(PhantomData::<Symbol!("foo")>, "foo".to_owned()) // __PartialFooBar<IsOptional, IsOptional>
.set(PhantomData::<Symbol!("bar")>, 42) // __PartialFooBar<IsOptional, IsOptional>
.finalize_optional() // Result<FooBar, String>
.unwrap() // FooBar
Unlike the original typestate builder, the type of the builder remains __PartialFooBar<IsOptional, IsOptional>
after each call to set
. The finalize_optional
method returns a Result
, producing an error if any field contains a None
value. This check is necessary because, without the typestate guarantees, the compiler cannot ensure at compile time that all fields are initialized.
You can also use the optional builder with finalize_with_default
if all fields in the extensible record implement Default
. In that case, you can safely finalize the record without handling a potential error:
let foo_bar = FooBar::optional_builder() // __PartialFooBar<IsOptional, IsOptional>
.set(PhantomData::<Symbol!("foo")>, "foo".to_owned()) // __PartialFooBar<IsOptional, IsOptional>
.finalize_with_default(); // FooBar
The optional builder is used by cgp-serde
to implement generic deserialization for extensible records.
Extensible visitor dispatchers
The extensible visitor providers have been redesigned to support a wider range of use cases. The library now includes the following dispatchers:
MatchWithValueHandlers
– matches and dispatches on an owned inputValue
.MatchWithValueHandlersRef
– matches and dispatches on a borrowed input&Value
.MatchWithValueHandlersMut
– matches and dispatches on a mutably borrowed input&mut Value
.
These dispatchers are compatible with both the owned and borrowed variants of the handler traits, such as Computer
and ComputerRef
. Within Computer
, the borrowed value, including the reference, is used as the Input
parameter. For example:
MatchWithValueHandlersRef: Computer<Context, &Value>
and also:
MatchWithValueHandlersRef: ComputerRef<Context, Value>
In addition, tuple variants of the matchers have been introduced to handle input values alongside additional arguments:
MatchFirstWithValueHandlers
– matches and dispatches on an ownedValue
within an input(Value, Args)
.MatchFirstWithValueHandlersRef
– matches and dispatches on a borrowed&Value
within an input(&Value, Args)
.MatchFirstWithValueHandlersMut
– matches and dispatches on a mutably borrowed&mut Value
within an input(&mut Value, Args)
.
These matchers are designed to support the implementation of #[cgp_auto_dispatch]
, enabling extra arguments to be passed through the dispatchers without being directly matched on. They can also be used in advanced extensible visitor scenarios where additional arguments need to be forwarded to the visitors.
It is important to note that the handlers do not implement the reference variants of the handler traits. For instance:
MatchFirstWithValueHandlersRef: Computer<Context, (&Value, Args)>
is implemented, but not:
MatchFirstWithValueHandlersRef: ComputerRef<Context, (Value, Args)>
This is because ComputerRef
converts the entire input type into a reference, making it difficult to pass an owned value within Args
.
This distinction highlights that the Computer
trait is more flexible than ComputerRef
, since it can work with borrowed values as input types. By contrast, ComputerRef
is simpler for developers who are new to the framework, as it allows working with borrowed values without needing to understand higher-ranked trait bounds. For more advanced use cases that involve extra arguments, it is generally more straightforward to use Computer
directly rather than extending ComputerRef
to accommodate those patterns.
Internally, all dispatchers share the same core abstraction that powers extensible visitors. This means the library does not reimplement the same logic multiple times for each of the six dispatchers. It also allows advanced users to build on these abstractions to define custom dispatchers for specialized use cases.
For a detailed explanation of how these dispatchers are implemented, see the section on extensible visitors. Additional blog posts will follow to explore these extended features in greater depth.
AsyncComputer
Trait
A new AsyncComputer
trait has been introduced as the asynchronous counterpart to Computer
. It is defined as follows:
#[cgp_component {
provider: AsyncComputer,
derive_delegate: [
UseDelegate<Code>,
UseInputDelegate<Input>,
],
}]
#[async_trait]
pub trait CanComputeAsync<Code, Input> {
type Output;
async fn compute_async(&self, _code: PhantomData<Code>, input: Input) -> Self::Output;
}
The AsyncComputer
trait is more general than Handler
because it does not require the function to return a Result
. This design creates a clearer symmetry between the synchronous and asynchronous variants of the computation traits: AsyncComputer
serves as the async version of Computer
, while Handler
serves as the async version of TryComputer
.
With this addition, abstractions such as extensible visitors and monadic computations now implement their asynchronous logic using AsyncComputer
rather than Handler
. The same promotion pattern used in the synchronous counterparts is then applied to lift these implementations to TryComputer
and Handler
, maintaining consistency across both sync and async components.
Monadic computation pipeline
The cgp-monad
crate has been introduced to provide foundational support for monadic computation. This functionality simplifies the implementation of extensible visitors by introducing a structured yet flexible way to handle composable computations.
Unlike the standard definition of monads in languages such as Haskell, the monadic implementation in CGP is retroactive. This means that existing Rust types like Result
and Option
can be treated as monads without requiring direct trait implementations on those types.
For example, instead of defining a monad as a higher-kinded type, CGP allows the extraction of inner value types from a monadic type using a monadic provider:
pub trait ContainsValue<Output> {
type Value;
}
impl<T, E> ContainsValue<Result<T, E>> for ErrMonadic {
type Value = T;
}
In this example, the ContainsValue
trait matches on an Output
type and yields its underlying Value
type. The ErrMonadic
provider implements ContainsValue
by extracting the value type T
from a Result<T, E>
.
Instead of relying on the standard monadic bind
operation, CGP introduces a lifting mechanism that wraps the input of a Computer
provider:
impl<Context, Code, T1, T2, E, M, Cont> Computer<Context, Code, Result<T1, E>> for BindErr<M, Cont>
where
Cont: Computer<Context, Code, T1>,
M: ContainsValue<Cont::Output, Value = Result<T2, E>> + ...,
{ ... }
In this case, the provider BindErr<ErrMonadic, Cont>
wraps a provider Cont
that implements Computer
with an input type of T1
and an output type of Result<T2, E>
. It transforms it into a Computer
provider that accepts Result<T1, E>
as input and produces Result<T2, E>
as output.
Conceptually, this corresponds to the following Haskell signature:
bind :: (a -> m b) -> m a -> m b
which reverses the argument order of the standard bind operator (>>=)
:
(>>=) :: m a -> (a -> m b) -> m b
By reversing the argument order, CGP effectively turns the bind operator into a higher-order function that lifts monadic functions, allowing them to be composed afterward through ordinary function composition.
At present, CGP’s monadic implementation requires separate bind implementations for synchronous and asynchronous computations. This limitation exists because stable Rust does not yet support impl trait in type aliases, which prevents us to name anonymous impl Future
as the Output
type in Computer
. Once this feature becomes stable, CGP will be able to extend monadic support to opaque types behind impl Trait
, including Future
, Stream
, and Iterator
.
A complete introduction to monadic computation within CGP deserves its own dedicated article. For now, the key takeaway is that CGP v0.5.0 establishes a foundational monadic layer that enables the development of more advanced abstractions, such as those seen in extensible visitors, and paves the way for future extensions of the framework.
Generate &'static str
from symbols
The type produced by the Symbol!
macro can now generically produce a &'static str
value that can be used within generic code. For example:
const HELLO: &'static str = <Symbol!("hello") as StaticString>::VALUE;
This functionality is provided through the StaticString
trait along with a blanket implementation, defined as follows:
pub trait StaticString {
const VALUE: &'static str;
}
This represents a major breakthrough, as it allows actual &'static str
values to be “passed” as types through generic parameters by encapsulating them in CGP symbol types, without requiring ad hoc implementations to be written.
Internally, a symbol such as Symbol!("abc")
is expanded into:
ψ<3, ζ<'a', ζ<'b', ζ<'c', ε>>>>
or, in a more human-readable form:
Symbol<3, Chars<'a', Chars<'b', Chars<'c', Nil>>>>
The value 3
in the first position represents the length of the symbol. This metadata is generated by the Symbol!
macro starting from v0.5.0, enabling the reconstruction of the corresponding &'static str
value during const evaluation.
This approach is necessary because, without it, full const generics support would be required to “count” the number of characters in a type, which is not yet available in stable Rust. By precomputing the length inside the Symbol!
macro, it becomes possible to construct an array of the correct size and iterate within const evaluation to recreate the string value.
The ability to produce static strings is particularly valuable in generic code that needs to pass &str
values to other functions, such as in cgp-serde
. Without this feature, a new String
value would have to be reconstructed every time a function is called, which would significantly impact the performance of using symbols as string values.
Breaking Changes
Alongside the new features, v0.5.0 introduces several breaking changes. These updates are part of the ongoing effort to prepare for eventual v1 stabilization, ensuring that any necessary breaking changes are made early rather than later.
Removal of Async
and Send
bounds for async constructs
The most significant breaking change in this release is the removal of the Async
trait, which was previously defined as:
pub trait Async: Send + Sync {}
impl<T: Send + Sync> Async for T {}
In earlier versions of CGP, the Async
trait served as an alias for Send + Sync
. It was used in trait bounds for abstract types and generic parameters to ensure that the Future
returned by generic async functions could implement Send
. This requirement is common in functions such as tokio::spawn
, and it has traditionally been one of the main reasons Rust developers annotate Send + Sync
throughout their code.
In order to support Send-able futures, CGP code used to be full of boilerplate of the use of Async. It also makes it more complicated to support both async and sync use cases in CGP.
Prior to v0.5.0, CGP’s codebase relied heavily on Async
bounds to helps ensure that generic async functions can be called within tokio::spawn
, but it also introduced considerable boilerplate. It also made it cumbersome for CGP to support both synchronous and asynchronous use cases, requiring traits such as HasAsyncErrorType
to act as aliases for HasErrorType<Error: Async> + Async
. Maintaining both async and sync versions of similar traits was necessary so that non-async users could instantiate abstract types like Error
with values that were not Send
.
To improve ergonomics around Send
-able futures, the Rust compiler team has been developing Return Type Notation (RTN). RTN will allow developers to impose the Send
bound on a Future
retroactively, at the point where it is actually needed, such as when passing it to tokio::spawn
. With RTN, async code using CGP could avoid the need for pervasive Send + Sync
annotations.
However, RTN does not appear to be close to stabilization. As a result, CGP originally planned to keep the Async
constructs for compatibility in the short term. The good news is that an alternative approach has been found to retroactively add the Send
bound to futures without relying on RTN. This allows CGP to simplify its design and remove the redundant Async
abstractions entirely, while still maintaining compatibility with common async workflows.
Emulating RTN
The key idea is to introduce a second proxy trait that provides the same method as the original, but with the returned Future
implementing Send
. This proxy trait must be implemented manually by a concrete context, outside of CGP, but the implementation can forward to the original trait, which can still be implemented using CGP.
A complete example demonstrating the use of tokio::spawn
is available here and here. The example uses the CanRun
trait, defined as:
#[cgp_component(Runner)]
pub trait CanRun: HasErrorType {
fn run(&self) -> impl Future<Output = Result<(), Self::Error>>;
}
To use run
inside tokio::spawn
, we define a proxy trait that adds the Send
bound to the returned Future
:
#[cgp_component(SendRunner)]
pub trait CanSendRun: Send + HasErrorType {
fn send_run(&self) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
The Runner
trait can still be used to implement context-generic providers, so we do not need to require Send
on generic types. For example:
#[cgp_new_provider]
impl<Context> Runner<Context> for RunWithFooBar
where
Context: HasFooBar, // don't need to require Context::FooBar:Send
{
async fn run(&self) { ... }
}
If we need to call CanRun
inside tokio::spawn
within generic code, we can instead use CanSendRun
:
fn spawn_and_run_foo_bar<
Context: 'static + CanSendRun<RunFooBar>,
>(context: Context) {
tokio::spawn(async move {
let _ = context.send_run(PhantomData).await;
})
}
When implementing a concrete context, we delegate Runner
to RunWithFooBar
and manually implement SendRunner
by forwarding to Runner
:
#[cgp_context]
pub struct App { ... }
delegate_components! {
AppComponents {
RunnerComponent:
RunWithFooBar,
...
}
}
#[cgp_provider]
impl SendRunner<App> for AppComponents {
async fn send_run(context: &App) -> Result<(), Error> {
context.run(code).await
}
}
By directly implementing SendRunner
for App
, the trait system can access the concrete type and its associated types, allowing it to retroactively determine that the future returned by run
implements Send
. This effectively emulates RTN by implementing the proxy trait at the top level.
Using this approach, CGP is able remove all uses of Async
without waiting for RTN to stabilize. Meanwhile, users who need Send
-bound futures can rely on the proxy trait technique to recover the Send
bound through the concrete context.
This hack is expected to be temporary. Once RTN is stabilized, the proxy traits can be eliminated entirely. Migrating to the proxy trait approach now is simpler than retaining Async
long-term, and it avoids a potentially painful migration in the future if CGP becomes widely adopted.
Migration Advice
For existing codebases that still rely on Async
and the Send
-safe variants such as HasAsyncErrorType
, you can copy these definitions locally to continue using them in your projects.
Although CGP has removed the Async
constructs, this does not prevent developers from enforcing Send
bounds in their own code. The main impact is that all async traits officially defined by the cgp
crate no longer impose a Send
bound on the returned Future
. If your project defines traits that require returned futures to implement Send
, you may encounter issues when calling CGP’s async traits. In other words, the split between Send
and non-Send
only becomes an issue if your project aims to interop with the remaining CGP ecosystem.
In the short term, the easiest way to address this is to remove all Send
bounds of async functions in your own codebase. If that is not practical, the simplest workaround is to define your own versions of the relevant CGP traits that explicitly include Send
in the returned Future
.
Currently, there are only a small number of async traits in CGP, such as CanRun
, CanComputeAsync
, and CanHandle
. Unless your project heavily relies on these abstractions, redefining these traits locally to include Send
should be straightforward and require minimal effort.
Desugaring of Symbols!
The symbol!
macro has been renamed to Symbol!
to better indicate that it desugars to a type rather than a value. In addition to the renaming, the macro now desugars to a different form.
Before v0.5.0, symbol!("abc")
desugared to:
ι<'a', ι<'b', ι<'c', ε>>>
Starting from v0.5.0, it desugars to:
ψ<3, ζ<'a', ζ<'b', ζ<'c', ε>>>>
The ι
identifier has been replaced with ζ
because using ι
could trigger Rust’s confusable_idents
warning if the variable i
appears elsewhere in the code. Choosing ζ
reduces the likelihood of such warnings.
Additionally, ψ
is added at the head of the symbol type along with the length metadata. This enables the implementation of StaticString
, allowing the construction of a &'static str
without waiting for full const-generic support in Rust.
Finally, the Char
type, previously an alias for ζ
, has been renamed to Chars
to better reflect that it represents a list of characters rather than a single character.
Reorganize exports in cgp-field
The cgp-field
crate has grown significantly, so the exports have been reorganized into several submodules. For example, the HasField
trait is now exported from cgp::fields::traits::HasField
instead of the top-level path cgp::fields::HasField
.
Add __Partial
prefix to derived partial data types
Partial data types generated by extensible data type macros now receive a __Partial
prefix. This makes them effectively hidden and reduces the likelihood of name conflicts with user-defined structs.
For example, given:
#[derive(CgpData)]
pub struct Person {
pub name: String,
pub age: u8,
}
The derived partial data type is now named __PartialPerson
:
pub struct __PartialPerson<F0: MapType, F1: MapType> {
pub name: F0::Map<String>,
pub age: F1::Map<u8>,
}
Prior to v0.5.0, the partial type was named PartialPerson
, which could easily conflict with a user-defined struct of the same name in the same module.
This breaking change should not affect most existing code, as partial data types are not intended to be used directly by end users.
Add Code
parameter to CanRun
The CanRun
trait has been updated to include a Code
parameter:
#[cgp_component {
provider: Runner,
derive_delegate: UseDelegate<Code>,
}]
#[async_trait]
pub trait CanRun<Code>: HasErrorType {
async fn run(&self, _code: PhantomData<Code>) -> Result<(), Self::Error>;
}
Prior to v0.5.0, the trait was defined as:
#[cgp_component(Runner)]
#[async_trait]
pub trait CanRun: HasErrorType {
async fn run(&self) -> Result<(), Self::Error>;
}
The Code
parameter allows embedding type-level DSLs for running top-level functions, similar to how Code
is used in Computer
and Handler
for DSLs like Hypershell.
For existing users who wish to continue using the original CanRun
trait without migrating, you can copy the previous definition into your project and continue using it locally.
Removal of HasInner
trait
The HasInner
trait has been removed, along with the cgp-inner
crate. It was previously defined as:
#[cgp_component {
name: InnerComponent,
provider: ProvideInner,
}]
pub trait HasInner {
type Inner;
fn inner(&self) -> &Self::Inner;
}
The functionality provided by HasInner
has largely been superseded by the UseField
pattern, which enables more flexible composition of multiple contexts.
For existing users who wish to continue using the original HasInner
trait without migrating, you can copy its previous definition into your project and use it locally.
Improvements
Several improvements have been made to existing constructs in CGP. Here are some highlights.
Allow non-self argument in getter methods
It is now possible to use #[cgp_getter]
and #[cgp_auto_getter]
with target types other than Self
. For example, you can now define a trait like:
#[cgp_getter]
pub trait HasFooBar: HasFooType + HasBarType {
fn foo_bar(foo: &Self::Foo) -> &Self::Bar;
}
With this, the provider UseField<Symbol!("bar")>
would implement FooBarGetter<Context>
if Context::Foo
implements HasField<Symbol!("bar"), Value = Context::Bar>
.
Support use of lifetime parameters inside CGP traits
Lifetimes can now be included inside component trait parameters. For instance, cgp-serde
defines a component corresponding to serde
's Deserialize
as follows:
#[cgp_component {
provider: ValueDeserializer,
derive_delegate: UseDelegate<Value>,
}]
pub trait CanDeserializeValue<'de, Value> {
fn deserialize<D>(&self, deserializer: D) -> Result<Value, D::Error>
where
D: serde::Deserializer<'de>;
}
Within the type parameters of IsProviderFor
, the lifetime 'de
is captured as Life<'de>
, which is defined as:
pub struct Life<'a>(pub PhantomData<*mut &'a ()>);
Using Life
, the lifetime can be referred to inside check_components!
, as in the following example:
check_components! {
<'de> CanDeserializeApp for App {
ValueDeserializerComponent: [
(Life<'de>, u64),
(Life<'de>, String),
]
}
}
Shortcut for overriding provider names in #[cgp_type]
and #[cgp_getter]
You can now customize only the name of the provider trait in #[cgp_type]
and #[cgp_getter]
without using the fully qualified key-value syntax.
For example:
#[cgp_type(FooTypeProviderComponent)]
pub trait HasFooType {
type Foo;
}
#[cgp_getter(FooGettterComponent)]
pub trait HasFoo: HasFooType {
fn foo(&self) -> &Self::Foo;
}
is equivalent to:
#[cgp_type {
provider: FooTypeProviderComponent,
}]
pub trait HasFooType {
type Foo;
}
#[cgp_getter {
provider: FooGettterComponent,
}]
pub trait HasFoo: HasFooType {
fn foo(&self) -> &Self::Foo;
}
Bug Fixes
Fix use of new
in delegate_components!
when keys array is used
A bug in delegate_components!
has been fixed that occurred when value expressions like UseDelegate<new InnerComponents { ... }>
were used with multiple keys in a list.
For example:
delegate_components! {
MyAppComponents {
[
FooComponent,
BarComponent,
]:
UseDelegate<new InnerComponents {
u64: HandleNumber,
String: HandleString,
}>,
}
}
Previously, the inner struct InnerComponents
would be expanded twice. With this fix, InnerComponents
is expanded only once, regardless of the number of keys in the delegate entry.
Other Updates
RustLab Presentation
Next month in November, I will be presenting about CGP at RustLab in Florence. The presentation is titled How to Stop Fighting with Coherence and Start Writing Context-Generic Trait Impls.
If you are interested in attending, you can use the discount code SP20FR
for a 20% discount.
cgp-serde
Alongside the RustLab presentation, I am working on cgp-serde
, which provides an extensible version of the popular serde
crate. Here is a sneak preview of its capabilities.
The crate offers context-generic versions of the Serialize
and Deserialize
traits:
#[cgp_component {
provider: ValueSerializer,
derive_delegate: UseDelegate<Value>,
}]
pub trait CanSerializeValue<Value: ?Sized> {
fn serialize<S>(&self, value: &Value, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer;
}
#[cgp_component {
provider: ValueDeserializer,
derive_delegate: UseDelegate<Value>,
}]
pub trait CanDeserializeValue<'de, Value> {
fn deserialize<D>(&self, deserializer: D) -> Result<Value, D::Error>
where
D: serde::Deserializer<'de>;
}
The cgp-serde
traits remain compatible with the original serde
traits. This allows reuse of existing serde
implementations without reimplementing them for cgp-serde
.
In addition, cgp-serde
allows customizing how specific field types are serialized. For example, Vec<u8>
or Datetime
can be serialized in a custom manner without being restricted by trait coherence rules.
Another key feature of cgp-serde
is that it enables the use of context and capabilities patterns with serde
. For instance, here is a provider implementation that allocates memory for a value type using an arena allocator provided by the context:
#[cgp_new_provider]
impl<'de, 'a, Context, Value> ValueDeserializer<'de, Context, &'a Value>
for DeserializeAndAllocate
where
Context: CanAlloc<'a, Value> + CanDeserializeValue<'de, Value>,
{
fn deserialize<D>(context: &Context, deserializer: D) -> Result<&'a Value, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = context.deserialize(deserializer)?;
let value = context.alloc(value);
Ok(value)
}
}
This allows the implementation of a deserialization context that provides an arena allocator and uses it to deserialize into a &'a Value
.
An example of a deserializer context with custom deserializer providers is shown below:
#[derive(CgpData)]
pub struct Payload { ... }
#[cgp_context]
#[derive(HasField)]
pub struct App<'a> {
pub arena: &'a Arena<Payload>,
}
delegate_components! {
AppComponents {
...,
ValueDeserializerComponent:
UseDelegate<new DeserializeComponents {
u64:
UseSerde,
Vec<u8>:
DeserializeHex,
Payload:
DeserializeRecordFields,
<'a> &'a Payload:
DeserializeAndAllocate,
...
}>,
}
}
As we can see in the above example, within the wiring for ValueDeserializerComponent
, we use UseDelegate
to create a table lookup for deserialization implementations corresponding to different value types. First, UseSerde
is used to implement deserialization via the original Deserialize
trait from serde
. After that, DeserializeHex
handles the conversion of a hex string into a Vec<u8>
.
Next, DeserializeRecordFields
is applied to deserialize each field in the Payload
struct using their respective value deserializers. This functionality is enabled by the #[derive(CgpData)]
attribute on Payload
. The example also illustrates that it is not necessary to derive any serialization traits on Payload
to make it work with cgp-serde
.
Finally, DeserializeAndAllocate
is used to deserialize a &'a Payload
value by allocating the payload in the arena allocator provided by the context.
There are many additional details that will be explained further with the official release of cgp-serde
. The crate is nearly ready, with the remaining work focused on documentation. I will also demonstrate cgp-serde
at RustLab. If you are interested in learning more, join the conference to see it in action.
Acknowledgement
Thank you April Gonçalves, Abhishek Tripathi and Dzmitry Lahoda for sponsoring the development of CGP!