clients 0.1.0

Concrete-struct dependency injection for Rust using function pointers instead of trait objects
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# clients

> [!NOTE]
> This is a POC **fully** implemented by ChatGPT 5.4 with
> steering from Alex Cyon looking for a simpler DI 
> solution for Rust - inspired by [`swift-dependencies`]https://github.com/pointfreeco/swift-dependencies
> Also this README (except this NOTE) has been generated by AI.


`clients` is a concrete-struct dependency injection library for Rust.

The core idea is simple:

- dependencies are plain structs, not traits
- each dependency method is backed by a raw function pointer
- production code resolves concrete clients from `Dependency::live()`
- tests override individual methods with almost no ceremony

This is intentionally closer to `swift-dependencies` than to traditional Rust DI crates built around `Arc<dyn Trait>` or `Box<dyn Trait>`.

## Why this crate exists

Many Rust DI solutions lean on trait objects. That works, but it often adds boilerplate:

- defining traits just for testability
- threading `Arc<dyn Client>` or `Box<dyn Client>` through the app
- writing mock structs or mock frameworks

`clients` takes a different route. A dependency client is a concrete `struct` whose fields are function pointers. Methods call those function pointers. Tests swap pointers in scoped override layers.

The result is:

- concrete dependency types
- direct call sites
- fast, flat tests
- support for sync and async APIs

## Quick start

```rust
use clients::{client, deps, test_deps};

#[derive(Debug, Clone, PartialEq, Eq)]
struct User {
    id: u64,
    name: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum UserClientError {
    Unavailable,
}

client! {
    pub struct UserClient as user_client {
        pub fn fetch_user(id: u64) -> Result<User, UserClientError>;
    }
}

client! {
    pub struct Clock as clock {
        pub fn now_millis() -> u64 = || 0;
    }
}

pub fn greeting_for_user(id: u64) -> Result<String, UserClientError> {
    deps! {
        fetch_user = user_client.fetch_user,
        now = clock.now_millis,
    }

    let user = fetch_user(id)?;
    let now = now();
    Ok(format!("Hello, {} @ {}", user.name, now))
}

#[test]
fn greeting_uses_flat_test_overrides() {
    test_deps! {
        user_client.fetch_user => |id| Ok(User { id, name: "Blob".into() }),
        clock.now_millis => || 1234,
    }

    let result = greeting_for_user(42).unwrap();
    assert_eq!(result, "Hello, Blob @ 1234");
}
```


## How it works

`client!` expands into a concrete struct whose fields are raw function pointers. `get::<Client>()` resolves that concrete value from either `Dependency::live()` or the current override stack, and `deps!` binds the already-erased function pointers into locals for the current scope. `test_deps!` works by cloning the concrete client, swapping one or more function pointers, and pushing that value into a scoped global override layer.

The only subtle part is closure erasure: live implementations are written as non-capturing closures, but stored as plain `fn(...) -> ...` pointers. `clients` handles that with a small family of arity-specific erasers plus one tightly-scoped `unsafe` reconstruction step for zero-sized closure types. For the safety discussion, see [Safety](#safety).

The longer version, including generated code shape, override storage, and a detailed explanation of `define_erasers!`, lives in [HOW_IT_WORKS.md](./HOW_IT_WORKS.md).


## Defining clients

Use the `client!` proc macro to declare a dependency client:

```rust
use clients::client;

#[derive(Debug, Clone, PartialEq, Eq)]
struct User {
    id: u64,
    name: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum UserClientError {
    Unavailable,
}

client! {
    pub struct UserClient as user_client {
        pub fn fetch_user(id: u64) -> Result<User, UserClientError>;
    }
}
```

This generates:

- a concrete `UserClient` struct
- methods like `user_client.fetch_user`
- a `Dependency` implementation that resolves the live client
- a helper module named `user_client`
- nested helper modules like `user_client::fetch_user`

If you omit the implementation on a method, calling it without a test override panics with a descriptive error such as `UserClient.fetch_user`. That is useful when you want tests to provide the implementation explicitly.

## Using dependencies in global functions

The `deps!` macro binds dependency methods to local names:

```rust
use clients::{DependencyError, client, deps};

client! {
    struct Clock as clock {
        fn now_millis() -> u64 = || 1234;
    }
}

fn now_string() -> Result<String, DependencyError> {
    deps! {
        now = clock.now_millis,
    }

    Ok(now().to_string())
}

assert_eq!(now_string().unwrap(), "1234");
```

This is especially useful in free functions where you do not want to thread a container or context object through the call stack.

## Using dependencies as fields

Use `#[derive(Depends)]` plus `#[dep]` to build structs from dependencies:

```rust
use clients::{DependencyError, Depends, client};

#[derive(Debug, Clone, PartialEq, Eq)]
struct User {
    id: u64,
    name: String,
}

client! {
    struct UserClient as user_client {
        fn fetch_user(id: u64) -> Result<User, DependencyError> = |id| {
            Ok(User { id, name: format!("User {id}") })
        };
    }
}

client! {
    struct Clock as clock {
        fn now_millis() -> u64 = || 1234;
    }
}

#[derive(Depends)]
struct Greeter {
    #[dep]
    user_client: UserClient,
    #[dep]
    clock: Clock,
}

impl Greeter {
    fn greeting_for_user(&self, id: u64) -> Result<String, DependencyError> {
        let user = self.user_client.fetch_user(id)?;
        Ok(format!("Hello, {} @ {}", user.name, self.clock.now_millis()))
    }
}

let greeter = Greeter::from_deps();
assert_eq!(greeter.greeting_for_user(7).unwrap(), "Hello, User 7 @ 1234");
```

`Depends` currently generates:

- `Default`, where `#[dep]` fields resolve from the dependency system and all other fields use `Default::default()`
- `from_deps()`, a convenience constructor that forwards to `Default`

## Example app

See [examples/rick_and_morty_cli.rs](./examples/rick_and_morty_cli.rs) for a small binary example that:

- declares an `ApiClient` with `client!`
- uses live `http_client`, `clock`, `env`, `random`, and `filesystem` built-ins
- fetches a character from the public Rick and Morty API
- caches successful responses to the system temp directory

Run it with:

```bash
cargo run --example rick_and_morty_cli --features reqwest -- 1
```

If you do not pass an explicit id, the example picks one from a random byte. You can also set `DEP_EXAMPLE_CHARACTER_ID` to choose the default character id or `RICK_AND_MORTY_BASE_URL` to point the client at another compatible server.

## Async support

Async dependency methods are supported directly:

```rust
use clients::{DependencyError, client, deps, test_deps};

client! {
    struct AsyncClock as async_clock {
        async fn now_millis() -> u64 = || async { 4321 };
    }
}

async fn read_now() -> Result<u64, DependencyError> {
    deps! {
        now = async_clock.now_millis,
    }

    Ok(now().await)
}

test_deps! {
    async_clock.now_millis => || async { 9999 },
}

# fn block_on<F: std::future::Future>(future: F) -> F::Output {
#     use std::pin::Pin;
#     use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
#     let mut future = Box::pin(future);
#     unsafe fn clone(_: *const ()) -> RawWaker { RawWaker::new(std::ptr::null(), &VTABLE) }
#     unsafe fn wake(_: *const ()) {}
#     unsafe fn wake_by_ref(_: *const ()) {}
#     unsafe fn drop(_: *const ()) {}
#     static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
#     let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
#     let mut context = Context::from_waker(&waker);
#     loop {
#         match Pin::as_mut(&mut future).poll(&mut context) {
#             Poll::Ready(value) => break value,
#             Poll::Pending => std::thread::yield_now(),
#         }
#     }
# }
assert_eq!(block_on(read_now()).unwrap(), 9999);
```

## Client-on-client composition

Clients can depend on other clients directly:

```rust
use clients::{DependencyError, client, deps};

client! {
    struct Clock as clock {
        fn now_millis() -> u64 = || 1234;
    }
}

client! {
    struct Formatter as formatter {
        fn formatted_now() -> Result<String, DependencyError> = || {
            deps! {
                now = clock.now_millis,
            }

            Ok(format!("now={}", now()))
        };
    }
}

assert_eq!(formatter::get().formatted_now().unwrap(), "now=1234");
```

## Fine-grained override control

Most tests should use `test_deps!`, but there is also a lower-level API:

```rust
use clients::{OverrideBuilder, client, erase_sync_0, get};

client! {
    struct Clock as clock {
        fn now_millis() -> u64 = || 0;
    }
}

let _test_scope = OverrideBuilder::new().enter_test();

let override_clock = Clock {
    now_millis: erase_sync_0(|| 1234),
};

let _guard = OverrideBuilder::new().set(override_clock).enter();

assert_eq!(get::<Clock>().now_millis(), 1234);
```

This is useful when you want to replace a whole client at once or compute an override from the current client via `OverrideBuilder::update`.

## Built-in clients

`clients` now ships a small built-in client set for common system interactions.

Always available:

- `clock`
- `env`
- `random`
- `filesystem`

Feature-gated:

- `uuid`: `uuid`
- `reqwest`: `http_client`

This keeps the default crate small while still letting you opt into UUID generation and an overridable HTTP client only when you need those dependencies.

## Safety

`clients` uses a small amount of `unsafe`, but it is narrowly scoped. The runtime unsafe is confined to reconstructing zero-sized non-capturing closure values inside the eraser trampolines that turn `|| ...` syntax into plain function pointers. Before that path is taken, the runtime checks `mem::size_of::<F>() == 0`, which rejects capturing closures and keeps the unsafe logic limited to function items and compiler-generated zero-sized closure types.

The rest of the runtime uses ordinary safe Rust building blocks: `TypeId`, `Any`, `RwLock`, and RAII guards.

(The other `unsafe` you may notice in tests or examples is just no-op `RawWaker` boilerplate for polling futures without bringing in an async runtime.)

## Performance

For curious engineers, the most useful comparison is not "DI vs no DI", but "`clients` vs the common `Arc<dyn HttpClientTrait>` style".

On the synchronous path, once you have already resolved the dependency, `clients` is in the same performance class and **can be a bit faster**. A small local no-op microbenchmark on a Macbook Pro M2 Max came out roughly like this:

- resolved `clients` client: about `0.86-0.90 ns` per call
- `deps!` local function binding: about `0.86 ns` per call
- `Arc<dyn HttpClientTrait>` call: about `1.16-1.54 ns` per call

That matches the implementation: `clients` ends up doing a raw function-pointer call, while `Arc<dyn Trait>` does a trait-object call through a vtable. In real HTTP code this difference is usually irrelevant because the network and serialization costs dominate.

There are still real costs to be aware of:

- `get::<D>()` performs a global override lookup guarded by an `RwLock`
- resolved clients are cloned when returned from the override stack
- async dependency methods allocate because they return `BoxFuture`
- override state is process-global and optimized for application glue and tests, not per-iteration hot loops

The place where `clients` does lose is repeated resolution. In the same microbenchmark, calling `get::<D>()` inside the loop was about `12.3 ns` per call, because it pays for the global override lookup on every iteration.

So the practical rule is simple: if you resolve once and then call many times, `clients` is fine and often slightly better than `Arc<dyn Trait>`. If you repeatedly resolve dependencies inside a hot inner loop, `clients` is the wrong shape.

That makes `clients` a poor fit for code such as per-packet networking loops, per-row data processing kernels, schedulers, or other throughput-sensitive inner loops where the dependency method itself does almost no work and the dependency is re-resolved over and over. In those cases, keeping an already-resolved `Arc<dyn HttpClientTrait>` (or another concrete handle) is more suitable because the lookup cost is paid up front and each call is just one dynamic dispatch.

## Supported today

- sync dependency methods
- async dependency methods
- dependency access inside free functions via `deps!`
- dependency access inside structs via `#[derive(Depends)]`
- nested dependency scopes
- low-ceremony test overrides via `test_deps!`
- dependencies implemented in terms of other dependencies
- direct builder-based override control through `OverrideBuilder`

## Current limitations

- live implementations and test overrides must be non-capturing closures or function items
- `client!` currently supports up to 4 method arguments
- `Depends` currently supports simple braced structs and does not handle generics or where-clauses
- override state is process-global rather than task-local
- `test_deps!` serializes scopes within a process so concurrent tests do not trample each other

## Relationship to Rust trait-based DI

`clients` does not replace all trait-based design. Trait objects are still a good fit when you genuinely need polymorphism as part of the domain model.

This crate is for a narrower problem:

- you want ergonomic dependency injection
- you want very light test setup
- you prefer concrete clients
- you do not want to define traits solely for testability

That trade-off is the entire point of the crate.