sandly 2.0.1 → 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
@@ -276,12 +276,35 @@ const dbLayer = Layer.service(Database, [], {
276
276
  const ApiKeyTag = Tag.of('apiKey')<string>();
277
277
  const configLayer = Layer.value(ApiKeyTag, process.env.API_KEY!);
278
278
 
279
- // ServiceTag (pre-instantiated instances, useful for testing)
279
+ // ServiceTag (pre-instantiated instances)
280
280
  class UserService {
281
- getUsers() { return []; }
281
+ getUsers() {
282
+ return [];
283
+ }
282
284
  }
283
- const mockUserService = new UserService();
284
- const testLayer = Layer.value(UserService, mockUserService);
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
285
308
  ```
286
309
 
287
310
  **Layer.create**: Custom factory logic
@@ -453,17 +476,18 @@ try {
453
476
 
454
477
  ### Layer
455
478
 
456
- | Method | Description |
457
- | -------------------------------------- | --------------------------------- |
458
- | `Layer.service(class, deps, options?)` | Create layer for a class |
459
- | `Layer.value(tag, value)` | Create layer for a constant value |
460
- | `Layer.create({ requires, apply })` | Create custom layer |
461
- | `Layer.empty()` | Create empty layer |
462
- | `Layer.merge(a, b)` | Merge two layers |
463
- | `Layer.mergeAll(...layers)` | Merge multiple layers |
464
- | `layer.provide(dep)` | Satisfy dependencies |
465
- | `layer.provideMerge(dep)` | Satisfy and merge provisions |
466
- | `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 |
467
491
 
468
492
  ### ScopedContainer
469
493
 
@@ -483,6 +507,57 @@ try {
483
507
  | `Tag.id(tag)` | Get tag's string identifier |
484
508
  | `Tag.isTag(value)` | Check if value is a tag |
485
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
560
+
486
561
  ## Comparison with Alternatives
487
562
 
488
563
  | Feature | Sandly | NestJS | InversifyJS | TSyringe |
package/dist/index.d.ts CHANGED
@@ -352,6 +352,46 @@ declare const Layer: {
352
352
  * ```
353
353
  */
354
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
+ * ```
393
+ */
394
+ mock<T extends AnyTag>(tag: T, implementation: T extends ServiceTag ? Partial<TagType<T>> | TagType<T> : TagType<T>): Layer<never, T>;
355
395
  /**
356
396
  * Creates a custom layer with full control over the factory logic.
357
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.1",
3
+ "version": "2.1.0",
4
4
  "keywords": [
5
5
  "typescript",
6
6
  "sandly",