@effected/config-file 0.3.1 → 0.4.1

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/ConfigFile.js CHANGED
@@ -133,7 +133,7 @@ const makeImpl = (options, fs, path, resolverEnv) => {
133
133
  path: s.path,
134
134
  resolver: s.resolver
135
135
  }));
136
- const decode = (parsed, at) => Schema.decodeUnknownEffect(options.schema)(parsed).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
136
+ const decode = (parsed, at) => Schema.decodeUnknownEffect(options.schema)(parsed, options.parseOptions).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
137
137
  path: at,
138
138
  issue: error.issue
139
139
  }))));
@@ -354,7 +354,7 @@ const read = (path, options) => Effect.gen(function* () {
354
354
  cause
355
355
  })));
356
356
  const parsed = yield* options.codec.parse(raw);
357
- return yield* Schema.decodeUnknownEffect(options.schema)(parsed).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
357
+ return yield* Schema.decodeUnknownEffect(options.schema)(parsed, options.parseOptions).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new ConfigValidationError({
358
358
  path: Option.some(path),
359
359
  issue: error.issue
360
360
  }))));
package/README.md CHANGED
@@ -117,6 +117,32 @@ Effect.runPromise(program.pipe(Effect.provide(NodeFileSystem.layer))).then(conso
117
117
 
118
118
  It is deliberately read-only and discovery-free — no resolver chain, no `save`/`update`. Reach for `ConfigFile.layer` the moment either is wanted.
119
119
 
120
+ ## Rejecting keys the schema does not know
121
+
122
+ Effect's decoder ignores unknown keys by default, which for a config loader means a typo'd section is dropped in silence. The user gets no error, the setting they wrote has no effect, and nothing in the run says why. `parseOptions` threads decode options into every decode the loader performs, on `ConfigFile.layer` and `ConfigFile.read` alike:
123
+
124
+ ```ts
125
+ import { ConfigFile, ConfigResolver, JsonCodec, MergeStrategy } from "@effected/config-file";
126
+ import { Schema } from "effect";
127
+
128
+ class Settings extends Schema.Class<Settings>("Settings")({ port: Schema.Number }) {}
129
+ class SettingsConfig extends ConfigFile.Service<SettingsConfig, Settings>()("app/Settings") {}
130
+
131
+ export const SettingsLive = ConfigFile.layer(SettingsConfig, {
132
+ schema: Settings,
133
+ codec: JsonCodec,
134
+ resolvers: [ConfigResolver.upwardWalk({ filename: ".apprc" })],
135
+ strategy: MergeStrategy.firstMatch<Settings>(),
136
+ parseOptions: { onExcessProperty: "error", errors: "all" },
137
+ });
138
+ // A file carrying `{ "port": 3000, "prot": 3001 }` now fails with a
139
+ // ConfigValidationError whose issue tree names the offending path.
140
+ ```
141
+
142
+ The `validate` option cannot stand in for this: it runs on the decoded value, by which point the excess keys are already gone. Pair `onExcessProperty: "error"` with `errors: "all"` — the decoder reports only the first problem otherwise, so a file with three typos costs the user three fix-and-rerun cycles. The extra work happens only on a document that is already failing.
143
+
144
+ A schema that deliberately admits a pass-through section keeps working under `"error"` — but know why, because the shape suggests the opposite: a `Schema.StructWithRest` rest **switches excess checking off for that struct entirely**, not merely for the keys the rest covers. Structs without a rest stay strict independently, so strictness is decided per level rather than per key. Omitting `parseOptions` changes nothing, which makes turning this on a per-loader decision rather than a migration.
145
+
120
146
  ## Errors
121
147
 
122
148
  Every failure is a tagged error you route on with `Effect.catchTag`. The tags exist so that recovery can differ:
@@ -185,6 +211,7 @@ export const secret = EncryptedCodec(migrating, EncryptedCodecKey.fromPassphrase
185
211
  ## Features
186
212
 
187
213
  - `ConfigFile.Service` / `ConfigFile.layer` / `ConfigFile.testLayer` — a per-schema service class and its layers. `testLayer` seeds files into a temp directory and wires the *real* implementation over them, so tests exercise the actual pipeline rather than a stub that can drift from it.
214
+ - `parseOptions` — decode options threaded into every decode, on the layer and on `read`. `onExcessProperty: "error"` is the only way to report a typo'd section or a field the schema deliberately removed.
188
215
  - `ConfigFile.read` — the one-shot escape from the service: read, decode and validate one explicit path, schema and codec named per call, with no resolver chain and no write path.
189
216
  - `ConfigResolver` — `explicitPath`, `staticDir`, `upwardWalk`, `workspaceRoot`, `gitRoot` and `systemEtc`. A resolver's error channel is `never` by contract: every filesystem failure becomes `Option.none()`, so one unreadable tier never aborts the chain.
190
217
  - `MergeStrategy` — `firstMatch` and `layeredMerge`, combining discovered sources in priority order.
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ConfigProvider, Context, Effect, FileSystem, Layer, Option, Path, PubSub, Schema } from "effect";
1
+ import { ConfigProvider, Context, Effect, FileSystem, Layer, Option, Path, PubSub, Schema, SchemaAST } from "effect";
2
2
  //#region src/ConfigCodec.d.ts
3
3
  declare const ConfigCodecError_base: Schema.Class<ConfigCodecError, Schema.TaggedStruct<"ConfigCodecError", {
4
4
  /** The codec that failed, e.g. `"json"`. */
@@ -564,6 +564,31 @@ interface ConfigFileOptions<A, I, RR> {
564
564
  readonly strategy: MergeStrategy<A>;
565
565
  /** An optional caller-supplied check run after schema decoding. */
566
566
  readonly validate?: (value: A) => Effect.Effect<A, ConfigValidationError>;
567
+ /**
568
+ * Parse options threaded into every schema decode this performs.
569
+ *
570
+ * @remarks
571
+ * The field that matters here is `onExcessProperty`. It defaults to
572
+ * `"ignore"` in core, so a document's unknown keys are dropped silently and
573
+ * a loader cannot report a typo'd section — or enforce a field this schema
574
+ * deliberately removed. `{ onExcessProperty: "error" }` turns both into a
575
+ * {@link ConfigValidationError} whose issue names the offending path.
576
+ *
577
+ * It cannot be expressed with {@link ConfigFileOptions.validate}: that runs
578
+ * on the *decoded* value, by which point the excess keys are already gone.
579
+ *
580
+ * Keys covered by a `Schema.StructWithRest` rest are not excess, so a schema
581
+ * that deliberately admits a pass-through section keeps working under
582
+ * `"error"`.
583
+ *
584
+ * Absent, nothing changes: core's defaults apply.
585
+ *
586
+ * Pair it with `errors: "all"`. Core defaults to `"first"`, which for a
587
+ * *loader* means a file with three typos surfaces one per run — fix,
588
+ * re-run, discover the next. The extra work only happens on a document
589
+ * that is already failing.
590
+ */
591
+ readonly parseOptions?: SchemaAST.ParseOptions;
567
592
  /**
568
593
  * Where {@link ConfigFileShape.save} writes when given no explicit path.
569
594
  *
@@ -640,6 +665,14 @@ interface ConfigReadOptions<A, I> {
640
665
  * their engines stay out of the bundle.
641
666
  */
642
667
  readonly codec: ConfigCodec;
668
+ /**
669
+ * Parse options for the decode, chiefly `onExcessProperty`.
670
+ *
671
+ * @remarks
672
+ * See {@link ConfigFileOptions.parseOptions}; it means the same thing here.
673
+ * Absent, core's defaults apply and unknown keys are dropped silently.
674
+ */
675
+ readonly parseOptions?: SchemaAST.ParseOptions;
643
676
  }
644
677
  /**
645
678
  * The config file service: a per-schema service factory, its layers, and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/config-file",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "Composable config file loading for Effect: JSON, JSONC, YAML and TOML codecs, resolution strategies, and merge behaviors.",
6
6
  "keywords": [