opencode-effect-enforcer 0.2.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/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-config
|
|
3
|
+
description: Load and validate typed configuration with Config and ConfigProvider. Use this skill when reading environment variables, building structured config, providing test config, or working with .env files, JSON config, and custom config sources.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are an Effect TypeScript expert specializing in typed configuration loading, validation, and provider composition.
|
|
7
|
+
|
|
8
|
+
## Effect Source Reference
|
|
9
|
+
|
|
10
|
+
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
11
|
+
Browse and read files there directly to look up APIs, types, and implementations.
|
|
12
|
+
|
|
13
|
+
Reference these files for Config/ConfigProvider details:
|
|
14
|
+
|
|
15
|
+
- `packages/effect/CONFIG.md` — primary guide
|
|
16
|
+
- `packages/effect/src/Config.ts` — Config API source
|
|
17
|
+
- `packages/effect/src/ConfigProvider.ts` — ConfigProvider API source
|
|
18
|
+
|
|
19
|
+
## Core Imports
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { Config, ConfigProvider, Effect, Schema } from 'effect';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why Not `process.env`
|
|
26
|
+
|
|
27
|
+
Never read `process.env` directly in Effect code. `Config` provides:
|
|
28
|
+
|
|
29
|
+
1. **Type safety** — primitives decode strings into `number`, `boolean`, `Date`, `Duration`, etc.
|
|
30
|
+
2. **Validation** — invalid values produce structured `ConfigError` with clear messages
|
|
31
|
+
3. **Composability** — nest, combine, transform, and default configs declaratively
|
|
32
|
+
4. **Testability** — swap providers without mocking `process.env`
|
|
33
|
+
5. **Schema integration** — use `Config.schema` with `Schema.Struct` for complex shapes
|
|
34
|
+
|
|
35
|
+
## Config Primitives
|
|
36
|
+
|
|
37
|
+
Each constructor reads a single value and decodes it. The optional `name` parameter sets the root path segment for lookup. Omit it when the config is part of a larger `Config.schema`.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
Config.string('HOST'); // string
|
|
41
|
+
Config.nonEmptyString('HOST'); // string (rejects "")
|
|
42
|
+
Config.number('RATE'); // number (includes NaN, Infinity)
|
|
43
|
+
Config.finite('RATE'); // number (rejects NaN, Infinity)
|
|
44
|
+
Config.int('PORT'); // number (integers only)
|
|
45
|
+
Config.boolean('DEBUG'); // boolean (accepts true/false, yes/no, on/off, 1/0, y/n)
|
|
46
|
+
Config.port('PORT'); // number (integer in 1–65535)
|
|
47
|
+
Config.url('CALLBACK_URL'); // URL
|
|
48
|
+
Config.date('EXPIRES_AT'); // Date (rejects invalid dates)
|
|
49
|
+
Config.duration('TIMEOUT'); // Duration (parses "10 seconds", "500 millis", "Infinity", "-Infinity")
|
|
50
|
+
Config.logLevel('LOG_LEVEL'); // string (All|Fatal|Error|Warn|Info|Debug|Trace|None)
|
|
51
|
+
Config.redacted('API_KEY'); // Redacted<string> (hidden from logs and toString)
|
|
52
|
+
Config.literal('production', 'ENV'); // literal type (accepts only the given literal)
|
|
53
|
+
Config.literals(['development', 'production'], 'ENV'); // accepts one of several literals
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Config Combinators
|
|
57
|
+
|
|
58
|
+
### `Config.withDefault` — Fallback for Missing Keys
|
|
59
|
+
|
|
60
|
+
Only triggers when data is **missing**. Validation errors (wrong type, out of range) still propagate.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const port = Config.int('PORT').pipe(Config.withDefault(3000));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `Config.option` — Optional Values
|
|
67
|
+
|
|
68
|
+
Returns `Option.some(value)` on success, `Option.none()` when data is missing.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const maybePort = Config.option(Config.int('PORT'));
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `Config.map` — Transform a Value
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const upperHost = Config.string('HOST').pipe(
|
|
78
|
+
Config.map((s) => s.toUpperCase())
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `Config.orElse` — Fallback on Any Error
|
|
83
|
+
|
|
84
|
+
Unlike `withDefault`, this catches **all** `ConfigError`s:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const host = Config.string('HOST').pipe(
|
|
88
|
+
Config.orElse(() => Config.succeed('localhost'))
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `Config.all` — Combine Multiple Configs
|
|
93
|
+
|
|
94
|
+
Accepts a record or a tuple:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// As a record
|
|
98
|
+
const appConfig = Config.all({
|
|
99
|
+
host: Config.string('host'),
|
|
100
|
+
port: Config.int('port'),
|
|
101
|
+
debug: Config.boolean('debug')
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// As a tuple
|
|
105
|
+
const pair = Config.all([Config.string('a'), Config.int('b')]);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `Config.nested` — Scope Under a Prefix
|
|
109
|
+
|
|
110
|
+
Prepends a path segment to every key the inner config reads. With environment variables, nesting uses `_` as separator.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const dbConfig = Config.all({
|
|
114
|
+
host: Config.string('host'),
|
|
115
|
+
port: Config.int('port')
|
|
116
|
+
}).pipe(Config.nested('database'));
|
|
117
|
+
|
|
118
|
+
// Reads from env: database_host, database_port
|
|
119
|
+
// Or from JSON: { database: { host: "...", port: 5432 } }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Config.schema — Structured Config from Schema
|
|
123
|
+
|
|
124
|
+
For larger configs, use `Config.schema` with a concrete `StringTree` shape. The schema's canonical encoded shape determines whether the provider loads a scalar, object, array, or each member of a mixed-shape union.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const AppConfig = Config.schema(
|
|
128
|
+
Schema.Struct({
|
|
129
|
+
host: Schema.String,
|
|
130
|
+
port: Schema.Int,
|
|
131
|
+
debug: Schema.Boolean
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
With an optional name parameter for nesting:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const ServerConfig = Config.schema(
|
|
140
|
+
Schema.Struct({
|
|
141
|
+
host: Schema.String,
|
|
142
|
+
port: Schema.Int,
|
|
143
|
+
logLevel: Schema.Literals(['debug', 'info', 'warn', 'error'])
|
|
144
|
+
}),
|
|
145
|
+
'server' // reads from server_host, server_port, server_logLevel in env
|
|
146
|
+
);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Config Schemas for Use with `Config.schema`
|
|
150
|
+
|
|
151
|
+
| Schema | Type | Notes |
|
|
152
|
+
| --------------------------- | -------------- | ------------------------------------------ |
|
|
153
|
+
| `Config.Boolean` | `boolean` | Decodes `true/false/yes/no/on/off/1/0/y/n` |
|
|
154
|
+
| `Schema.DurationFromString` | `Duration` | Decodes duration strings; accepts `"Infinity"` / `"-Infinity"` |
|
|
155
|
+
| `Config.Port` | `number` | Integer in 1–65535 |
|
|
156
|
+
| `Config.LogLevel` | `string` | One of the standard log level literals |
|
|
157
|
+
| `Config.Record(key, value)` | `Record<K, V>` | Also parses flat `"k1=v1,k2=v2"` strings |
|
|
158
|
+
|
|
159
|
+
Plain `Schema.Array` and `Schema.Record` load structural provider children. Use `Config.Array` and `Config.Record` when a flat separated scalar should also be accepted. Opaque encodings such as `Schema.Any`, `Schema.Unknown`, and `Schema.Json` are rejected when `Config.schema` is constructed; use a concrete shape or `Schema.fromJsonString(Schema.Json)` to read scalar JSON.
|
|
160
|
+
|
|
161
|
+
Missing or unavailable representations are decoded as `undefined` before `Config.withDefault` and `Config.option` decide semantic absence. A successful decoded `undefined` or an explicitly present empty structure remains a real value and is not replaced by a default.
|
|
162
|
+
|
|
163
|
+
## Two Ways to Run a Config
|
|
164
|
+
|
|
165
|
+
### 1. Yield in `Effect.gen` — uses current ConfigProvider from service map
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const program = Effect.gen(function* () {
|
|
169
|
+
const host = yield* Config.string('HOST');
|
|
170
|
+
const port = yield* Config.int('PORT');
|
|
171
|
+
console.log(`${host}:${port}`);
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 2. Call `.parse(provider)` directly — useful for testing
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const host = Config.string('HOST');
|
|
179
|
+
const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
|
|
180
|
+
const result = Effect.runSync(host.parse(provider));
|
|
181
|
+
// "localhost"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## ConfigProvider Sources
|
|
185
|
+
|
|
186
|
+
### `ConfigProvider.fromEnv` — Environment Variables (Default)
|
|
187
|
+
|
|
188
|
+
The default provider. Path segments are joined with `_` for lookup. Env var names are split on `_` to build a tree, so `DATABASE_HOST=localhost` is accessible at both `["DATABASE_HOST"]` (flat) and `["DATABASE", "HOST"]` (nested).
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// Default — reads from process.env (merged with import.meta.env when available)
|
|
192
|
+
// No explicit provision needed; this is the default ConfigProvider.
|
|
193
|
+
|
|
194
|
+
// For testing, pass an explicit env object:
|
|
195
|
+
const provider = ConfigProvider.fromEnv({
|
|
196
|
+
env: {
|
|
197
|
+
DATABASE_HOST: 'localhost',
|
|
198
|
+
DATABASE_PORT: '5432'
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Empty strings are treated as missing by default. Pass `{ preserveEmptyStrings: true }` when an empty string is an explicit value.
|
|
204
|
+
|
|
205
|
+
### `ConfigProvider.fromEnvRecord` — Explicit Environment Records
|
|
206
|
+
|
|
207
|
+
Use `fromEnvRecord` when the environment record is supplied explicitly, especially in restricted runtimes where `fromEnv` cannot perform automatic environment detection. Unlike the `env` option of `fromEnv`, the record may contain `undefined` values; those entries are ignored.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const provider = ConfigProvider.fromEnvRecord({
|
|
211
|
+
HOST: 'localhost',
|
|
212
|
+
PORT: '3000',
|
|
213
|
+
OPTIONAL_VALUE: undefined
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `ConfigProvider.fromUnknown` — Plain JS Objects
|
|
218
|
+
|
|
219
|
+
Ideal for testing or embedding config in code. Supports nested objects and arrays. Primitive values are automatically stringified.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const provider = ConfigProvider.fromUnknown({
|
|
223
|
+
database: {
|
|
224
|
+
host: 'localhost',
|
|
225
|
+
port: 5432,
|
|
226
|
+
credentials: {
|
|
227
|
+
username: 'admin',
|
|
228
|
+
password: 'secret'
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
servers: ['server1', 'server2', 'server3']
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `ConfigProvider.fromDotEnvContents` — Parse `.env` Strings
|
|
236
|
+
|
|
237
|
+
Supports `export` prefixes, single/double/backtick quoting, inline comments, and escaped newlines.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const contents = `
|
|
241
|
+
# Database settings
|
|
242
|
+
HOST=localhost
|
|
243
|
+
PORT=3000
|
|
244
|
+
SECRET="my-secret-value"
|
|
245
|
+
`;
|
|
246
|
+
|
|
247
|
+
const provider = ConfigProvider.fromDotEnvContents(contents);
|
|
248
|
+
|
|
249
|
+
// With variable expansion:
|
|
250
|
+
const provider2 = ConfigProvider.fromDotEnvContents(
|
|
251
|
+
`PASSWORD=secret\nDB_PASS=$PASSWORD`,
|
|
252
|
+
{
|
|
253
|
+
expandVariables: true
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `ConfigProvider.fromDotEnv` — Load `.env` Files
|
|
259
|
+
|
|
260
|
+
Reads a `.env` file from disk. Returns an Effect (requires `FileSystem` in context).
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const program = Effect.gen(function* () {
|
|
264
|
+
const provider = yield* ConfigProvider.fromDotEnv();
|
|
265
|
+
// or: yield* ConfigProvider.fromDotEnv({ path: "/custom/.env" })
|
|
266
|
+
return provider;
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### `ConfigProvider.fromDir` — Directory Trees (Kubernetes ConfigMap/Secret)
|
|
271
|
+
|
|
272
|
+
Reads config from a file-system tree where each file is a leaf and each directory is a container. Requires `Path` and `FileSystem` in context.
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
/etc/myapp/
|
|
276
|
+
database/
|
|
277
|
+
host # contains "localhost"
|
|
278
|
+
port # contains "5432"
|
|
279
|
+
api_key # contains "sk-abc123"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const program = Effect.gen(function* () {
|
|
284
|
+
const provider = yield* ConfigProvider.fromDir({ rootPath: '/etc/myapp' });
|
|
285
|
+
return provider;
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### `ConfigProvider.make` — Custom Sources
|
|
290
|
+
|
|
291
|
+
Build a provider from any backing store. Return `undefined` for "not found". Only fail with `SourceError` for actual I/O errors.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
const data: Record<string, string> = {
|
|
295
|
+
host: 'localhost',
|
|
296
|
+
port: '5432'
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const provider = ConfigProvider.make((path) => {
|
|
300
|
+
const key = path.join('.');
|
|
301
|
+
const value = data[key];
|
|
302
|
+
return Effect.succeed(
|
|
303
|
+
value !== undefined ? ConfigProvider.makeValue(value) : undefined
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## ConfigProvider Combinators
|
|
309
|
+
|
|
310
|
+
### `ConfigProvider.orElse` — Fallback Sources
|
|
311
|
+
|
|
312
|
+
Falls back to a second provider when the first returns `undefined` (path not found). Does **not** catch `SourceError`.
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const envProvider = ConfigProvider.fromEnv({
|
|
316
|
+
env: { HOST: 'prod.example.com' }
|
|
317
|
+
});
|
|
318
|
+
const defaults = ConfigProvider.fromUnknown({
|
|
319
|
+
HOST: 'localhost',
|
|
320
|
+
PORT: '3000'
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const combined = ConfigProvider.orElse(envProvider, defaults);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
At the `Config` level, `Config.orElse` preserves evidence that the primary branch read provider input. Consequently, an outer `Config.withDefault` or `Config.option` does not hide a partially supplied `Config.all` group.
|
|
327
|
+
|
|
328
|
+
### `ConfigProvider.nested` — Prefix All Lookups
|
|
329
|
+
|
|
330
|
+
Prepends path segments so that all lookups are scoped:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const provider = ConfigProvider.fromEnv({
|
|
334
|
+
env: { APP_HOST: 'localhost', APP_PORT: '3000' }
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Lookups for ["HOST"] now resolve to ["APP", "HOST"]
|
|
338
|
+
const scoped = ConfigProvider.nested(provider, 'APP');
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### `ConfigProvider.constantCase` — CamelCase to SCREAMING_SNAKE_CASE
|
|
342
|
+
|
|
343
|
+
Bridges camelCase schema keys to environment variable naming:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
const provider = ConfigProvider.fromEnv({
|
|
347
|
+
env: { DATABASE_HOST: 'localhost' }
|
|
348
|
+
}).pipe(ConfigProvider.constantCase);
|
|
349
|
+
|
|
350
|
+
// path ["databaseHost"] now resolves to ["DATABASE_HOST"]
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### `ConfigProvider.mapInput` — Arbitrary Path Transforms
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
const upper = ConfigProvider.mapInput(provider, (path) =>
|
|
357
|
+
path.map((seg) => (typeof seg === 'string' ? seg.toUpperCase() : seg))
|
|
358
|
+
);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Installing a Provider
|
|
362
|
+
|
|
363
|
+
### `ConfigProvider.layer` — Replace the Active Provider
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
const TestLayer = ConfigProvider.layer(
|
|
367
|
+
ConfigProvider.fromUnknown({ port: 8080 })
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const program = Effect.gen(function* () {
|
|
371
|
+
const port = yield* Config.int('port');
|
|
372
|
+
return port;
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
Effect.runSync(Effect.provide(program, TestLayer)); // 8080
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Config-Backed Layer Constructors
|
|
379
|
+
|
|
380
|
+
Library-style services should usually expose a concrete `layer(options)` for direct use and tests, plus `layerConfig(config)` when callers need runtime configuration. Type the latter with `Config.Wrap<Options>` and decode it once with `Config.unwrap`.
|
|
381
|
+
|
|
382
|
+
`Config.Wrap<Options>` accepts either one `Config<Options>` or a recursively wrapped object whose leaves are `Config` values. It does not accept raw concrete option values.
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
export const layer = (options: ClientOptions) =>
|
|
386
|
+
Layer.effect(Client.Service, makeClient(options));
|
|
387
|
+
|
|
388
|
+
export const layerConfig = (config: Config.Wrap<ClientOptions>) =>
|
|
389
|
+
Layer.effect(
|
|
390
|
+
Client.Service,
|
|
391
|
+
Config.unwrap(config).pipe(
|
|
392
|
+
Effect.flatMap(makeClient),
|
|
393
|
+
Effect.map(Client.Service.of)
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Use `layer(options)` when options are already decoded. Use `layerConfig(...)` only at a configuration boundary; do not repeatedly read configuration inside business operations.
|
|
399
|
+
|
|
400
|
+
### `ConfigProvider.layerAdd` — Add Without Replacing
|
|
401
|
+
|
|
402
|
+
By default the new provider is a **fallback**:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
// process.env is tried first; defaults is the fallback
|
|
406
|
+
const DefaultsLayer = ConfigProvider.layerAdd(
|
|
407
|
+
ConfigProvider.fromUnknown({ HOST: 'localhost', PORT: '3000' })
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Set { asPrimary: true } to make the new provider the primary source instead
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### `Effect.provideService` — One-Off Override
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
|
|
417
|
+
|
|
418
|
+
const program = Effect.gen(function* () {
|
|
419
|
+
const host = yield* Config.string('HOST');
|
|
420
|
+
return host;
|
|
421
|
+
}).pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider));
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Testing Patterns
|
|
425
|
+
|
|
426
|
+
Always use `ConfigProvider.fromUnknown` or `ConfigProvider.fromEnvRecord({...})` in tests for deterministic, hermetic config:
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { Config, ConfigProvider, Effect } from 'effect';
|
|
430
|
+
|
|
431
|
+
// Pattern 1: .parse(provider) for direct testing
|
|
432
|
+
const config = Config.all({
|
|
433
|
+
host: Config.string('host'),
|
|
434
|
+
port: Config.int('port')
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const testProvider = ConfigProvider.fromUnknown({
|
|
438
|
+
host: 'localhost',
|
|
439
|
+
port: 5432
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const result = Effect.runSync(config.parse(testProvider));
|
|
443
|
+
// { host: "localhost", port: 5432 }
|
|
444
|
+
|
|
445
|
+
// Pattern 2: ConfigProvider.layer for program-level tests
|
|
446
|
+
const TestConfigLayer = ConfigProvider.layer(
|
|
447
|
+
ConfigProvider.fromUnknown({
|
|
448
|
+
server: { host: 'localhost', port: 3000 },
|
|
449
|
+
debug: true
|
|
450
|
+
})
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const program = Effect.gen(function* () {
|
|
454
|
+
const host = yield* Config.string('host').pipe(Config.nested('server'));
|
|
455
|
+
return host;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
Effect.runSync(Effect.provide(program, TestConfigLayer));
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Error Handling
|
|
462
|
+
|
|
463
|
+
Config operations fail with `ConfigError`, which wraps either:
|
|
464
|
+
|
|
465
|
+
- **`SourceError`** — the provider could not read data (I/O failure, permission error)
|
|
466
|
+
- **`SchemaError`** — data was found but didn't match the schema (wrong type, out of range, missing key)
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
const program = Config.int('PORT')
|
|
470
|
+
.parse(ConfigProvider.fromUnknown({ PORT: 'not-a-number' }))
|
|
471
|
+
.pipe(
|
|
472
|
+
Effect.tapError((error) =>
|
|
473
|
+
Effect.sync(() => {
|
|
474
|
+
if (error.cause._tag === 'SchemaError') {
|
|
475
|
+
console.log('Validation failed:', error.message);
|
|
476
|
+
} else {
|
|
477
|
+
console.log('Source error:', error.message);
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Important**: `Config.withDefault` and `Config.option` only recover from **missing-data** errors. Validation errors still propagate.
|
|
485
|
+
|
|
486
|
+
## Practical Example: Full Application Config
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { Config, ConfigProvider, Effect, Schema } from 'effect';
|
|
490
|
+
|
|
491
|
+
// Define structured config sections with Config.schema
|
|
492
|
+
const ServerConfig = Config.schema(
|
|
493
|
+
Schema.Struct({
|
|
494
|
+
host: Schema.String,
|
|
495
|
+
port: Schema.Int,
|
|
496
|
+
logLevel: Schema.Literals(['debug', 'info', 'warn', 'error'])
|
|
497
|
+
}),
|
|
498
|
+
'server'
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const DbConfig = Config.schema(
|
|
502
|
+
Schema.Struct({
|
|
503
|
+
url: Schema.String,
|
|
504
|
+
poolSize: Schema.Int
|
|
505
|
+
}),
|
|
506
|
+
'db'
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
// Combine with primitive configs
|
|
510
|
+
const AppConfig = Config.all({
|
|
511
|
+
server: ServerConfig,
|
|
512
|
+
db: DbConfig,
|
|
513
|
+
debug: Config.boolean('debug').pipe(Config.withDefault(false))
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// In production — just yield it, reads from process.env
|
|
517
|
+
const program = Effect.gen(function* () {
|
|
518
|
+
const config = yield* AppConfig;
|
|
519
|
+
console.log(config);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// For testing — provide a specific provider
|
|
523
|
+
const testProvider = ConfigProvider.fromUnknown({
|
|
524
|
+
server: { host: 'localhost', port: 3000, logLevel: 'debug' },
|
|
525
|
+
db: { url: 'postgres://localhost/testdb', poolSize: 5 },
|
|
526
|
+
debug: true
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
Effect.runSync(
|
|
530
|
+
program.pipe(Effect.provide(ConfigProvider.layer(testProvider)))
|
|
531
|
+
);
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
With environment variables, the same config reads:
|
|
535
|
+
|
|
536
|
+
```
|
|
537
|
+
server_host=localhost
|
|
538
|
+
server_port=3000
|
|
539
|
+
server_logLevel=debug
|
|
540
|
+
db_url=postgres://localhost/mydb
|
|
541
|
+
db_poolSize=10
|
|
542
|
+
debug=true
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
## Anti-Patterns
|
|
546
|
+
|
|
547
|
+
### NEVER read process.env directly
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
// BAD
|
|
551
|
+
const port = parseInt(process.env.PORT ?? '3000');
|
|
552
|
+
|
|
553
|
+
// GOOD
|
|
554
|
+
const port = Config.int('PORT').pipe(Config.withDefault(3000));
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### NEVER validate config manually
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
// BAD
|
|
561
|
+
const raw = process.env.LOG_LEVEL;
|
|
562
|
+
if (!['debug', 'info', 'warn', 'error'].includes(raw)) throw new Error('...');
|
|
563
|
+
|
|
564
|
+
// GOOD
|
|
565
|
+
const logLevel = Config.schema(
|
|
566
|
+
Schema.Literals(['debug', 'info', 'warn', 'error']),
|
|
567
|
+
'LOG_LEVEL'
|
|
568
|
+
);
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### NEVER mock process.env in tests
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
// BAD
|
|
575
|
+
process.env.HOST = 'localhost';
|
|
576
|
+
|
|
577
|
+
// GOOD
|
|
578
|
+
const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
|
|
579
|
+
Effect.runSync(config.parse(provider));
|
|
580
|
+
```
|