Skip to content

Commit 87475db

Browse files
committed
feat: added a build process for the readme and rules files. There we type errors and bugs in the old mdx examples. Now, with a build process, all the TS examples are now linted, typechecked, tested and code reviewed before being published.
1 parent 1a11572 commit 87475db

237 files changed

Lines changed: 12933 additions & 2149 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -260,21 +260,12 @@ How to test Effect code effectively, reliably, and deterministically.
260260

261261
## AI Coding Rules
262262

263-
This project provides several machine-readable sets of coding rules, tailored for different needs and optimized for AI-powered IDEs and coding agents like Cursor or GitHub Copilot.
263+
This project provides a machine-readable set of coding rules for AI-powered IDEs and coding agents.
264264

265-
You can find the latest rule files in the [`rules`](./rules/) directory. These are automatically generated from the content in this repository.
265+
You can find the latest rules files in the [rules directory](./rules/). These files are designed for integration with tools like Cursor, GitHub Copilot, and other AI coding assistants.
266266

267-
### Available Formats
268-
269-
- **Comprehensive Rules (`rules.md`)**: A human-readable markdown file containing all patterns, guidelines, and rationale.
270-
- **Compact Rules (`rules-compact.md`)**: A condensed version containing only the core rule for each pattern, designed for minimal token usage.
271-
- **Structured JSON (`rules.json`)**: A machine-readable JSON file containing all rule information for programmatic use.
272-
- **By Skill Level**: Detailed rules, including examples, split into separate files for each skill level.
273-
- [`beginner.md`](./rules/beginner.md)
274-
- [`intermediate.md`](./rules/intermediate.md)
275-
- [`advanced.md`](./rules/advanced.md)
276-
- **By Use Case**: Detailed rules, including examples, grouped by practical application areas.
277-
- [View all use cases](./rules/by-use-case/)
267+
- **rules.md**: Human-readable rules summary
268+
- **rules.json**: Structured rules for programmatic consumption
278269

279270
---
280271

bun.lock

Lines changed: 215 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

content/access-config-in-context.mdx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,31 @@ This allows your business logic to declaratively state its dependency on a piece
2323

2424
## Good Example
2525

26-
<Example path="./src/access-config-in-context.ts" />
26+
```typescript
27+
import { Config, Effect, Layer } from "effect";
28+
29+
// Define config service
30+
class AppConfig extends Effect.Service<AppConfig>()(
31+
"AppConfig",
32+
{
33+
sync: () => ({
34+
host: "localhost",
35+
port: 3000
36+
})
37+
}
38+
) {}
39+
40+
// Create program that uses config
41+
const program = Effect.gen(function* () {
42+
const config = yield* AppConfig;
43+
yield* Effect.log(`Starting server on http://${config.host}:${config.port}`);
44+
});
45+
46+
// Run the program with default config
47+
Effect.runPromise(
48+
Effect.provide(program, AppConfig.Default)
49+
);
50+
```
2751

2852
**Explanation:**
2953
By yielding the config object, you make your dependency explicit and leverage Effect's context system for testability and modularity.

content/accessing-current-time-with-clock.mdx

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,62 @@ This makes any time-dependent logic pure, deterministic, and easy to test with p
4242

4343
This example shows a function that checks if a token is expired. Its logic depends on `Clock`, making it fully testable.
4444

45-
<Example path="./src/accessing-current-time-with-clock.ts" />
45+
```typescript
46+
import { Effect, Clock, Duration } from "effect";
47+
48+
interface Token {
49+
readonly value: string;
50+
readonly expiresAt: number; // UTC milliseconds
51+
}
52+
53+
// This function is pure and testable because it depends on Clock
54+
const isTokenExpired = (token: Token): Effect.Effect<boolean, never, Clock.Clock> =>
55+
Clock.currentTimeMillis.pipe(
56+
Effect.map((now) => now > token.expiresAt),
57+
Effect.tap((expired) => Effect.log(`Token expired? ${expired} (current time: ${new Date().toISOString()})`))
58+
);
59+
60+
// Create a test clock service that advances time
61+
const makeTestClock = (timeMs: number): Clock.Clock => ({
62+
currentTimeMillis: Effect.succeed(timeMs),
63+
currentTimeNanos: Effect.succeed(BigInt(timeMs * 1_000_000)),
64+
sleep: (duration: Duration.Duration) => Effect.succeed(void 0),
65+
unsafeCurrentTimeMillis: () => timeMs,
66+
unsafeCurrentTimeNanos: () => BigInt(timeMs * 1_000_000),
67+
[Clock.ClockTypeId]: Clock.ClockTypeId,
68+
});
69+
70+
// Create a token that expires in 1 second
71+
const token = { value: "abc", expiresAt: Date.now() + 1000 };
72+
73+
// Check token expiry with different clocks
74+
const program = Effect.gen(function* () {
75+
// Check with current time
76+
yield* Effect.log("Checking with current time...");
77+
yield* isTokenExpired(token);
78+
79+
// Check with past time
80+
yield* Effect.log("\nChecking with past time (1 minute ago)...");
81+
const pastClock = makeTestClock(Date.now() - 60_000);
82+
yield* isTokenExpired(token).pipe(
83+
Effect.provideService(Clock.Clock, pastClock)
84+
);
85+
86+
// Check with future time
87+
yield* Effect.log("\nChecking with future time (1 hour ahead)...");
88+
const futureClock = makeTestClock(Date.now() + 3600_000);
89+
yield* isTokenExpired(token).pipe(
90+
Effect.provideService(Clock.Clock, futureClock)
91+
);
92+
});
93+
94+
// Run the program with default clock
95+
Effect.runPromise(
96+
program.pipe(
97+
Effect.provideService(Clock.Clock, makeTestClock(Date.now()))
98+
)
99+
);
100+
```
46101

47102
---
48103

content/accumulate-multiple-errors-with-either.mdx

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,86 @@ However, for tasks like validating a user's input, this is poor user experience.
4040

4141
Using `Schema.decode` with the `allErrors: true` option demonstrates this pattern perfectly. The underlying mechanism uses `Either` to collect all parsing errors into an array instead of stopping at the first one.
4242

43-
`<Example path="./src/accumulate-multiple-errors-with-either.ts" />`
43+
````typescript
44+
import { Effect, Schema, Data } from "effect";
45+
46+
// Define validation error type
47+
class ValidationError extends Data.TaggedError("ValidationError")<{
48+
readonly field: string;
49+
readonly message: string;
50+
}> {}
51+
52+
// Define user type
53+
type User = {
54+
name: string;
55+
email: string;
56+
};
57+
58+
// Define schema with custom validation
59+
const UserSchema = Schema.Struct({
60+
name: Schema.String.pipe(
61+
Schema.minLength(3),
62+
Schema.filter((name) => /^[A-Za-z\s]+$/.test(name), {
63+
message: () => "name must contain only letters and spaces"
64+
})
65+
),
66+
email: Schema.String.pipe(
67+
Schema.pattern(/@/),
68+
Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, {
69+
message: () => "email must be a valid email address"
70+
})
71+
),
72+
});
73+
74+
// Example inputs
75+
const invalidInputs: User[] = [
76+
{
77+
name: "Al", // Too short
78+
email: "bob-no-at-sign.com", // Invalid pattern
79+
},
80+
{
81+
name: "John123", // Contains numbers
82+
email: "john@incomplete", // Invalid email
83+
},
84+
{
85+
name: "Alice Smith", // Valid
86+
email: "alice@example.com", // Valid
87+
}
88+
];
89+
90+
// Validate a single user
91+
const validateUser = (input: User) =>
92+
Effect.gen(function* () {
93+
const result = yield* Schema.decode(UserSchema)(input, { errors: "all" });
94+
return result;
95+
});
96+
97+
// Process multiple users and accumulate all errors
98+
const program = Effect.gen(function* () {
99+
console.log("Validating users...\n");
100+
101+
for (const input of invalidInputs) {
102+
const result = yield* Effect.either(validateUser(input));
103+
104+
console.log(`Validating user: ${input.name} <${input.email}>`);
105+
106+
yield* Effect.match(result, {
107+
onFailure: (error) => Effect.sync(() => {
108+
console.log("❌ Validation failed:");
109+
console.log(error.message);
110+
console.log(); // Empty line for readability
111+
}),
112+
onSuccess: (user) => Effect.sync(() => {
113+
console.log("✅ User is valid:", user);
114+
console.log(); // Empty line for readability
115+
})
116+
});
117+
}
118+
});
119+
120+
// Run the program
121+
Effect.runSync(program);
122+
````
44123

45124
---
46125

content/add-caching-by-wrapping-a-layer.mdx

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,72 @@ This approach is powerful because:
4545

4646
We have a `WeatherService` that makes slow API calls. We create a `WeatherService.cached` wrapper layer that adds an in-memory cache using a `Ref` and a `Map`.
4747

48-
<Example path="./src/add-caching-by-wrapping-a-layer.ts" />
48+
```typescript
49+
import { Effect, Layer, Ref } from "effect";
50+
51+
// 1. Define the service interface
52+
class WeatherService extends Effect.Service<WeatherService>()(
53+
"WeatherService",
54+
{
55+
sync: () => ({
56+
getForecast: (city: string) => Effect.succeed(`Sunny in ${city}`),
57+
}),
58+
}
59+
) {}
60+
61+
// 2. The "Live" implementation that is slow
62+
const WeatherServiceLive = Layer.succeed(
63+
WeatherService,
64+
WeatherService.of({
65+
getForecast: (city) =>
66+
Effect.succeed(`Sunny in ${city}`).pipe(
67+
Effect.delay("2 seconds"),
68+
Effect.tap(() => Effect.log(`Fetched live forecast for ${city}`))
69+
),
70+
})
71+
);
72+
73+
// 3. The Caching Wrapper Layer
74+
const WeatherServiceCached = Layer.effect(
75+
WeatherService,
76+
Effect.gen(function* () {
77+
// It REQUIRES the original WeatherService
78+
const underlyingService = yield* WeatherService;
79+
const cache = yield* Ref.make(new Map<string, string>());
80+
81+
return WeatherService.of({
82+
getForecast: (city) =>
83+
Ref.get(cache).pipe(
84+
Effect.flatMap((map) =>
85+
map.has(city)
86+
? Effect.log(`Cache HIT for ${city}`).pipe(
87+
Effect.as(map.get(city)!)
88+
)
89+
: Effect.log(`Cache MISS for ${city}`).pipe(
90+
Effect.flatMap(() => underlyingService.getForecast(city)),
91+
Effect.tap((forecast) =>
92+
Ref.update(cache, (map) => map.set(city, forecast))
93+
)
94+
)
95+
)
96+
),
97+
});
98+
})
99+
);
100+
101+
// 4. Compose the final layer. The wrapper is provided with the live implementation.
102+
const AppLayer = Layer.provide(WeatherServiceCached, WeatherServiceLive);
103+
104+
// 5. The application logic
105+
const program = Effect.gen(function* () {
106+
const weather = yield* WeatherService;
107+
yield* weather.getForecast("London"); // First call is slow (MISS)
108+
yield* weather.getForecast("London"); // Second call is instant (HIT)
109+
});
110+
111+
Effect.runPromise(Effect.provide(program, AppLayer));
112+
113+
```
49114

50115
---
51116

content/add-custom-metrics.mdx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,38 @@ This allows you to answer questions like:
4747

4848
This example creates a counter to track how many times a user is created and a histogram to track the duration of the database operation.
4949

50-
<Example path="./src/add-custom-metrics.ts" />
50+
```typescript
51+
import { Effect, Metric, Duration } from "effect"; // We don't need MetricBoundaries anymore
52+
53+
// 1. Define your metrics
54+
const userRegisteredCounter = Metric.counter("users_registered_total", {
55+
description: "A counter for how many users have been registered.",
56+
});
57+
58+
const dbDurationTimer = Metric.timer(
59+
"db_operation_duration",
60+
"A timer for DB operation durations"
61+
);
62+
63+
// 2. Simulated database call
64+
const saveUserToDb = Effect.succeed("user saved").pipe(
65+
Effect.delay(Duration.millis(Math.random() * 100)),
66+
);
67+
68+
// 3. Instrument the business logic
69+
const createUser = Effect.gen(function* () {
70+
// Time the operation
71+
yield* saveUserToDb.pipe(Metric.trackDuration(dbDurationTimer));
72+
73+
// Increment the counter
74+
yield* Metric.increment(userRegisteredCounter);
75+
76+
return { status: "success" };
77+
});
78+
79+
// Run the Effect
80+
Effect.runPromise(createUser).then(console.log);
81+
```
5182

5283
---
5384

content/avoid-long-andthen-chains.mdx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,32 @@ debug than deeply nested functional chains.
2525

2626
## Good Example
2727

28-
<Example path="./src/avoid-long-andthen-chains.ts" />
28+
```typescript
29+
import { Effect } from "effect";
30+
31+
// Define our steps with logging
32+
const step1 = (): Effect.Effect<number> =>
33+
Effect.succeed(42).pipe(
34+
Effect.tap(n => Effect.log(`Step 1: ${n}`))
35+
);
36+
37+
const step2 = (a: number): Effect.Effect<string> =>
38+
Effect.succeed(`Result: ${a * 2}`).pipe(
39+
Effect.tap(s => Effect.log(`Step 2: ${s}`))
40+
);
41+
42+
// Using Effect.gen for better readability
43+
const program = Effect.gen(function* () {
44+
const a = yield* step1();
45+
const b = yield* step2(a);
46+
return b;
47+
});
48+
49+
// Run the program
50+
Effect.runPromise(program).then(
51+
result => Effect.runSync(Effect.log(`Final result: ${result}`))
52+
);
53+
```
2954

3055
**Explanation:**
3156
Generators keep sequential logic readable and easy to maintain.

content/beyond-the-date-type.mdx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,34 @@ This makes your time-based logic pure, predictable, and easy to test.
4545

4646
This example shows a function that creates a timestamped event. It depends on the `Clock` service, making it fully testable.
4747

48-
<Example path="./src/beyond-the-date-type.ts" />
48+
```typescript
49+
import { Effect, Clock } from "effect";
50+
import type * as Types from "effect/Clock";
51+
52+
interface Event {
53+
readonly message: string;
54+
readonly timestamp: number; // Store as a primitive number (UTC millis)
55+
}
56+
57+
// This function is pure and testable because it depends on Clock
58+
const createEvent = (message: string): Effect.Effect<Event, never, Types.Clock> =>
59+
Effect.gen(function* () {
60+
const timestamp = yield* Clock.currentTimeMillis;
61+
return { message, timestamp };
62+
});
63+
64+
// Create and log some events
65+
const program = Effect.gen(function* () {
66+
const loginEvent = yield* createEvent("User logged in");
67+
console.log("Login event:", loginEvent);
68+
69+
const logoutEvent = yield* createEvent("User logged out");
70+
console.log("Logout event:", logoutEvent);
71+
});
72+
73+
// Run the program
74+
Effect.runPromise(program.pipe(Effect.provideService(Clock.Clock, Clock.make()))).catch(console.error);
75+
```
4976

5077
---
5178

0 commit comments

Comments
 (0)