CGP v0.5.0 Release - Auto dispatchers, extensible datatype improvements, monadic computation, RTN emulation, modular serde, and more
Posted on 2025-10-12
Authored by Soares Chen

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 input Value.
  • 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 owned Value 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!