Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generated API Reference

krucyfiks has two code-generation entry points:

  • #[krucyfiks::effect] turns one async capability trait into a leaf protocol.
  • #[derive(krucyfiks::Effect)] turns a single-field tuple enum into a composed protocol.

Generated item visibility follows the input item. A pub trait Random produces public generated enums and traits. A private trait produces private generated items. The only generated item that is always private is the trait-adapter struct named like RandomEffectTraitHandler<T>.

Supported Macro Input

#[krucyfiks::effect] supports async traits whose methods use &self receivers and owned request arguments. Return values may be owned values, (), tuples, or Result<T, E>.

Trait type parameters are supported, including bounds, defaults, and where clauses that constrain type parameters. The macro intentionally rejects method generics, lifetimes, associated types, associated constants, const generics, method where clauses, receivers other than &self, and borrowed request data.

Generated code refers to the runtime crate as ::krucyfiks by default. If a downstream crate renames the dependency or uses a re-exported macro, pass the runtime path explicitly:

#![allow(unused)]
fn main() {
#[kx::effect(crate = "::kx")]
#[async_trait::async_trait]
pub trait Logger: Send + Sync {
    async fn log(&self, message: String);
}

#[derive(Debug, Clone, PartialEq, kx::Effect)]
#[krucyfiks(crate = "::kx")]
pub enum AppEffect {
    Logger(LoggerEffect),
}
}

Leaf Effects

Start with a capability trait:

#![allow(unused)]
fn main() {
#[krucyfiks::effect]
#[async_trait::async_trait]
pub trait Random: Send + Sync {
    async fn get_number(&self, from: i32, to: i32) -> i32;
    async fn reset(&self);
}
}

The macro keeps the trait and generates the items below.

RandomEffect

Generated shape:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
pub enum RandomEffect {
    GetNumber { from: i32, to: i32 },
    Reset,
}
}

Use it as the request protocol at boundaries. Tests, debuggers, FFI layers, and direct handlers can inspect this enum instead of trying to call a Rust trait object.

#![allow(unused)]
fn main() {
let pending = random_handler.next().await?;

assert_eq!(
    pending.request,
    RandomEffect::GetNumber { from: 1, to: 10 },
);
}

Each async method becomes one PascalCase variant. Identifier arguments become owned struct fields. Borrowed arguments are rejected because effects may cross async, thread, FFI, or debugger boundaries.

RandomEffectOutput

Generated shape:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
pub enum RandomEffectOutput {
    GetNumberDone(i32),
    ResetDone,
}
}

Use it as the response protocol for RandomEffect.

#![allow(unused)]
fn main() {
pending
    .respond_async(RandomEffectOutput::GetNumberDone(42))
    .await?;
}

Return values become payloads on MethodDone variants. Methods returning () or omitting a return type get a unit-like MethodDone variant.

impl Effect for RandomEffect

Generated shape:

#![allow(unused)]
fn main() {
impl krucyfiks::Effect for RandomEffect {
    type Output = RandomEffectOutput;
}
}

This links the request enum to the output enum. Core adapters such as EffectChannel<RandomEffect>, EffectHandler<RandomEffect>, and composed protocols depend on this association.

impl Protocol and impl ProtocolLeaf

Generated shape:

#![allow(unused)]
fn main() {
impl krucyfiks::protocol::Protocol for RandomEffect {
    type Path<Leaf: krucyfiks::Effect + 'static> =
        krucyfiks::protocol::SelfPath<Leaf>;
    /* extract and complete */
}

impl<Leaf> krucyfiks::protocol::ProtocolLeaf<Leaf> for RandomEffect
where
    Leaf: krucyfiks::Effect + 'static,
{
    /* inject_leaf and project_leaf */
}
}

Use these indirectly. They are the base case that lets a composed protocol eventually extract RandomEffect from a larger AppEffect, then complete the matching RandomEffectOutput back through the same path.

#![allow(unused)]
fn main() {
handler
    .handle_get_number(async |from, to| from + to)
    .await?;
}

In that example handler may be an EffectChannelHandler<AppEffect>. The generated channel helper uses Protocol::extract::<RandomEffect> before calling the closure and Protocol::complete::<RandomEffect> after it returns.

Generated EffectSink Adapter

Use EffectSink when a test or runtime wants a capability implementation that can ignore unit-returning effects such as logging or rendering.

#![allow(unused)]
fn main() {
let logger = krucyfiks::effect_sink::EffectSink::unbounded();
logger.log("ignored in this test".to_owned()).await;
}

EffectSink can also be used with mixed capabilities. Unit-returning methods are acknowledged immediately, while value-returning methods are routed through the sink’s internal channel. Keep sink.handler() and answer those requests with the generated handle_* methods.

#![allow(unused)]
fn main() {
let random = krucyfiks::effect_sink::EffectSink::unbounded();
let random_handler = random.handler();

let mut call = Box::pin(random.get_number(20, 22));
krucyfiks::assert_pending!(&mut call);

random_handler
    .handle_get_number(async |from, to| from + to)
    .await?;
}

RandomHandler

Generated shape:

#![allow(unused)]
fn main() {
#[allow(async_fn_in_trait)]
pub trait RandomHandler {
    async fn handle_get_number(
        &self,
        f: impl AsyncFnMut(i32, i32) -> i32,
    ) -> Result<(), krucyfiks::GeneratedHandlerError>;

    async fn handle_reset(
        &self,
        f: impl AsyncFnMut(),
    ) -> Result<(), krucyfiks::GeneratedHandlerError>;
}
}

Generated impl:

#![allow(unused)]
fn main() {
impl<E> RandomHandler for krucyfiks::effect_channel::EffectChannelHandler<E>
where
    E: krucyfiks::protocol::Protocol + Send + 'static,
    E::Output: Send + 'static,
{
    /* receives, extracts, handles, completes */
}
}

Use it in tests and scripted Rust hosts when you want to handle the next queued effect by method name.

#![allow(unused)]
fn main() {
let (random, random_handler) = krucyfiks::effect_channel::EffectChannel::unbounded();

let mut call = Box::pin(random.get_number(20, 22));
krucyfiks::assert_pending!(&mut call);

random_handler
    .handle_get_number(async |from, to| from + to)
    .await?;

assert_eq!(call.await, 42);
}

GeneratedHandlerError is HandleError<ProtocolError>. It reports channel shutdown and wrong-branch protocol reads instead of panicking.

RandomRequestHandler

Generated shape:

#![allow(unused)]
fn main() {
#[allow(async_fn_in_trait)]
pub trait RandomRequestHandler {
    async fn handle_get_number(
        self,
        f: impl AsyncFnMut(i32, i32) -> i32,
    ) -> Result<RandomEffectOutput, krucyfiks::ProtocolError>;
}

impl RandomRequestHandler
    for krucyfiks::effect_request::EffectRequestHandler<RandomEffect>
{
    /* checks the request variant and builds RandomEffectOutput */
}
}

Use it when you already own one RandomEffect value and want to handle exactly one expected method without using a channel.

#![allow(unused)]
fn main() {
let output = RandomRequestHandler::handle_get_number(
    krucyfiks::effect_request::EffectRequestHandler::new(
        RandomEffect::GetNumber { from: 20, to: 22 },
    ),
    async |from, to| from + to,
)
.await?;

assert_eq!(output, RandomEffectOutput::GetNumberDone(42));
}

If the request is another variant, this returns ProtocolError::WrongBranch.

RandomEffectHandler

Generated shape:

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait RandomEffectHandler {
    async fn handle_effect(&self, effect: RandomEffect) -> RandomEffectOutput;
}
}

Use it for direct production-style handling of a leaf protocol. The handler takes plain request data and returns the matching plain output data.

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

#[async_trait::async_trait]
impl RandomEffectHandler for FixedRandom {
    async fn handle_effect(&self, effect: RandomEffect) -> RandomEffectOutput {
        match effect {
            RandomEffect::GetNumber { from, to } => {
                RandomEffectOutput::GetNumberDone(from + to)
            }
            RandomEffect::Reset => RandomEffectOutput::ResetDone,
        }
    }
}
}

The composed handler builder also accepts anything that can become an Arc<dyn RandomEffectHandler + Send + Sync>.

RandomEffectHandlerInput

Generated shape:

#![allow(unused)]
fn main() {
pub trait RandomEffectHandlerInput {
    fn into_effect_handler(
        self,
    ) -> std::sync::Arc<dyn RandomEffectHandler + Send + Sync>;
}
}

Use it indirectly through generated builders:

#![allow(unused)]
fn main() {
let counter = CounterEffect::handler()
    .with_random(FixedRandom)
    .with_render(krucyfiks::effect_sink::EffectSink::unbounded())
    .build()?;
}

The macro implements this trait for any concrete T that implements the original capability trait:

#![allow(unused)]
fn main() {
impl<T> RandomEffectHandlerInput for T
where
    T: Random + Send + Sync + 'static,
{
    /* wraps T in RandomEffectTraitHandler<T> */
}
}

This is why with_random(FixedRandom) works even when FixedRandom implements Random, not RandomEffectHandler.

RandomEffectTraitHandler<T>

Generated shape:

#![allow(unused)]
fn main() {
struct RandomEffectTraitHandler<T> {
    inner: T,
}
}

This struct is private generated glue. It adapts a concrete implementation of the original trait into RandomEffectHandler.

You should not name or construct it. Use RandomEffectHandlerInput through with_random(...) or call into_effect_handler() when a direct conversion is really needed.

Generated Handler Implementations

The macro also generates these impls:

#![allow(unused)]
fn main() {
impl RandomEffectHandler for krucyfiks::effect_channel::EffectChannel<RandomEffect> { /* ... */ }
impl RandomEffectHandler for krucyfiks::effect_sink::EffectSink<RandomEffect> { /* ... */ }
impl<T> RandomEffectHandler for std::sync::Arc<T>
where
    T: RandomEffectHandler + Send + Sync + ?Sized,
{
    /* ... */
}
impl<Handler, Scope> RandomEffectHandler
    for krucyfiks::protocol::ScopedEffectHandler<Handler, Scope>
where
    Scope: krucyfiks::protocol::EffectScope<RandomEffect> + Send + Sync,
{
    /* ... */
}
}

These impls make the common adapters interchangeable:

#![allow(unused)]
fn main() {
let channel: std::sync::Arc<dyn Random> =
    krucyfiks::effect_channel::EffectChannel::<RandomEffect>::new().0;

let scoped: std::sync::Arc<dyn Random> =
    std::sync::Arc::new(AppEffect::counter_scope(root_handler));
}

The scoped impl is fail-fast: it panics if the root handler returns an output that cannot be projected back to the requested leaf. Use ScopedEffectHandler::try_handle directly when you want a ProtocolError instead.

Blanket impl Random for T

Generated shape:

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
impl<T> Random for T
where
    T: RandomEffectHandler + Send + Sync,
{
    async fn get_number(&self, from: i32, to: i32) -> i32 {
        let out = self
            .handle_effect(RandomEffect::GetNumber { from, to })
            .await;

        match out {
            RandomEffectOutput::GetNumberDone(result) => result,
            _ => panic!("unexpected output variant"),
        }
    }
}
}

This is the adapter that lets the app keep depending on Arc<dyn Random> while tests and runtimes provide generated effect handlers.

Generic Leaf Effects

For generic traits, krucyfiks only puts type parameters on generated request or output enums that actually need them.

If a parameter appears in a request argument:

#![allow(unused)]
fn main() {
#[krucyfiks::effect]
#[async_trait::async_trait]
pub trait Cache<T = String>: Send + Sync {
    async fn put(&self, value: T);
}
}

the request enum is generic:

#![allow(unused)]
fn main() {
pub enum CacheEffect<T = String> {
    Put { value: T },
}
}

If a parameter appears only in an output:

#![allow(unused)]
fn main() {
#[krucyfiks::effect]
#[async_trait::async_trait]
pub trait Foo<T = String>: Send + Sync {
    async fn get(&self) -> T;
}
}

the request enum stays non-generic and the output enum is generic:

#![allow(unused)]
fn main() {
pub enum FooEffect {
    Get,
}

pub enum FooEffectOutput<T = String> {
    GetDone(T),
}
}

Output-only generics are represented through the runtime context parameter:

#![allow(unused)]
fn main() {
impl<T> krucyfiks::Effect<T> for FooEffect {
    type Output = FooEffectOutput<T>;
}
}

Generated public declarations preserve type bounds and defaults where Rust allows them. Generated impl and function generics strip defaults because Rust does not allow defaults in those positions.

Composed Effects

Start with a single-field tuple enum whose fields are effect types:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, krucyfiks::Effect)]
pub enum CounterEffect {
    Random(RandomEffect),
    Render(RenderEffect),
}

#[derive(Debug, Clone, PartialEq, krucyfiks::Effect)]
pub enum AppEffect {
    Account(AccountEffect),
    Counter(CounterEffect),
}
}

The derive macro keeps the enum and generates the items below.

Composed enums may use type parameters with bounds and defaults. If a child effect needs an output-only generic parameter, declare it with #[krucyfiks(output_param(...))]:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, krucyfiks::Effect)]
#[krucyfiks(output_param(T: Clone = String))]
pub enum AppEffect {
    #[krucyfiks(ctx = T)]
    Foo(FooEffect),
}
}

output_param(T) adds T to generated output-side items such as AppEffectOutput<T>, handler traits, and handler builders. ctx = T tells the derive macro to use the child branch as Effect<T>. Without ctx, child branches default to Effect<()>.

Multiple output params may be declared separately or grouped:

#![allow(unused)]
fn main() {
#[krucyfiks(output_param(T), output_param(U))]
#[krucyfiks(output_param(T: Clone, U: std::fmt::Debug))]
}

AppEffectOutput

Generated shape:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
pub enum AppEffectOutput {
    Account(<AccountEffect as krucyfiks::Effect>::Output),
    Counter(<CounterEffect as krucyfiks::Effect>::Output),
}
}

Use it as the response protocol for AppEffect.

#![allow(unused)]
fn main() {
let output = AppEffectOutput::Counter(
    CounterEffectOutput::Random(RandomEffectOutput::GetNumberDone(7)),
);
}

Each output branch mirrors the request branch. A handler for AppEffect::Counter(...) must return AppEffectOutput::Counter(...).

AppEffectPath<Leaf>

Generated shape:

#![allow(unused)]
fn main() {
pub enum AppEffectPath<Leaf>
where
    Leaf: krucyfiks::Effect + 'static,
{
    Account(<AccountEffect as krucyfiks::protocol::Protocol>::Path<Leaf>),
    Counter(<CounterEffect as krucyfiks::protocol::Protocol>::Path<Leaf>),
}
}

Use it indirectly. It is the receipt returned by Protocol::extract and later consumed by Protocol::complete.

#![allow(unused)]
fn main() {
let (leaf, path) =
    <AppEffect as krucyfiks::protocol::Protocol>::extract::<RandomEffect>(effect)?;

let output =
    <AppEffect as krucyfiks::protocol::Protocol>::complete::<RandomEffect>(
        path,
        RandomEffectOutput::GetNumberDone(7),
    );
}

This path is what keeps duplicate leaf effects sound. If both account and counter contain RenderEffect, the path records which branch produced this particular render request.

AppEffectAccountScope

Generated shape for each branch:

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

impl<Leaf> krucyfiks::protocol::EffectScope<Leaf> for AppEffectAccountScope
where
    Leaf: krucyfiks::Effect + 'static,
    AccountEffect: krucyfiks::protocol::ProtocolLeaf<Leaf>,
{
    type Root = AppEffect;
    /* inject and project */
}

impl krucyfiks::protocol::EffectBranch<AccountEffect> for AppEffectAccountScope {
    type Root = AppEffect;
    /* inject_child and project_child */
}
}

Use the generated constructor instead of naming the scope directly:

#![allow(unused)]
fn main() {
let account_effects = std::sync::Arc::new(AppEffect::account_scope(root.clone()));
let account_render: std::sync::Arc<dyn Render> = account_effects;
}

The scope means “route leaf requests through AppEffect::Account.” It matters when the same leaf appears in more than one branch.

Name the struct only in advanced type signatures or when building your own adapter around ScopedEffectHandler or BranchEffectHandler.

impl Effect for AppEffect

Generated shape:

#![allow(unused)]
fn main() {
impl krucyfiks::Effect for AppEffect {
    type Output = AppEffectOutput;
}
}

This makes the composed enum usable anywhere an effect protocol is expected: channels, direct handlers, hooks, and parent composed protocols.

impl Protocol for AppEffect

Generated shape:

#![allow(unused)]
fn main() {
impl krucyfiks::protocol::Protocol for AppEffect {
    type Path<Leaf: krucyfiks::Effect + 'static> = AppEffectPath<Leaf>;

    fn extract<Leaf>(self) -> Result<(Leaf, Self::Path<Leaf>), Self> { /* ... */ }

    fn complete<Leaf>(path: Self::Path<Leaf>, output: Leaf::Output) -> Self::Output {
        /* ... */
    }
}
}

Use it indirectly through generated channel helpers and scoped handlers. Use it directly when writing custom host dispatch code that peels one leaf request out of a root protocol.

impl ProtocolLeaf for AppEffect

Generated shape:

#![allow(unused)]
fn main() {
impl<Leaf> krucyfiks::protocol::ProtocolLeaf<Leaf> for AppEffect
where
    Leaf: krucyfiks::Effect + 'static,
    AccountEffect: krucyfiks::protocol::ProtocolLeaf<Leaf>,
    CounterEffect: krucyfiks::protocol::ProtocolLeaf<Leaf>,
{
    /* inject_leaf and project_leaf */
}
}

Use it indirectly when a parent protocol or a scope needs to inject a leaf through nested composed enums.

#![allow(unused)]
fn main() {
let render = TuiAppEffect::domain_scope(root_handler);
render.render().await;
}

That call can inject RenderEffect through TuiAppEffect::Domain, then through the matching branch inside AppEffect.

AppEffectHandler

Generated shape:

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait AppEffectHandler {
    async fn handle_effect(&self, effect: AppEffect) -> AppEffectOutput;
}
}

Use it for direct handling of the composed protocol, especially inside generated builders. Most production code can use the core trait krucyfiks::effect_handler::EffectHandler<AppEffect> instead.

AppEffectHandlerInput

Generated shape:

#![allow(unused)]
fn main() {
pub trait AppEffectHandlerInput {
    fn into_effect_handler(
        self,
    ) -> std::sync::Arc<dyn AppEffectHandler + Send + Sync>;
}
}

Use it indirectly through parent builders. It lets a parent branch accept any concrete handler that already implements AppEffectHandler.

AppEffectHandlerBuilder

Generated shape:

#![allow(unused)]
fn main() {
pub struct AppEffectHandlerBuilder {
    account: Option<std::sync::Arc<dyn AccountEffectHandler + Send + Sync>>,
    counter: Option<std::sync::Arc<dyn CounterEffectHandler + Send + Sync>>,
}
}

Fields are private. Use AppEffect::handler() and the generated with_* methods.

#![allow(unused)]
fn main() {
let app_effects = AppEffect::handler()
    .with_account(account_handler)
    .with_counter(counter_handler)
    .build()?;
}

This is the usual production assembly path when each branch already has its own handler.

with_account and with_counter

Generated shape:

#![allow(unused)]
fn main() {
impl AppEffectHandlerBuilder {
    pub fn with_account<Handler>(mut self, handler: Handler) -> Self
    where
        Handler: AccountEffectHandlerInput,
    {
        /* stores Arc<dyn AccountEffectHandler + Send + Sync> */
    }
}
}

Use one with_* call for every branch before build().

#![allow(unused)]
fn main() {
let counter = CounterEffect::handler()
    .with_random(FixedRandom)
    .with_render(krucyfiks::effect_sink::EffectSink::unbounded())
    .build()?;
}

Missing branches are reported as BuildError::MissingBranch.

AppEffectBuiltHandler

Generated shape:

#![allow(unused)]
fn main() {
pub struct AppEffectBuiltHandler {
    account: std::sync::Arc<dyn AccountEffectHandler + Send + Sync>,
    counter: std::sync::Arc<dyn CounterEffectHandler + Send + Sync>,
}

#[async_trait::async_trait]
impl AppEffectHandler for AppEffectBuiltHandler { /* ... */ }

#[async_trait::async_trait]
impl krucyfiks::effect_handler::EffectHandler<AppEffect>
    for AppEffectBuiltHandler
{
    /* ... */
}
}

Use the value returned by build() as the root effect handler:

#![allow(unused)]
fn main() {
let root = std::sync::Arc::new(app_effects);
let app = TeaApp::new(root);
}

It dispatches by request branch and wraps each child output in the matching AppEffectOutput branch.

AppEffect::handler()

Generated shape:

#![allow(unused)]
fn main() {
impl AppEffect {
    pub fn handler() -> AppEffectHandlerBuilder {
        /* empty builder */
    }
}
}

Use it to start composing branch handlers.

AppEffect::account_scope(handler)

Generated shape:

#![allow(unused)]
fn main() {
impl AppEffect {
    pub fn account_scope<Handler>(
        handler: Handler,
    ) -> krucyfiks::protocol::ScopedEffectHandler<
        Handler,
        AppEffectAccountScope,
    > {
        krucyfiks::protocol::ScopedEffectHandler::new(handler)
    }
}
}

Use it when app internals need a leaf capability trait, but the boundary owns a root handler.

#![allow(unused)]
fn main() {
let account_effects = std::sync::Arc::new(AppEffect::account_scope(root.clone()));
let account_render: std::sync::Arc<dyn Render> = account_effects;
}

The returned scoped handler can implement generated leaf capability traits such as Render because #[krucyfiks::effect] generated impl<T> Render for T where T: RenderEffectHandler + Send + Sync.

AppEffect::account_handler(handler)

Generated shape:

#![allow(unused)]
fn main() {
impl AppEffect {
    pub fn account_handler<Handler>(
        handler: Handler,
    ) -> krucyfiks::protocol::BranchEffectHandler<
        Handler,
        AppEffectAccountScope,
    > {
        krucyfiks::protocol::BranchEffectHandler::new(handler)
    }
}
}

Use it when the caller already speaks the direct child protocol.

#![allow(unused)]
fn main() {
let domain = TeaApp::new(std::sync::Arc::new(
    TuiAppEffect::domain_handler(root.clone()),
));
}

Here the nested domain app wants an EffectHandler<AppEffect>, while the outer TUI runtime has an EffectHandler<TuiAppEffect>.

Audit Notes

The currently generated surface has these public generated names:

#[effect] Trait:
  TraitEffect
  TraitEffectOutput
  TraitHandler
  TraitRequestHandler
  TraitEffectHandler
  TraitEffectHandlerInput
  TraitEffectTraitHandler<T>        private

#[derive(Effect)] Enum:
  EnumOutput
  EnumPath<Leaf>
  EnumVariantScope                 one per branch
  EnumHandler
  EnumHandlerInput
  EnumHandlerBuilder
  EnumBuiltHandler