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