typed_use_cases 0.1.2

Formalize use cases at the type level. Zero runtime overhead. Experimental proof-of-concept.
Documentation

typed_use_cases

Formalize use cases at the type level. Zero runtime overhead.

A Rust library that brings UML use cases into your type system, providing compile-time awareness when your code structure changes in ways that affect declared use cases.

⚠️ Experimental: This is a proof-of-concept library. It has not been validated in production environments and should be considered an exploratory tool for representing use cases in type systems.


The Problem

In traditional development, use cases live outside the code:

  • Maintained manually: Use case diagrams and specifications are separate documents
  • Silent drift: When code changes, no one remembers to update the use cases
  • Becomes stale: Documentation diverges from implementation over time
  • Poorly defined: Use cases often end up ambiguous, generic, or unmeasurable
  • Abandoned: Teams stop using use cases because they're too hard to maintain

The fundamental issue: use cases and code evolve independently with no connection between them.


What This Library Does

typed_use_cases brings use cases into your type system with zero runtime cost:

  • Formalizes use cases as named traits in Rust
  • Compile-time awareness: The compiler knows which use cases exist
  • Breaking changes are visible: If you change a method signature or remove an implementation, the compiler breaks at the use case level
  • No runtime overhead: Uses a zero-sized System type (0 bytes)
  • Verification in tests only: Uses #[cfg(test)] to verify implementations

What This Library Is NOT

This library is not:

  • NOT a verification tool - It does not prove your program satisfies a use case
  • NOT a testing framework - It does not test that your system fulfills use case requirements (this is not feasible to automate)
  • NOT a formal methods tool - It does not prove program properties
  • NOT for runtime enforcement - All verification happens at compile time, in test builds only

What This Library IS

This library is:

  • A type-level representation - Makes your type system aware of declared use cases
  • A compile-time alarm - Alerts you when changes might violate use case contracts
  • A documentation tool - Use cases live in code, not separate documents
  • A proof of concept - An experiment to see if type-level use cases provide value

You can think of it as: "The compiler knows which use cases should exist, and will complain if you break their structure."

It does not guarantee that your implementation is correct or complete, only that the structure exists.


Possible Use Cases for This Library

1. LLM Context Enhancement

This library may be useful for Large Language Models (LLMs) to:

  • Parse use case declarations directly from code
  • Use use cases as a source of truth when generating or modifying code
  • Better understand system intent and behavior contracts

2. Documentation Generation

Use case metadata (NAME, DESCRIPTION) is accessible at compile time and can be:

  • Extracted for automatic documentation
  • Used to generate diagrams
  • Displayed in developer tools

3. Architectural Awareness

During refactoring or feature development:

  • The type system alerts you when changes affect use cases
  • Use case traits act as stable interfaces
  • Easier to see which parts of the system implement which business logic

The Solution

typed_use_cases makes use cases first-class citizens in your type system:

use typed_use_cases::{Actor, Entity, UseCase};

// 1. Declare actors and entities
#[derive(Actor)]
struct Authenticated { user_id: u64 }

#[derive(Entity)]
struct Cart {
    owner: Authenticated,
    items: Vec<u64>,
}

// 2. Declare use cases as named traits
trait AddItemToCart: UseCase<
    Authenticated,
    Cart,
    Input = Product,
    Output = Result<Cart, String>,
    Dependencies = (Box<dyn InventoryService>, Box<dyn CartRepository>),
> {}

// 3. Define your System type (application-specific, not part of the library)
struct System;  // Zero-sized type

// 4. Implement use cases on your System
impl UseCase<Authenticated, Cart> for System {
    const NAME: &'static str = "Add item to cart";
    const DESCRIPTION: &'static str = "An authenticated user can add a product to their cart";
    
    type Input = Product;
    type Output = Result<Cart, String>;
    type Dependencies = (Box<dyn InventoryService>, Box<dyn CartRepository>);

    fn satisfy(
        actor: Authenticated,
        cart: Cart,
        product: Product,
        deps: Self::Dependencies,
    ) -> Self::Output {
        let (inventory, cart_repo) = deps;
        // Implementation here...
        Ok(cart)
    }
}

impl AddItemToCart for System {}

// 5. Verify at compile time (in tests) that all use cases are implemented
typed_use_cases::implement_all_use_cases!(System: [
    AddItemToCart,
    // The compiler will complain if you declare a use case but forget to implement it
]);

Result: The type system is aware of your use cases. If you change a signature or remove an implementation, compilation fails in tests.

Important: The System type is defined by you, not by this library. It represents your application's use case implementations.


How It Works

Zero-Sized Type (ZST)

System is a zero-sized type — it occupies 0 bytes at runtime:

assert_eq!(std::mem::size_of::<System>(), 0);

The System type exists purely for the compiler. It acts as a witness that your use cases are implemented.

Compile-Time Verification

The implement_all_use_cases! macro expands only in test builds:

#[cfg(test)]
mod __use_cases_verification {
    static_assertions::assert_impl_all!(System: AddItemToCart);
    static_assertions::assert_impl_all!(System: Checkout);
    // ... one assertion per use case
}

This generates zero runtime code. The assertions run at compile time during cargo test.

No Controller Changes

Your HTTP handlers and controllers remain unchanged. The library is purely a compile-time concern:

// Your controller code - unchanged
async fn add_to_cart_handler(req: Request) -> Response {
    let actor = authenticate(&req)?;
    let cart = load_cart(actor.user_id)?;
    let product = parse_product(&req)?;
    
    // Call the use case execution logic
    let updated_cart = add_item_to_cart_logic(actor, cart, product);
    
    save_cart(&updated_cart)?;
    Response::ok()
}

The System type and trait implementations serve as documentation and verification, not as runtime infrastructure.


Install

Using cargo (recommended):

cargo add typed_use_cases

Or add to your Cargo.toml:

[dependencies]
typed_use_cases = "0.1"

That's it! No additional dependencies needed.

Note: static_assertions is only needed for the compile-time verification macro and does not appear in release builds.


Concepts

Actor

An actor is the initiator of a use case. Actors exist independently of any entity or action.

#[derive(Actor)]
struct Anonymous;

#[derive(Actor)]
struct Authenticated { user_id: u64 }

Entity

An entity is what a use case operates on — the subject of the action.

#[derive(Entity)]
struct Catalog {
    products: Vec<Product>,
}

#[derive(Entity)]
struct Cart {
    owner: Authenticated,
    items: Vec<u64>,
}

DependentEntity

A dependent entity is an entity whose existence is tied to a specific actor (e.g., a Cart belongs to an Authenticated user).

The #[derive(Entity)] macro automatically implements DependentEntity<A> if:

  • A field named owner exists
  • Its type is a single-segment path (e.g., Authenticated)
#[derive(Entity)]
struct Cart {
    owner: Authenticated,  // Automatically generates DependentEntity<Authenticated>
    items: Vec<u64>,
}

Manual implementation:

impl DependentEntity<Authenticated> for Cart {
    fn owner(&self) -> &Authenticated {
        &self.owner
    }
}

UseCase

A use case IS the action. It's declared as a named trait that extends UseCase<Actor, Entity> and fixes all type parameters:

trait AddItemToCart: UseCase<
    Authenticated,       // Who initiates
    Cart,                // What it operates on
    Input = Product,     // Additional input data
    Output = Cart,       // What it produces
    Dependencies = (),   // External services needed
> {}

Each use case has compile-time metadata:

impl UseCase<Authenticated, Cart> for System {
    const NAME: &'static str = "Add item to cart";
    const DESCRIPTION: &'static str = "An authenticated user can add a product to their cart";
    
    // ... implementation
}

System

A zero-sized type defined by the user (not by the library) that implements all use cases. It exists only for the compiler and occupies 0 bytes at runtime:

struct System;  // Your application defines this

impl UseCase<Anonymous, Catalog> for System { /* ... */ }
impl UseCase<Authenticated, Cart> for System { /* ... */ }

Important: The System type belongs to your application, not to the typed_use_cases library. Each application defines its own System type to implement its use cases.

implement_all_use_cases!

A macro that verifies (at compile time, in test builds only) that System implements every declared use case:

typed_use_cases::implement_all_use_cases!(System: [
    BrowseCatalog,
    AddItemToCart,
    Checkout,
]);

If any use case is missing, compilation fails during cargo test.


Example: E-Commerce

See examples/ecommerce.rs for a complete working example with:

  • 3 actors: Anonymous, Registered, Authenticated
  • 4 entities: Catalog, Product, Cart, Order
  • 3 use cases: BrowseCatalog, AddItemToCart, Checkout

Run it with:

cargo run --example ecommerce
cargo test --example ecommerce

Design Principles

  • Zero runtime overhead: System is a ZST. No allocation, no dynamic dispatch.
  • Compile-time verification: Use cases are checked at compile time, not runtime.
  • No framework lock-in: Compatible with any web framework, DI container, or async runtime.
  • No boilerplate in production: Controllers remain unchanged. This is purely additive.
  • Stable Rust: No nightly features required.

FAQ

Does this work with async?

The UseCase::execute signature is synchronous by default. For async use cases:

  1. Use the trait as a contract witness only (for compile-time verification)
  2. Implement your actual async logic separately
  3. Or wrap the execution in an async block

Does this impose a dependency injection pattern?

No. Dependencies is a free associated type. Use () if you have no dependencies, or pass in any type you want (a struct, a trait object, a tuple of services, etc.).

What about multiple use cases with the same Actor + Entity?

Each UseCase<A, E> implementation must be unique. If you have two use cases with the same actor and entity types, use different entity types (e.g., Cart vs Order) or create wrapper types.

Does static_assertions appear in my release binary?

No. The implement_all_use_cases! macro expands only under #[cfg(test)], and static_assertions is listed as a # No additional dependencies needed!.


License

MIT


Contributing

Contributions welcome! Please open an issue or pull request on GitHub.