Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/server/src/CatsApiImpl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Effect } from "effect";
import { CatsService } from "./CatsService.ts"; // Import CatsService
import { CatsServicePort } from "./CatsServicePort.ts"; // Import CatsServicePort
import { HttpApiBuilder } from "@effect/platform";
import { api } from "@effect-cats/domain";

Expand All @@ -8,19 +8,19 @@ export const catsApiLiveGroup = HttpApiBuilder.group(
"cats",
(handlers) =>
Effect.gen(function* (_) {
const catsService = yield* _(CatsService); // Use CatsService
const catsService = yield* _(CatsServicePort); // Use CatsServicePort
return handlers
.handle("getAllCats", () => catsService.getAllCats) // Use CatsService method
.handle("getCatById", ({ path: { id } }) => catsService.getCatById(id)) // Use CatsService method
.handle("getAllCats", () => catsService.getAllCats) // Use CatsServicePort method
.handle("getCatById", ({ path: { id } }) => catsService.getCatById(id)) // Use CatsServicePort method
.handle(
"createCat",
({ payload: { name, breed, age } }) =>
catsService.createCat(name, breed, age), // Use CatsService method
catsService.createCat(name, breed, age), // Use CatsServicePort method
)
.handle(
"updateCat",
({ path: { id }, payload }) => catsService.updateCat(id, payload), // Use CatsService method
({ path: { id }, payload }) => catsService.updateCat(id, payload), // Use CatsServicePort method
)
.handle("deleteCat", ({ path: { id } }) => catsService.deleteCat(id)); // Use CatsService method
.handle("deleteCat", ({ path: { id } }) => catsService.deleteCat(id)); // Use CatsServicePort method
}),
);
Original file line number Diff line number Diff line change
@@ -1,65 +1,60 @@
import { Effect, Layer, Schema } from "effect"; // Removed Context, Data as they might not be needed directly
import { assert, assertEquals } from "jsr:@std/assert"; // Corrected assert import
import { Effect, Layer, Schema } from "effect";
import { assert, assertEquals } from "jsr:@std/assert";
import { describe, it } from "jsr:@std/testing/bdd";

// Domain imports - assuming CatNotFound is exported from domain now
import { Cat, CatId, CatNotFound } from "@effect-cats/domain";

// Service and ACTUAL Repository Tag imports
import { CatsService, CatsServiceLive } from "./CatsService.ts";
// Service Port and Application Service imports
import { CatsServicePort } from "./CatsServicePort.ts";
import { CatsApplicationServiceLive } from "./CatsApplicationService.ts";
import { CatsRepositoryPort } from "./CatsRepositoryPort.ts";

// The mock implementation's type should ideally match the actual service interface provided by CatsRepositoryPort
// This line assumes CatsRepositoryPort has an 'of' static method and its first parameter is the service impl
// If CatsRepositoryPort is just a Tag<Interface>, this will need adjustment.
// For now, let's define a similar structure to what CatsRepositoryPort.of might expect.

// We'll use this Partial type for providing mocks.
// We'll use this Partial type for providing mocks for the repository.
type MockRepoPartial = Partial<CatsRepositoryPort["Type"]>;

const runEffectTest = <E, A>(
effectToRun: Effect.Effect<A, E, CatsService>, // The effect needs CatsService
// UPDATE: Use Partial<CatsRepository["Type"]>
mockRepoPartialImpl: Partial<CatsRepositoryPort["Type"]> = {}, // Default to empty mock
effectToRun: Effect.Effect<A, E, CatsServicePort>, // The effect now needs CatsServicePort
mockRepoPartialImpl: MockRepoPartial = {}, // Default to empty mock
) => {
// Create a full mock implementation by merging partial mock with defaults that throw
// UPDATE: Use CatsRepository["Type"]
// Create a full mock implementation for the repository
const fullMockImpl: CatsRepositoryPort["Type"] = {
getAll: Effect.die("getAll not implemented in mock"),
getById: (id: CatId) =>
Effect.die(`getById(${id}) not implemented in mock`),
create: (name: string, breed: string, age: number) =>
// Added types
Effect.die(`create(${name}, ${breed}, ${age}) not implemented in mock`),
update: (id: CatId, data: Partial<Omit<Cat, "id">>) =>
// Added types
Effect.die(
`update(${id}, ${JSON.stringify(data)}) not implemented in mock`,
),
remove: (id: CatId) => Effect.die(`remove(${id}) not implemented in mock`),
...mockRepoPartialImpl, // Override defaults with provided mocks
};

// This is the critical part:
// It assumes CatsRepositoryPort is a Tag for a service that can be constructed with CatsRepositoryPort.of()
// or if CatsRepositoryPort is Tag<Interface>, then it should be CatsRepositoryPort (the Tag itself)
// and the second argument is the implementation (fullMockImpl).
// The instruction `CatsRepositoryPort.of(fullMockImpl)` implies CatsRepositoryPort is a class or object with `of`.
// Corrected to directly use fullMockImpl as CatsRepositoryPort is a Context.Tag
// Layer for the mock repository
const mockCatsRepositoryLayer = Layer.succeed(
CatsRepositoryPort,
CatsRepositoryPort.of(fullMockImpl), // Construct the service implementation
CatsRepositoryPort.of(fullMockImpl),
);

// Provide CatsApplicationServiceLive which depends on CatsRepositoryPort,
// and then provide the mock repository layer to CatsApplicationServiceLive.
// CatsApplicationServiceLive provides the implementation for CatsServicePort.
const testLayer = Layer.provide(
CatsApplicationServiceLive, // This provides CatsServicePort
mockCatsRepositoryLayer, // This provides CatsRepositoryPort to CatsApplicationServiceLive
);

const testLayer = Layer.provide(CatsServiceLive, mockCatsRepositoryLayer);
// Provide the testLayer (which includes the service and its mock dependency) to the effect to run.
const providedEffect = Effect.provide(effectToRun, testLayer);

return Effect.runPromise(providedEffect);
};

describe("CatsService (Refined)", () => {
describe("CatsApplicationService (using CatsServicePort)", () => {
it("getAllCats should return an empty array when repository is empty", async () => {
const testEffect = Effect.gen(function* (_) {
const service = yield* _(CatsService);
const service = yield* _(CatsServicePort); // Use CatsServicePort
const cats = yield* _(service.getAllCats);
assertEquals(cats.length, 0);
});
Expand All @@ -86,7 +81,7 @@ describe("CatsService (Refined)", () => {
];

const testEffect = Effect.gen(function* (_) {
const service = yield* _(CatsService);
const service = yield* _(CatsServicePort); // Use CatsServicePort
const cats = yield* _(service.getAllCats);
assertEquals(cats, sampleCats);
});
Expand All @@ -97,7 +92,7 @@ describe("CatsService (Refined)", () => {
});

it("getCatById should return a cat when found", async () => {
const catId = Schema.decodeUnknownSync(CatId)(3); // Using casting for CatId
const catId = Schema.decodeUnknownSync(CatId)(3);
const sampleCat = new Cat({
id: catId,
name: "Felix",
Expand All @@ -106,7 +101,7 @@ describe("CatsService (Refined)", () => {
});

const testEffect = Effect.gen(function* (_) {
const service = yield* _(CatsService);
const service = yield* _(CatsServicePort); // Use CatsServicePort
const cat = yield* _(service.getCatById(catId));
assertEquals(cat, sampleCat);
});
Expand All @@ -120,16 +115,15 @@ describe("CatsService (Refined)", () => {
});

it("getCatById should return CatNotFound error when cat is not found", async () => {
const nonExistentCatId = Schema.decodeUnknownSync(CatId)(99); // Using casting for CatId
const nonExistentCatId = Schema.decodeUnknownSync(CatId)(99);

const testEffect = Effect.gen(function* (_) {
const service = yield* _(CatsService);
const service = yield* _(CatsServicePort); // Use CatsServicePort
return yield* _(service.getCatById(nonExistentCatId));
}).pipe(
Effect.match({
onFailure: (error) => {
assertEquals(error._tag, "CatNotFound");
// Ensure CatNotFound has an 'id' property if this assertion is to pass
if (error._tag === "CatNotFound") {
assertEquals((error as CatNotFound).id, nonExistentCatId);
} else {
Expand All @@ -149,7 +143,6 @@ describe("CatsService (Refined)", () => {
);

await runEffectTest(testEffect, {
// Ensure new CatNotFound({id: ...}) matches the actual error structure from the domain
getById: (_id: CatId) =>
Effect.fail(new CatNotFound({ id: nonExistentCatId })),
});
Expand Down
81 changes: 81 additions & 0 deletions packages/server/src/CatsApplicationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Cat, CatId, CatNotFound } from "@effect-cats/domain";
import { Effect, Layer } from "effect";
import { CatsRepositoryPort } from "./CatsRepositoryPort.ts";
import { CatsServicePort, CatsServicePortType } from "./CatsServicePort.ts";

// Implement the service
export class CatsApplicationService implements CatsServicePortType {
constructor(private readonly repository: CatsRepositoryPort["Type"]) {}

readonly getAllCats = Effect.logDebug("getAllCats called").pipe(
Effect.flatMap(() => this.repository.getAll),
Effect.tap((cats) => Effect.logInfo(`Retrieved ${cats.length} cats`)),
Effect.withSpan("CatsApplicationService/getAllCats"),
);

readonly getCatById = (id: CatId) =>
Effect.logDebug(`getCatById called with id: ${id}`).pipe(
Effect.flatMap(() => this.repository.getById(id)),
Effect.tap((cat) => Effect.logInfo(`Retrieved cat: ${cat.name}`)),
Effect.tapErrorTag(
"CatNotFound",
(e) => Effect.logWarning(`Cat with id: ${e.id} not found`),
),
Effect.withSpan("CatsApplicationService/getCatById", {
attributes: { "cat.id": id },
}),
);

readonly createCat = (name: string, breed: string, age: number) =>
Effect.logDebug(`createCat called with name: ${name}`).pipe(
Effect.flatMap(() => this.repository.create(name, breed, age)),
Effect.tap((cat) =>
Effect.logInfo(`Created cat: ${cat.name} with id: ${cat.id}`)
),
Effect.withSpan("CatsApplicationService/createCat", {
attributes: {
"cat.name": name,
"cat.breed": breed,
"cat.age": age,
},
}),
);

readonly updateCat = (id: CatId, data: Partial<Omit<Cat, "id">>) =>
Effect.logDebug(`updateCat called with id: ${id}`).pipe(
Effect.flatMap(() => this.repository.update(id, data)),
Effect.tap((cat) => Effect.logInfo(`Updated cat: ${cat.name}`)),
Effect.tapErrorTag(
"CatNotFound",
(e) =>
Effect.logWarning(`Cat with id: ${e.id} not found during update`),
),
Effect.withSpan("CatsApplicationService/updateCat", {
attributes: { "cat.id": id, "cat.updateData": true },
}),
);

readonly deleteCat = (id: CatId) =>
Effect.logDebug(`deleteCat called with id: ${id}`).pipe(
Effect.flatMap(() => this.repository.remove(id)),
Effect.tap(() =>
Effect.logInfo(`Attempted to delete cat with id: ${id}`)
),
Effect.tapErrorTag(
"CatNotFound",
(e) =>
Effect.logWarning(`Cat with id: ${e.id} not found for deletion`),
),
Effect.withSpan("CatsApplicationService/deleteCat", {
attributes: { "cat.id": id },
}),
);
}

export const CatsApplicationServiceLive = Layer.effect(
CatsServicePort, // Provide for the CatsServicePort Tag
Effect.gen(function* (_) {
const repository = yield* _(CatsRepositoryPort);
return new CatsApplicationService(repository);
}),
);
92 changes: 0 additions & 92 deletions packages/server/src/CatsService.ts

This file was deleted.

24 changes: 24 additions & 0 deletions packages/server/src/CatsServicePort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Cat, CatId, CatNotFound } from "@effect-cats/domain";
import { Context, Effect } from "effect";

// Define the interface for our service (Inbound Port)
export interface CatsServicePortType {
readonly getAllCats: Effect.Effect<ReadonlyArray<Cat>, never>;
readonly getCatById: (id: CatId) => Effect.Effect<Cat, CatNotFound>;
readonly createCat: (
name: string,
breed: string,
age: number,
) => Effect.Effect<Cat, never>;
readonly updateCat: (
id: CatId,
data: Partial<Omit<Cat, "id">>,
) => Effect.Effect<Cat, CatNotFound>;
readonly deleteCat: (id: CatId) => Effect.Effect<void, CatNotFound>;
}

// Create a context tag for the service port
export class CatsServicePort extends Context.Tag("Cats/ServicePort")<
CatsServicePort,
CatsServicePortType
>() {}
4 changes: 2 additions & 2 deletions packages/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import {
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
import { Config, Effect, Layer } from "effect";

import { CatsServiceLive } from "./CatsService.ts"; // Import CatsServiceLive
import { CatsApplicationServiceLive } from "./CatsApplicationService.ts"; // Import CatsApplicationServiceLive
import { CatsRepositoryAdapterInMemoryLive } from "./CatsRepositoryAdapter.ts";
import { catsApiLiveGroup } from "./CatsApiImpl.ts";
import { healthApiLiveGroup } from "./HealthApiImpl.ts";
import { api } from "@effect-cats/domain";

// Create a combined layer for the application services
const AppLive = Layer.provide(
CatsServiceLive,
CatsApplicationServiceLive, // Use CatsApplicationServiceLive
CatsRepositoryAdapterInMemoryLive,
);
// This will be the main export for the server to build the API
Expand Down