sandly 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sandly
2
2
 
3
- Type-safe dependency injection for TypeScript. No decorators, no runtime reflection, just compile-time safety that catches errors before your code runs.
3
+ Sandly ("Services And Layers") is a type-safe dependency injection library for TypeScript. No decorators, no runtime reflection, just compile-time safety that catches errors before your code runs.
4
4
 
5
5
  ## Why Sandly?
6
6
 
@@ -10,12 +10,16 @@ Most TypeScript DI libraries rely on experimental decorators and runtime reflect
10
10
  import { Container, Layer } from 'sandly';
11
11
 
12
12
  class Database {
13
- query(sql: string) { return []; }
13
+ query(sql: string) {
14
+ return [];
15
+ }
14
16
  }
15
17
 
16
18
  class UserService {
17
- constructor(private db: Database) {}
18
- getUsers() { return this.db.query('SELECT * FROM users'); }
19
+ constructor(private db: Database) {}
20
+ getUsers() {
21
+ return this.db.query('SELECT * FROM users');
22
+ }
19
23
  }
20
24
 
21
25
  // Define layers
@@ -60,24 +64,24 @@ import { Container, Layer, Tag } from 'sandly';
60
64
 
61
65
  // Any class can be a dependency - no special base class needed
62
66
  class Database {
63
- async query(sql: string) {
64
- return [{ id: 1, name: 'Alice' }];
65
- }
66
- async close() {
67
- console.log('Database closed');
68
- }
67
+ async query(sql: string) {
68
+ return [{ id: 1, name: 'Alice' }];
69
+ }
70
+ async close() {
71
+ console.log('Database closed');
72
+ }
69
73
  }
70
74
 
71
75
  class UserRepository {
72
- constructor(private db: Database) {}
73
- findAll() {
74
- return this.db.query('SELECT * FROM users');
75
- }
76
+ constructor(private db: Database) {}
77
+ findAll() {
78
+ return this.db.query('SELECT * FROM users');
79
+ }
76
80
  }
77
81
 
78
82
  // Create layers
79
83
  const dbLayer = Layer.service(Database, [], {
80
- cleanup: (db) => db.close()
84
+ cleanup: (db) => db.close(),
81
85
  });
82
86
 
83
87
  const userRepoLayer = Layer.service(UserRepository, [Database]);
@@ -104,7 +108,9 @@ Tags identify dependencies. There are two types:
104
108
 
105
109
  ```typescript
106
110
  class UserService {
107
- getUsers() { return []; }
111
+ getUsers() {
112
+ return [];
113
+ }
108
114
  }
109
115
 
110
116
  // UserService is both the class and its tag
@@ -118,7 +124,9 @@ const PortTag = Tag.of('Port')<number>();
118
124
  const ConfigTag = Tag.of('Config')<{ apiUrl: string }>();
119
125
 
120
126
  const portLayer = Layer.value(PortTag, 3000);
121
- const configLayer = Layer.value(ConfigTag, { apiUrl: 'https://api.example.com' });
127
+ const configLayer = Layer.value(ConfigTag, {
128
+ apiUrl: 'https://api.example.com',
129
+ });
122
130
  ```
123
131
 
124
132
  ### Container
@@ -131,19 +139,20 @@ const container = Container.from(appLayer);
131
139
 
132
140
  // Or build manually
133
141
  const container = Container.builder()
134
- .add(Database, () => new Database())
135
- .add(UserService, async (ctx) =>
136
- new UserService(await ctx.resolve(Database))
137
- )
138
- .build();
142
+ .add(Database, () => new Database())
143
+ .add(
144
+ UserService,
145
+ async (ctx) => new UserService(await ctx.resolve(Database))
146
+ )
147
+ .build();
139
148
 
140
149
  // Resolve dependencies
141
150
  const db = await container.resolve(Database);
142
151
  const [db, users] = await container.resolveAll(Database, UserService);
143
152
 
144
153
  // Use and discard pattern - resolves, runs callback, then destroys
145
- const result = await container.use(UserService, (service) =>
146
- service.getUsers()
154
+ const result = await container.use(UserService, (service) =>
155
+ service.getUsers()
147
156
  );
148
157
 
149
158
  // Manual clean up
@@ -165,11 +174,12 @@ const configLayer = Layer.value(ConfigTag, { port: 3000 });
165
174
 
166
175
  // Layer.create for custom factory logic
167
176
  const cacheLayer = Layer.create({
168
- requires: [ConfigTag],
169
- apply: (builder) => builder.add(Cache, async (ctx) => {
170
- const config = await ctx.resolve(ConfigTag);
171
- return new Cache({ ttl: config.cacheTtl });
172
- })
177
+ requires: [ConfigTag],
178
+ apply: (builder) =>
179
+ builder.add(Cache, async (ctx) => {
180
+ const config = await ctx.resolve(ConfigTag);
181
+ return new Cache({ ttl: config.cacheTtl });
182
+ }),
173
183
  });
174
184
  ```
175
185
 
@@ -195,16 +205,17 @@ Scoped containers enable hierarchical dependency management:
195
205
  ```typescript
196
206
  // Application scope - use builder to add dependencies
197
207
  const appContainer = ScopedContainer.builder('app')
198
- .add(Database, () => new Database())
199
- .build();
208
+ .add(Database, () => new Database())
209
+ .build();
200
210
 
201
211
  // Request scope - use child() to create a child builder
202
- const requestContainer = appContainer.child('request')
203
- .add(RequestContext, () => new RequestContext())
204
- .build();
212
+ const requestContainer = appContainer
213
+ .child('request')
214
+ .add(RequestContext, () => new RequestContext())
215
+ .build();
205
216
 
206
217
  // Child can resolve both its own and parent dependencies
207
- const db = await requestContainer.resolve(Database); // From parent
218
+ const db = await requestContainer.resolve(Database); // From parent
208
219
  const ctx = await requestContainer.resolve(RequestContext); // From child
209
220
 
210
221
  // Destroy child without affecting parent
@@ -215,8 +226,9 @@ Or use layers with `childFrom`:
215
226
 
216
227
  ```typescript
217
228
  const appContainer = ScopedContainer.from('app', dbLayer);
218
- const requestContainer = appContainer.childFrom('request',
219
- Layer.value(RequestContext, new RequestContext())
229
+ const requestContainer = appContainer.childFrom(
230
+ 'request',
231
+ Layer.value(RequestContext, new RequestContext())
220
232
  );
221
233
  ```
222
234
 
@@ -227,8 +239,8 @@ The `use()` method resolves a service, runs a callback, and automatically destro
227
239
  ```typescript
228
240
  // Perfect for short-lived operations like Lambda handlers or worker jobs
229
241
  const result = await appContainer
230
- .childFrom('request', requestLayer)
231
- .use(UserService, (service) => service.processEvent(event));
242
+ .childFrom('request', requestLayer)
243
+ .use(UserService, (service) => service.processEvent(event));
232
244
  // Container is automatically destroyed after callback completes
233
245
  ```
234
246
 
@@ -242,7 +254,10 @@ This is especially useful for serverless functions or message handlers where the
242
254
 
243
255
  ```typescript
244
256
  class ApiClient {
245
- constructor(private config: Config, private logger: Logger) {}
257
+ constructor(
258
+ private config: Config,
259
+ private logger: Logger
260
+ ) {}
246
261
  }
247
262
 
248
263
  // Dependencies must match constructor parameters in order
@@ -250,28 +265,60 @@ const apiLayer = Layer.service(ApiClient, [Config, Logger]);
250
265
 
251
266
  // With cleanup function
252
267
  const dbLayer = Layer.service(Database, [], {
253
- cleanup: (db) => db.close()
268
+ cleanup: (db) => db.close(),
254
269
  });
255
270
  ```
256
271
 
257
- **Layer.value**: Constant values
272
+ **Layer.value**: Constant values or pre-instantiated instances
258
273
 
259
274
  ```typescript
275
+ // ValueTag (constants)
260
276
  const ApiKeyTag = Tag.of('apiKey')<string>();
261
277
  const configLayer = Layer.value(ApiKeyTag, process.env.API_KEY!);
278
+
279
+ // ServiceTag (pre-instantiated instances)
280
+ class UserService {
281
+ getUsers() {
282
+ return [];
283
+ }
284
+ }
285
+ const userService = new UserService();
286
+ const testLayer = Layer.value(UserService, userService);
287
+ ```
288
+
289
+ **Layer.mock**: Partial mocks for testing (ServiceTags only)
290
+
291
+ ```typescript
292
+ class UserService {
293
+ constructor(private db: Database) {}
294
+ getUsers() {
295
+ return this.db.query('SELECT * FROM users');
296
+ }
297
+ getUserById(id: number) {
298
+ return this.db.query(`...`);
299
+ }
300
+ }
301
+
302
+ // Mock only the methods you need - no constructor dependencies required
303
+ const testLayer = Layer.mock(UserService, {
304
+ getUsers: () => Promise.resolve([{ id: 1, name: 'Alice' }]),
305
+ });
306
+
307
+ // TypeScript still validates the mock's method signatures
262
308
  ```
263
309
 
264
310
  **Layer.create**: Custom factory logic
265
311
 
266
312
  ```typescript
267
313
  const dbLayer = Layer.create({
268
- requires: [ConfigTag],
269
- apply: (builder) => builder.add(Database, async (ctx) => {
270
- const config = await ctx.resolve(ConfigTag);
271
- const db = new Database(config.dbUrl);
272
- await db.connect();
273
- return db;
274
- })
314
+ requires: [ConfigTag],
315
+ apply: (builder) =>
316
+ builder.add(Database, async (ctx) => {
317
+ const config = await ctx.resolve(ConfigTag);
318
+ const db = new Database(config.dbUrl);
319
+ await db.connect();
320
+ return db;
321
+ }),
275
322
  });
276
323
  ```
277
324
 
@@ -286,10 +333,10 @@ const serviceLayer = Layer.service(UserService, [UserRepository, Logger]);
286
333
 
287
334
  // Compose into complete application
288
335
  const appLayer = serviceLayer
289
- .provide(repoLayer)
290
- .provide(dbLayer)
291
- .provide(configLayer)
292
- .provide(Layer.service(Logger, []));
336
+ .provide(repoLayer)
337
+ .provide(dbLayer)
338
+ .provide(configLayer)
339
+ .provide(Layer.service(Logger, []));
293
340
 
294
341
  // Create container - all dependencies satisfied
295
342
  const container = Container.from(appLayer);
@@ -324,48 +371,55 @@ const container = Container.from(incomplete); // Type error!
324
371
  import { ScopedContainer, Layer } from 'sandly';
325
372
 
326
373
  // App-level dependencies (shared across requests)
327
- const appContainer = ScopedContainer.from('app',
328
- Layer.mergeAll(dbLayer, loggerLayer)
374
+ const appContainer = ScopedContainer.from(
375
+ 'app',
376
+ Layer.mergeAll(dbLayer, loggerLayer)
329
377
  );
330
378
 
331
379
  // Express middleware
332
380
  app.use(async (req, res, next) => {
333
- // Create request scope with request-specific dependencies
334
- const requestScope = appContainer.childFrom('request',
335
- Layer.value(RequestContext, {
336
- requestId: crypto.randomUUID(),
337
- userId: req.user?.id
338
- })
339
- );
340
-
341
- res.locals.container = requestScope;
342
-
343
- res.on('finish', () => requestScope.destroy());
344
- next();
381
+ // Create request scope with request-specific dependencies
382
+ const requestScope = appContainer.childFrom(
383
+ 'request',
384
+ Layer.value(RequestContext, {
385
+ requestId: crypto.randomUUID(),
386
+ userId: req.user?.id,
387
+ })
388
+ );
389
+
390
+ res.locals.container = requestScope;
391
+
392
+ res.on('finish', () => requestScope.destroy());
393
+ next();
345
394
  });
346
395
 
347
396
  // Route handler
348
397
  app.get('/users', async (req, res) => {
349
- const userService = await res.locals.container.resolve(UserService);
350
- res.json(await userService.getUsers());
398
+ const userService = await res.locals.container.resolve(UserService);
399
+ res.json(await userService.getUsers());
351
400
  });
352
401
  ```
353
402
 
354
403
  ### Destruction Order
355
404
 
356
405
  When destroying a scoped container:
406
+
357
407
  1. Child scopes are destroyed first
358
408
  2. Then the current scope's finalizers run
359
409
  3. Parent scope is unaffected
360
410
 
361
411
  ```typescript
362
412
  const parent = ScopedContainer.builder('parent')
363
- .add(Database, { create: () => new Database(), cleanup: (db) => db.close() })
364
- .build();
413
+ .add(Database, {
414
+ create: () => new Database(),
415
+ cleanup: (db) => db.close(),
416
+ })
417
+ .build();
365
418
 
366
- const child = parent.child('child')
367
- .add(Cache, { create: () => new Cache(), cleanup: (c) => c.clear() })
368
- .build();
419
+ const child = parent
420
+ .child('child')
421
+ .add(Cache, { create: () => new Cache(), cleanup: (c) => c.clear() })
422
+ .build();
369
423
 
370
424
  await parent.destroy(); // Destroys child first (Cache.clear), then parent (Database.close)
371
425
  ```
@@ -376,25 +430,25 @@ Sandly provides specific error types for common issues:
376
430
 
377
431
  ```typescript
378
432
  import {
379
- UnknownDependencyError,
380
- CircularDependencyError,
381
- DependencyCreationError,
382
- DependencyFinalizationError
433
+ UnknownDependencyError,
434
+ CircularDependencyError,
435
+ DependencyCreationError,
436
+ DependencyFinalizationError,
383
437
  } from 'sandly';
384
438
 
385
439
  try {
386
- const service = await container.resolve(UserService);
440
+ const service = await container.resolve(UserService);
387
441
  } catch (error) {
388
- if (error instanceof CircularDependencyError) {
389
- console.log(error.message);
390
- // "Circular dependency detected for UserService: UserService -> Database -> UserService"
391
- }
392
-
393
- if (error instanceof DependencyCreationError) {
394
- // Get the original error that caused the failure
395
- const rootCause = error.getRootCause();
396
- console.log(rootCause.message);
397
- }
442
+ if (error instanceof CircularDependencyError) {
443
+ console.log(error.message);
444
+ // "Circular dependency detected for UserService: UserService -> Database -> UserService"
445
+ }
446
+
447
+ if (error instanceof DependencyCreationError) {
448
+ // Get the original error that caused the failure
449
+ const rootCause = error.getRootCause();
450
+ console.log(rootCause.message);
451
+ }
398
452
  }
399
453
  ```
400
454
 
@@ -402,68 +456,121 @@ try {
402
456
 
403
457
  ### Container
404
458
 
405
- | Method | Description |
406
- |--------|-------------|
407
- | `Container.from(layer)` | Create container from a fully resolved layer |
408
- | `Container.builder()` | Create a container builder |
409
- | `Container.empty()` | Create an empty container |
410
- | `Container.scoped(scope)` | Create an empty scoped container |
411
- | `container.resolve(tag)` | Get a dependency instance |
412
- | `container.resolveAll(...tags)` | Get multiple dependencies |
413
- | `container.use(tag, fn)` | Resolve, run callback, then destroy container |
414
- | `container.destroy()` | Run finalizers and clean up |
459
+ | Method | Description |
460
+ | ------------------------------- | --------------------------------------------- |
461
+ | `Container.from(layer)` | Create container from a fully resolved layer |
462
+ | `Container.builder()` | Create a container builder |
463
+ | `Container.empty()` | Create an empty container |
464
+ | `Container.scoped(scope)` | Create an empty scoped container |
465
+ | `container.resolve(tag)` | Get a dependency instance |
466
+ | `container.resolveAll(...tags)` | Get multiple dependencies |
467
+ | `container.use(tag, fn)` | Resolve, run callback, then destroy container |
468
+ | `container.destroy()` | Run finalizers and clean up |
415
469
 
416
470
  ### ContainerBuilder
417
471
 
418
- | Method | Description |
419
- |--------|-------------|
472
+ | Method | Description |
473
+ | ------------------------ | --------------------- |
420
474
  | `builder.add(tag, spec)` | Register a dependency |
421
- | `builder.build()` | Create the container |
475
+ | `builder.build()` | Create the container |
422
476
 
423
477
  ### Layer
424
478
 
425
- | Method | Description |
426
- |--------|-------------|
427
- | `Layer.service(class, deps, options?)` | Create layer for a class |
428
- | `Layer.value(tag, value)` | Create layer for a constant value |
429
- | `Layer.create({ requires, apply })` | Create custom layer |
430
- | `Layer.empty()` | Create empty layer |
431
- | `Layer.merge(a, b)` | Merge two layers |
432
- | `Layer.mergeAll(...layers)` | Merge multiple layers |
433
- | `layer.provide(dep)` | Satisfy dependencies |
434
- | `layer.provideMerge(dep)` | Satisfy and merge provisions |
435
- | `layer.merge(other)` | Merge with another layer |
479
+ | Method | Description |
480
+ | -------------------------------------- | ----------------------------------------------- |
481
+ | `Layer.service(class, deps, options?)` | Create layer for a class |
482
+ | `Layer.value(tag, value)` | Create layer for a constant value |
483
+ | `Layer.mock(tag, implementation)` | Create layer with mock (partial for ServiceTag) |
484
+ | `Layer.create({ requires, apply })` | Create custom layer |
485
+ | `Layer.empty()` | Create empty layer |
486
+ | `Layer.merge(a, b)` | Merge two layers |
487
+ | `Layer.mergeAll(...layers)` | Merge multiple layers |
488
+ | `layer.provide(dep)` | Satisfy dependencies |
489
+ | `layer.provideMerge(dep)` | Satisfy and merge provisions |
490
+ | `layer.merge(other)` | Merge with another layer |
436
491
 
437
492
  ### ScopedContainer
438
493
 
439
- | Method | Description |
440
- |--------|-------------|
441
- | `ScopedContainer.builder(scope)` | Create a new scoped container builder |
442
- | `ScopedContainer.empty(scope)` | Create empty scoped container |
443
- | `ScopedContainer.from(scope, layer)` | Create from layer |
444
- | `container.child(scope)` | Create child scope builder |
445
- | `container.childFrom(scope, layer)` | Create child scope from layer (convenience) |
494
+ | Method | Description |
495
+ | ------------------------------------ | ------------------------------------------- |
496
+ | `ScopedContainer.builder(scope)` | Create a new scoped container builder |
497
+ | `ScopedContainer.empty(scope)` | Create empty scoped container |
498
+ | `ScopedContainer.from(scope, layer)` | Create from layer |
499
+ | `container.child(scope)` | Create child scope builder |
500
+ | `container.childFrom(scope, layer)` | Create child scope from layer (convenience) |
446
501
 
447
502
  ### Tag
448
503
 
449
- | Method | Description |
450
- |--------|-------------|
451
- | `Tag.of(id)<T>()` | Create a ValueTag |
452
- | `Tag.id(tag)` | Get tag's string identifier |
453
- | `Tag.isTag(value)` | Check if value is a tag |
504
+ | Method | Description |
505
+ | ------------------ | --------------------------- |
506
+ | `Tag.of(id)<T>()` | Create a ValueTag |
507
+ | `Tag.id(tag)` | Get tag's string identifier |
508
+ | `Tag.isTag(value)` | Check if value is a tag |
509
+
510
+ ## Testing
511
+
512
+ Sandly makes testing easy with `Layer.mock()`, which allows you to create partial mocks without satisfying constructor dependencies. Import your production layers and override dependencies with mocks:
513
+
514
+ ```typescript
515
+ // Production code (e.g., src/services/user-service.ts)
516
+ import { Layer } from 'sandly';
517
+ import { ResourcesRepository } from '../repositories/resources-repository';
518
+
519
+ export class UserService {
520
+ constructor(private repo: ResourcesRepository) {}
521
+ async getUsers() {
522
+ return this.repo.listByCrawlId('crawl-123');
523
+ }
524
+ }
525
+
526
+ // Layer definition in the same file
527
+ export const userServiceLayer = Layer.service(UserService, [
528
+ ResourcesRepository,
529
+ ]);
530
+
531
+ // Test file (e.g., src/services/user-service.test.ts)
532
+ import { Container, Layer } from 'sandly';
533
+ import { userServiceLayer } from './user-service';
534
+ import { ResourcesRepository } from '../repositories/resources-repository';
535
+
536
+ // Override production dependencies with mocks
537
+ const testLayer = userServiceLayer.provide(
538
+ Layer.mock(ResourcesRepository, {
539
+ listByCrawlId: async () => [
540
+ { id: '1', name: 'Alice' },
541
+ { id: '2', name: 'Bob' },
542
+ ],
543
+ })
544
+ );
545
+
546
+ const container = Container.from(testLayer);
547
+ const userService = await container.resolve(UserService);
548
+
549
+ // Use the service - mock is automatically injected
550
+ const users = await userService.getUsers();
551
+ expect(users).toHaveLength(2);
552
+ ```
553
+
554
+ **Benefits:**
555
+
556
+ - ✅ No need to satisfy constructor dependencies for mocks
557
+ - ✅ TypeScript validates mock method signatures
558
+ - ✅ Works seamlessly with `Layer.service()` composition
559
+ - ✅ Clear intent: `mock()` for tests, `value()` for production
454
560
 
455
561
  ## Comparison with Alternatives
456
562
 
457
- | Feature | Sandly | NestJS | InversifyJS | TSyringe |
458
- |---------|--------|--------|-------------|----------|
459
- | Compile-time type safety | ✅ | ❌ | ⚠️ Partial | ❌ |
460
- | No experimental decorators | ✅ | ❌ | ❌ | ❌ |
461
- | Async factories | ✅ | ✅ | ❌ | ❌ |
462
- | Framework-agnostic | ✅ | ❌ | ✅ | ✅ |
463
- | Layer composition | ✅ | ❌ | ❌ | ❌ |
464
- | Zero dependencies | ✅ | ❌ | ❌ | ❌ |
563
+ | Feature | Sandly | NestJS | InversifyJS | TSyringe |
564
+ | -------------------------- | ------ | ------ | ----------- | -------- |
565
+ | Compile-time type safety | ✅ | ❌ | ⚠️ Partial | ❌ |
566
+ | No experimental decorators | ✅ | ❌ | ❌ | ❌ |
567
+ | Async factories | ✅ | ✅ | ❌ | ❌ |
568
+ | Framework-agnostic | ✅ | ❌ | ✅ | ✅ |
569
+ | Layer composition | ✅ | ❌ | ❌ | ❌ |
570
+ | Zero dependencies | ✅ | ❌ | ❌ | ❌ |
465
571
 
466
572
  **Choose Sandly when you want:**
573
+
467
574
  - Type safety without sacrificing simplicity
468
575
  - DI without experimental decorators
469
576
  - Composable, reusable dependency modules
package/dist/index.d.ts CHANGED
@@ -325,12 +325,14 @@ declare const Layer: {
325
325
  cleanup?: Finalizer<InstanceType<TClass>>;
326
326
  }): Layer<ExtractTags<TDeps>, TClass>;
327
327
  /**
328
- * Creates a layer that provides a constant value.
328
+ * Creates a layer that provides a constant value or pre-instantiated instance.
329
329
  *
330
- * @param tag - The ValueTag to register
331
- * @param value - The value to provide
330
+ * Works with both ValueTags (for constants) and ServiceTags (for pre-instantiated instances, useful in tests).
332
331
  *
333
- * @example
332
+ * @param tag - The tag (ValueTag or ServiceTag) to register
333
+ * @param value - The value or instance to provide
334
+ *
335
+ * @example ValueTag (constant)
334
336
  * ```typescript
335
337
  * const ApiKeyTag = Tag.of('apiKey')<string>();
336
338
  * const ConfigTag = Tag.of('config')<{ port: number }>();
@@ -338,8 +340,58 @@ declare const Layer: {
338
340
  * const configLayer = Layer.value(ApiKeyTag, 'secret-key')
339
341
  * .merge(Layer.value(ConfigTag, { port: 3000 }));
340
342
  * ```
343
+ *
344
+ * @example ServiceTag (pre-instantiated instance, useful for testing)
345
+ * ```typescript
346
+ * class UserService {
347
+ * getUsers() { return []; }
348
+ * }
349
+ *
350
+ * const mockUserService = new UserService();
351
+ * const testLayer = Layer.value(UserService, mockUserService);
352
+ * ```
353
+ */
354
+ value<T extends AnyTag>(tag: T, value: TagType<T>): Layer<never, T>;
355
+ /**
356
+ * Creates a layer with a mock implementation for testing.
357
+ *
358
+ * Similar to `Layer.value()`, but allows partial implementations for ServiceTags,
359
+ * making it easier to create test mocks without satisfying constructor dependencies.
360
+ *
361
+ * **Use this for testing only.** For production code, use `Layer.value()` or `Layer.service()`.
362
+ *
363
+ * @param tag - The tag (ServiceTag or ValueTag) to register
364
+ * @param implementation - The mock implementation (can be partial for ServiceTags)
365
+ *
366
+ * @example ServiceTag with partial mock
367
+ * ```typescript
368
+ * class UserService {
369
+ * constructor(private db: Database) {}
370
+ * getUsers() { return this.db.query('SELECT * FROM users'); }
371
+ * }
372
+ *
373
+ * // Mock only the methods you need - no need to satisfy constructor
374
+ * const testLayer = Layer.mock(UserService, {
375
+ * getUsers: () => Promise.resolve([{ id: 1, name: 'Alice' }])
376
+ * });
377
+ * ```
378
+ *
379
+ * @example ServiceTag with full mock instance
380
+ * ```typescript
381
+ * const mockUserService = {
382
+ * getUsers: () => Promise.resolve([])
383
+ * } as UserService;
384
+ *
385
+ * const testLayer = Layer.mock(UserService, mockUserService);
386
+ * ```
387
+ *
388
+ * @example ValueTag (works same as Layer.value)
389
+ * ```typescript
390
+ * const ConfigTag = Tag.of('config')<{ port: number }>();
391
+ * const testLayer = Layer.mock(ConfigTag, { port: 3000 });
392
+ * ```
341
393
  */
342
- value<T extends ValueTag<TagId, unknown>>(tag: T, value: TagType<T>): Layer<never, T>;
394
+ mock<T extends AnyTag>(tag: T, implementation: T extends ServiceTag ? Partial<TagType<T>> | TagType<T> : TagType<T>): Layer<never, T>;
343
395
  /**
344
396
  * Creates a custom layer with full control over the factory logic.
345
397
  *
package/dist/index.js CHANGED
@@ -894,6 +894,11 @@ const Layer = {
894
894
  return builder.add(tag, () => value);
895
895
  });
896
896
  },
897
+ mock(tag, implementation) {
898
+ return createLayer((builder) => {
899
+ return builder.add(tag, () => implementation);
900
+ });
901
+ },
897
902
  create(options) {
898
903
  const layer = {
899
904
  apply: (builder) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandly",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "keywords": [
5
5
  "typescript",
6
6
  "sandly",