@effected/yaml 0.23.0 → 0.25.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/Yaml.js CHANGED
@@ -171,11 +171,14 @@ var YamlParseError = class extends Schema.TaggedError()("YamlParseError", {
171
171
  }
172
172
  };
173
173
  /**
174
- * Stringification failure (the circular-reference guard), carrying structured
175
- * {@link YamlDiagnostic} entries and the offending value. Raised by
176
- * {@link Yaml.stringify}, `YamlDocument#stringify` and the encode direction of
177
- * the schema factories. The error itself has no `code` field: read the code
178
- * from the diagnostics — `error.diagnostics[0].code` is the primary failure.
174
+ * Stringification failure, carrying structured {@link YamlDiagnostic} entries
175
+ * and the offending value. Raised by {@link Yaml.stringify},
176
+ * `YamlDocument#stringify` and the encode direction of the schema factories
177
+ * on a circular reference (`CircularReference`), a value nested past the
178
+ * stringifier's recursion budget (`NestingDepthExceeded`) or a value with no
179
+ * YAML representation (`UnrepresentableValue` — a function or a symbol). The
180
+ * error itself has no `code` field: read the code from the diagnostics —
181
+ * `error.diagnostics[0].code` is the primary failure.
179
182
  *
180
183
  * @public
181
184
  */
@@ -224,13 +227,17 @@ const aliasCountExceededError = (message, text) => new YamlParseError({
224
227
  /**
225
228
  * Map an internal stringifier throw to its typed {@link YamlStringifyError},
226
229
  * or return `undefined` for any other defect (which the caller re-throws).
227
- * Shared by the Effect and synchronous stringify paths so both surface the
228
- * hardening guards (circular reference, nesting-depth cap) identically.
230
+ * Shared by every stringify path — `Yaml.stringify`/`stringifyResult`, the
231
+ * schema encoders, `YamlDocument#stringifyResult` and `YamlFormat.modify`'s
232
+ * backstop — so all of them surface the hardening guards (circular
233
+ * reference, unrepresentable value, nesting-depth cap) identically.
234
+ *
235
+ * @internal
229
236
  */
230
237
  const stringifyDefectToError = (defect, value) => {
231
238
  if (defect instanceof StringifyFailure) return new YamlStringifyError({
232
239
  diagnostics: [YamlDiagnostic.make({
233
- code: "CircularReference",
240
+ code: defect.code,
234
241
  message: defect.reason,
235
242
  offset: 0,
236
243
  length: 0,
@@ -266,12 +273,13 @@ const parseResultImpl = (text, options) => {
266
273
  input: text
267
274
  }));
268
275
  const anchors = /* @__PURE__ */ new Map();
269
- try {
270
- return Result.succeed(nodeToJsValue(doc.contents, anchors, options?.maxAliasCount ?? 100));
271
- } catch (defect) {
272
- if (defect instanceof AliasExpansionBudgetExceeded) return Result.fail(aliasCountExceededError(defect.message, text));
273
- throw defect;
274
- }
276
+ return Result.try({
277
+ try: () => nodeToJsValue(doc.contents, anchors, options?.maxAliasCount ?? 100),
278
+ catch: (defect) => {
279
+ if (defect instanceof AliasExpansionBudgetExceeded) return aliasCountExceededError(defect.message, text);
280
+ throw defect;
281
+ }
282
+ });
275
283
  };
276
284
  /**
277
285
  * Synchronous multi-document parse returning a `Result`. Mirrors
@@ -304,19 +312,18 @@ const parseAllResultImpl = (text, options) => {
304
312
  };
305
313
  /**
306
314
  * Synchronous stringify returning a `Result`. Mirrors {@link Yaml.stringify}
307
- * without the Effect wrapper: a circular reference or a value nested past the
308
- * recursion budget yields a `Failure` carrying a typed {@link YamlStringifyError},
309
- * never a thrown defect.
315
+ * without the Effect wrapper: a circular reference, a function or symbol
316
+ * value, or a value nested past the recursion budget yields a `Failure`
317
+ * carrying a typed {@link YamlStringifyError}, never a thrown defect.
310
318
  */
311
- const stringifyResultImpl = (value, options) => {
312
- try {
313
- return Result.succeed(stringifyValue(value, toStringifyInput(options)));
314
- } catch (defect) {
319
+ const stringifyResultImpl = (value, options) => Result.try({
320
+ try: () => stringifyValue(value, toStringifyInput(options)),
321
+ catch: (defect) => {
315
322
  const error = stringifyDefectToError(defect, value);
316
- if (error !== void 0) return Result.fail(error);
323
+ if (error !== void 0) return error;
317
324
  throw defect;
318
325
  }
319
- };
326
+ });
320
327
  /**
321
328
  * Collect the raw diagnostics that make a composed document a parse failure:
322
329
  * every fatal-code error, plus DuplicateKey warnings promoted to errors when
@@ -328,14 +335,6 @@ const failureRecords = (doc, uniqueKeys) => {
328
335
  if (fatal.length > 0) return fatal;
329
336
  return uniqueKeys ? doc.warnings.filter((w) => w.code === "DuplicateKey") : [];
330
337
  };
331
- const stringifyOrFail = (value, options) => Effect.try({
332
- try: () => stringifyValue(value, toStringifyInput(options)),
333
- catch: (defect) => {
334
- const error = stringifyDefectToError(defect, value);
335
- if (error !== void 0) return error;
336
- throw defect;
337
- }
338
- });
339
338
  /**
340
339
  * Static entry points for YAML parsing, stringification, comment stripping,
341
340
  * semantic equality and the schema factories. Not instantiable.
@@ -405,10 +404,14 @@ var Yaml = class Yaml {
405
404
  static parseAll = Effect.fn("Yaml.parseAll")((text, options) => Effect.fromResult(Yaml.parseAllResult(text, options)));
406
405
  /**
407
406
  * Stringify a plain JavaScript value as YAML. Fails with
408
- * {@link YamlStringifyError} on circular references (`CircularReference`)
409
- * or on a value nested deeper than the stringifier's recursion budget
410
- * (`NestingDepthExceeded`) — both surface through the typed error channel
411
- * rather than as an unhandled stack-overflow defect.
407
+ * {@link YamlStringifyError} on circular references (`CircularReference`),
408
+ * on a value nested deeper than the stringifier's recursion budget
409
+ * (`NestingDepthExceeded`) and on a function or symbol anywhere in the
410
+ * value (`UnrepresentableValue`) — all surface through the typed error
411
+ * channel, never as a stack-overflow defect and never as the function's
412
+ * source text or `Symbol(s)` written into the document as a string. A
413
+ * `bigint` is an integer and renders bare, exactly as `YamlFormat.modify`
414
+ * writes one; `null` and `undefined` both render as `null`.
412
415
  *
413
416
  * @remarks
414
417
  * A `"<<"` object key is emitted **quoted** (`'<<': …`). This is the
@@ -425,9 +428,7 @@ var Yaml = class Yaml {
425
428
  * @returns An `Effect` that succeeds with the YAML text, or fails with
426
429
  * {@link YamlStringifyError}.
427
430
  */
428
- static stringify = Effect.fn("Yaml.stringify")(function* (value, options) {
429
- return yield* stringifyOrFail(value, options);
430
- });
431
+ static stringify = Effect.fn("Yaml.stringify")((value, options) => Effect.fromResult(stringifyResultImpl(value, options)));
431
432
  /**
432
433
  * Synchronous single-document parse, returning a `Result` instead of
433
434
  * an `Effect`. A pure escape hatch for config-time callers that cannot
@@ -510,10 +511,11 @@ var Yaml = class Yaml {
510
511
  * The pure counterpart to {@link Yaml.stringify} for config-time callers
511
512
  * that cannot `await`.
512
513
  *
513
- * Preserves the package contract — a circular reference (`CircularReference`)
514
- * or a value nested past the recursion budget (`NestingDepthExceeded`)
515
- * yields a `Failure` carrying a {@link YamlStringifyError} rather than a
516
- * thrown stack-overflow defect; the method never throws.
514
+ * Preserves the package contract — a circular reference (`CircularReference`),
515
+ * a value nested past the recursion budget (`NestingDepthExceeded`) or a
516
+ * function or symbol value (`UnrepresentableValue`) yields a `Failure`
517
+ * carrying a {@link YamlStringifyError} rather than a thrown defect; the
518
+ * method never throws.
517
519
  *
518
520
  * @example
519
521
  * ```ts
@@ -631,7 +633,7 @@ var Yaml = class Yaml {
631
633
  static fromString(options) {
632
634
  return Schema.String.pipe(Schema.decodeTo(Schema.Unknown, SchemaTransformation.transformEffect({
633
635
  decode: (input) => Yaml.parse(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, input))),
634
- encode: (value) => stringifyOrFail(value).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, value)))
636
+ encode: (value) => Effect.fromResult(stringifyResultImpl(value)).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, value)))
635
637
  })));
636
638
  }
637
639
  /**
@@ -655,7 +657,7 @@ var Yaml = class Yaml {
655
657
  if (values.length === 0) return "";
656
658
  const parts = [];
657
659
  for (let index = 0; index < values.length; index++) {
658
- const yaml = yield* stringifyOrFail(values[index]).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, values)));
660
+ const yaml = yield* Effect.fromResult(stringifyResultImpl(values[index])).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue({ message: error.message }, values)));
659
661
  parts.push(index > 0 ? `---\n${yaml}` : yaml);
660
662
  }
661
663
  return parts.join("");
@@ -774,4 +776,4 @@ function deepEqualValues(a, b) {
774
776
  return false;
775
777
  }
776
778
 
777
- export { Yaml, YamlParseError, YamlParseOptions, YamlStringifyError, YamlStringifyOptions };
779
+ export { Yaml, YamlParseError, YamlParseOptions, YamlStringifyError, YamlStringifyOptions, stringifyDefectToError };
package/YamlDiagnostic.js CHANGED
@@ -21,7 +21,10 @@ const YamlParseErrorCode = Schema.Literals(YAML_PARSE_ERROR_CODES);
21
21
  */
22
22
  const YamlComposerErrorCode = Schema.Literals(YAML_COMPOSE_ERROR_CODES);
23
23
  /**
24
- * Error codes emitted by the stringifier (the circular-reference guard).
24
+ * Error codes emitted by the stringifier: `CircularReference` (the value or
25
+ * synthetic AST points back at itself) and `UnrepresentableValue` (a function
26
+ * or symbol, which has no YAML rendering and is never written as its
27
+ * `String()` form).
25
28
  *
26
29
  * @public
27
30
  */
package/YamlDocument.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { YamlNode } from "./YamlNode.js";
2
2
  import { composeAllDocuments, composeFirstDocument } from "./internal/composer/document.js";
3
3
  import { isFatalCode } from "./internal/diagnostics.js";
4
- import { StringifyDepthExceeded, StringifyFailure, stringifyDocument } from "./internal/stringifier.js";
4
+ import { stringifyDocument } from "./internal/stringifier.js";
5
5
  import { YamlDiagnostic } from "./YamlDiagnostic.js";
6
- import { YamlParseError, YamlStringifyError } from "./Yaml.js";
7
- import { Effect, Schema, SchemaIssue, SchemaTransformation } from "effect";
6
+ import { YamlParseError, stringifyDefectToError } from "./Yaml.js";
7
+ import { Effect, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
8
8
 
9
9
  // src/YamlDocument.ts
10
10
  /**
@@ -58,11 +58,47 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
58
58
  hasDocumentEnd: Schema.optionalKey(Schema.Boolean),
59
59
  hasDocumentStartTab: Schema.optionalKey(Schema.Boolean)
60
60
  }) {
61
+ /**
62
+ * Synchronous single-document parse, returning a `Result` instead of an
63
+ * `Effect`. This is the primitive {@link YamlDocument.parse} is defined in
64
+ * terms of — a synchronous host can call it without an Effect runtime.
65
+ * Keeps the full AST, directives and recovered diagnostics; fails with the
66
+ * aggregate {@link YamlParseError} when any fatal-code diagnostic is
67
+ * present, and non-fatal diagnostics are data on the returned document.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { YamlDocument } from "@effected/yaml";
72
+ * import { Result } from "effect";
73
+ *
74
+ * const result = YamlDocument.parseResult("name: Alice\n");
75
+ * if (Result.isSuccess(result)) {
76
+ * console.log(result.success.toValue()); // => { name: "Alice" }
77
+ * }
78
+ * ```
79
+ *
80
+ * @param text - The YAML source to parse.
81
+ * @param options - Optional {@link YamlParseOptions}; defaults apply for
82
+ * omitted fields.
83
+ * @returns A `Result` succeeding with the {@link YamlDocument}, or failing
84
+ * with {@link YamlParseError}.
85
+ */
86
+ static parseResult(text, options) {
87
+ const raw = composeFirstDocument(text, toParseInput(options));
88
+ const fatal = raw.errors.filter((e) => isFatalCode(e.code));
89
+ if (fatal.length > 0) return Result.fail(new YamlParseError({
90
+ diagnostics: fatal.map((e) => YamlDiagnostic.fromRaw(e, text)),
91
+ input: text
92
+ }));
93
+ return Result.succeed(fromRawDocument(raw, text));
94
+ }
61
95
  /**
62
96
  * Parse a single YAML document, keeping the full AST, directives and
63
97
  * recovered diagnostics. Fails with the aggregate {@link YamlParseError}
64
98
  * when any fatal-code diagnostic is present; non-fatal diagnostics are
65
- * data on the returned document.
99
+ * data on the returned document. Defined in terms of
100
+ * {@link YamlDocument.parseResult} — synchronous callers can use that
101
+ * variant directly.
66
102
  *
67
103
  * @param text - The YAML source to parse.
68
104
  * @param options - Optional {@link YamlParseOptions}; defaults apply for
@@ -70,19 +106,46 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
70
106
  * @returns An `Effect` that succeeds with the {@link YamlDocument}, or fails
71
107
  * with {@link YamlParseError}.
72
108
  */
73
- static parse = Effect.fn("YamlDocument.parse")(function* (text, options) {
74
- const raw = composeFirstDocument(text, toParseInput(options));
75
- const fatal = raw.errors.filter((e) => isFatalCode(e.code));
76
- if (fatal.length > 0) return yield* new YamlParseError({
109
+ static parse = Effect.fn("YamlDocument.parse")((text, options) => Effect.fromResult(YamlDocument.parseResult(text, options)));
110
+ /**
111
+ * Synchronous multi-document parse, returning a `Result` instead of an
112
+ * `Effect` — the primitive {@link YamlDocument.parseAll} is defined in
113
+ * terms of. Any fatal diagnostic in any document — or a stream-level
114
+ * directive-placement error — fails the whole parse with one aggregate
115
+ * {@link YamlParseError}.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * import { YamlDocument } from "@effected/yaml";
120
+ * import { Result } from "effect";
121
+ *
122
+ * const result = YamlDocument.parseAllResult("a: 1\n---\nb: 2\n");
123
+ * if (Result.isSuccess(result)) {
124
+ * console.log(result.success.length); // => 2
125
+ * }
126
+ * ```
127
+ *
128
+ * @param text - The YAML stream to parse.
129
+ * @param options - Optional {@link YamlParseOptions}; defaults apply for
130
+ * omitted fields.
131
+ * @returns A `Result` succeeding with one {@link YamlDocument} per
132
+ * document, or failing with {@link YamlParseError}.
133
+ */
134
+ static parseAllResult(text, options) {
135
+ const { documents, streamErrors } = composeAllDocuments(text, toParseInput(options));
136
+ const fatal = [...streamErrors.filter((e) => e.code === "InvalidDirective"), ...documents.flatMap((d) => d.errors.filter((e) => isFatalCode(e.code)))];
137
+ if (fatal.length > 0) return Result.fail(new YamlParseError({
77
138
  diagnostics: fatal.map((e) => YamlDiagnostic.fromRaw(e, text)),
78
139
  input: text
79
- });
80
- return fromRawDocument(raw, text);
81
- });
140
+ }));
141
+ return Result.succeed(documents.map((raw) => fromRawDocument(raw, text)));
142
+ }
82
143
  /**
83
144
  * Parse a multi-document YAML stream into one {@link YamlDocument} per
84
145
  * document. Any fatal diagnostic in any document — or a stream-level
85
- * directive-placement error — fails the whole Effect.
146
+ * directive-placement error — fails the whole Effect. Defined in terms of
147
+ * {@link YamlDocument.parseAllResult} — synchronous callers can use that
148
+ * variant directly.
86
149
  *
87
150
  * @param text - The YAML stream to parse.
88
151
  * @param options - Optional {@link YamlParseOptions}; defaults apply for
@@ -90,15 +153,7 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
90
153
  * @returns An `Effect` that succeeds with one {@link YamlDocument} per
91
154
  * document, or fails with {@link YamlParseError}.
92
155
  */
93
- static parseAll = Effect.fn("YamlDocument.parseAll")(function* (text, options) {
94
- const { documents, streamErrors } = composeAllDocuments(text, toParseInput(options));
95
- const fatal = [...streamErrors.filter((e) => e.code === "InvalidDirective"), ...documents.flatMap((d) => d.errors.filter((e) => isFatalCode(e.code)))];
96
- if (fatal.length > 0) return yield* new YamlParseError({
97
- diagnostics: fatal.map((e) => YamlDiagnostic.fromRaw(e, text)),
98
- input: text
99
- });
100
- return documents.map((raw) => fromRawDocument(raw, text));
101
- });
156
+ static parseAll = Effect.fn("YamlDocument.parseAll")((text, options) => Effect.fromResult(YamlDocument.parseAllResult(text, options)));
102
157
  /**
103
158
  * A `Schema<YamlDocument, string>` decoding YAML text into a full
104
159
  * document (AST, directives, diagnostics) and encoding a document back to
@@ -119,11 +174,19 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
119
174
  }
120
175
  /**
121
176
  * Stringify this document (contents, directives and framing) as YAML.
122
- * Fails with {@link YamlStringifyError} on circular references introduced
123
- * into a synthetic AST (`CircularReference`) or on a synthetic AST nested
124
- * deeper than the stringifier's recursion budget (`NestingDepthExceeded`)
125
- * — both surface through the typed error channel rather than as an
126
- * unhandled stack-overflow defect.
177
+ * A parsed document always stringifies; the failures belong to hand-built
178
+ * ASTs and surface as {@link YamlStringifyError} through the typed error
179
+ * channel, never as an unhandled stack-overflow defect or as corrupted
180
+ * output: a cycle in the AST fails `CircularReference` on the first
181
+ * re-entry of an ancestor collection (the node path keeps the same
182
+ * ancestor set the value path does), a tree nested deeper than the
183
+ * stringifier's recursion budget fails `NestingDepthExceeded`, and a
184
+ * scalar node holding anything but `null`, a boolean, a number, a bigint
185
+ * or a string — a function, a symbol, an object — fails
186
+ * `UnrepresentableValue` rather than being written as its `String()` form.
187
+ * Defined in terms of {@link YamlDocument.stringifyResult} behind a
188
+ * `YamlDocument.stringify` span — synchronous callers can use that variant
189
+ * directly.
127
190
  *
128
191
  * @remarks
129
192
  * `YamlStringifyOptions.lineWidth` is not honored here: column-based
@@ -142,31 +205,45 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
142
205
  * {@link YamlStringifyError}.
143
206
  */
144
207
  stringify(options) {
145
- return Effect.try({
208
+ return Effect.suspend(() => Effect.fromResult(this.stringifyResult(options))).pipe(Effect.withSpan("YamlDocument.stringify"));
209
+ }
210
+ /**
211
+ * Synchronous stringify, returning a `Result` instead of an `Effect` — the
212
+ * primitive {@link YamlDocument.stringify} is defined in terms of, so a
213
+ * synchronous host can round-trip a document parsed with
214
+ * {@link YamlDocument.parseResult} without an Effect runtime. Fails with
215
+ * {@link YamlStringifyError} exactly where `stringify` does: a hand-built
216
+ * AST containing a cycle (`CircularReference`), one nested deeper than the
217
+ * stringifier's recursion budget (`NestingDepthExceeded`) or one whose
218
+ * scalar node holds a value with no YAML form (`UnrepresentableValue`).
219
+ *
220
+ * @remarks
221
+ * `YamlStringifyOptions.lineWidth` is not honored here, for the reason
222
+ * given on {@link YamlDocument.stringify}.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * import { YamlDocument } from "@effected/yaml";
227
+ * import { Result } from "effect";
228
+ *
229
+ * const parsed = YamlDocument.parseResult("# keep\nname: Alice\n");
230
+ * if (Result.isSuccess(parsed)) {
231
+ * const text = parsed.success.stringifyResult();
232
+ * if (Result.isSuccess(text)) console.log(text.success); // => "# keep\nname: Alice\n"
233
+ * }
234
+ * ```
235
+ *
236
+ * @param options - Optional {@link YamlStringifyOptions}; defaults apply for
237
+ * omitted fields.
238
+ * @returns A `Result` succeeding with the YAML text, or failing with
239
+ * {@link YamlStringifyError}.
240
+ */
241
+ stringifyResult(options) {
242
+ return Result.try({
146
243
  try: () => stringifyDocument(toRawDocument(this), toStringifyInput(options)),
147
244
  catch: (defect) => {
148
- if (defect instanceof StringifyFailure) return new YamlStringifyError({
149
- diagnostics: [YamlDiagnostic.make({
150
- code: "CircularReference",
151
- message: defect.reason,
152
- offset: 0,
153
- length: 0,
154
- line: 0,
155
- character: 0
156
- })],
157
- value: this
158
- });
159
- if (defect instanceof StringifyDepthExceeded) return new YamlStringifyError({
160
- diagnostics: [YamlDiagnostic.make({
161
- code: "NestingDepthExceeded",
162
- message: defect.message,
163
- offset: 0,
164
- length: 0,
165
- line: 0,
166
- character: 0
167
- })],
168
- value: this
169
- });
245
+ const error = stringifyDefectToError(defect, this);
246
+ if (error !== void 0) return error;
170
247
  throw defect;
171
248
  }
172
249
  });
package/YamlFormat.js CHANGED
@@ -3,11 +3,11 @@ import { EMPTY_DOCUMENT, composeAllDocuments, composeFirstDocumentCounted } from
3
3
  import { isFatalCode } from "./internal/diagnostics.js";
4
4
  import { renderDoubleQuoted, renderSingleQuoted, stringifyDocument, stringifyValue, stripNodeComments } from "./internal/stringifier.js";
5
5
  import { YamlDiagnostic } from "./YamlDiagnostic.js";
6
- import { YamlStringifyOptions } from "./Yaml.js";
6
+ import { YamlStringifyOptions, stringifyDefectToError } from "./Yaml.js";
7
7
  import { YamlEdit, YamlRange } from "./YamlEdit.js";
8
8
  import { computeEdits } from "./internal/diff.js";
9
9
  import { requoteScalarText } from "./internal/requote.js";
10
- import { Effect, Schema } from "effect";
10
+ import { Effect, Result, Schema } from "effect";
11
11
 
12
12
  // src/YamlFormat.ts
13
13
  /**
@@ -259,11 +259,18 @@ function formatStream(text, documents, streamErrors, options) {
259
259
  * Before this was recursive, a mapping or sequence value fell through to the
260
260
  * node stringifier's `String(value)` fallback and landed in the document as
261
261
  * the literal text `[object Object]` — a silent corruption that only surfaced
262
- * on the next read.
262
+ * on the next read. Every other value becomes a plain {@link YamlScalar} and
263
+ * renders exactly as the value path renders it: a `bigint` bare, `undefined`
264
+ * as `null`.
263
265
  *
264
- * The two conditions with no finite rendering fail typed rather than hanging
265
- * or overflowing the stack: a cycle raises `CircularReference` and a graph
266
- * deeper than `MAX_NESTING_DEPTH` raises `NestingDepthExceeded`. `seen` holds
266
+ * Three conditions fail typed rather than hanging, overflowing the stack or
267
+ * corrupting the document: a cycle raises `CircularReference`, a graph that
268
+ * would sit deeper than `MAX_NESTING_DEPTH` in the output document raises
269
+ * `NestingDepthExceeded`, and a function or symbol — which has no YAML
270
+ * rendering, and whose `String()` form the value path refuses to write too —
271
+ * raises `UnrepresentableValue`. `depth` is measured in the stringifier's own
272
+ * units — one per node from the document root — so the caller passes the
273
+ * target's depth, not zero. `seen` holds
267
274
  * the ancestors of the value being lowered, so a value repeated across
268
275
  * siblings (a shared, acyclic sub-object) is lowered twice rather than
269
276
  * rejected.
@@ -299,19 +306,31 @@ function jsValueToNode(value, seen, depth) {
299
306
  seen.delete(value);
300
307
  }
301
308
  }
302
- return YamlScalar.make({
309
+ if (typeof value === "function" || typeof value === "symbol") throw new ModifyFailure("UnrepresentableValue", `A ${typeof value} value has no YAML representation`, 0, 0);
310
+ return value === null || value === void 0 ? YamlScalar.make({
311
+ value: null,
312
+ style: "plain",
313
+ raw: "null",
314
+ offset: 0,
315
+ length: 0
316
+ }) : YamlScalar.make({
303
317
  value,
304
318
  style: "plain",
305
319
  offset: 0,
306
320
  length: 0
307
321
  });
308
322
  }
309
- /** Entry point for {@link jsValueToNode}: one `seen` set per lowering. */
310
- function lowerValue(value) {
311
- return jsValueToNode(value, /* @__PURE__ */ new Set(), 0);
323
+ /**
324
+ * Entry point for {@link jsValueToNode}: one `seen` set per lowering, counting
325
+ * depth from `targetDepth` — the depth the value will occupy in the output
326
+ * document (the path length: a root replacement sits at 0, a value one key
327
+ * down at 1).
328
+ */
329
+ function lowerValue(value, targetDepth) {
330
+ return jsValueToNode(value, /* @__PURE__ */ new Set(), targetDepth);
312
331
  }
313
332
  function modifyDocument(doc, path, value) {
314
- if (path.length === 0) return value === void 0 ? null : lowerValue(value);
333
+ if (path.length === 0) return value === void 0 ? null : lowerValue(value, 0);
315
334
  if (doc.contents === null) throw new ModifyFailure("EmptyDocument", "Cannot navigate path in empty document", 0, 0);
316
335
  return modifyNode(doc.contents, path, 0, value);
317
336
  }
@@ -327,7 +346,7 @@ function modifyNode(node, path, depth, value) {
327
346
  newItems.splice(pairIndex, 1);
328
347
  return rebuildMap(node, newItems);
329
348
  }
330
- const newValueNode = lowerValue(value);
349
+ const newValueNode = lowerValue(value, depth + 1);
331
350
  if (pairIndex >= 0) {
332
351
  const newItems = [...node.items];
333
352
  const oldPair = newItems[pairIndex];
@@ -367,8 +386,8 @@ function modifyNode(node, path, depth, value) {
367
386
  const newItems = [...node.items];
368
387
  if (value === void 0) {
369
388
  if (idx < newItems.length) newItems.splice(idx, 1);
370
- } else if (idx < newItems.length) newItems[idx] = lowerValue(value);
371
- else newItems.push(lowerValue(value));
389
+ } else if (idx < newItems.length) newItems[idx] = lowerValue(value, depth + 1);
390
+ else newItems.push(lowerValue(value, depth + 1));
372
391
  return rebuildSeq(node, newItems);
373
392
  }
374
393
  if (idx >= node.items.length) throw new ModifyFailure("InvalidIndex", `Index ${idx} out of bounds`, node.offset, node.length);
@@ -678,7 +697,9 @@ var YamlFormat = class YamlFormat {
678
697
  return YamlEdit.applyAll(text, YamlFormat.format(text, range, options));
679
698
  }
680
699
  /**
681
- * Compute the edits that insert, replace, or remove a value at `path`.
700
+ * Compute the edits that insert, replace, or remove a value at `path`,
701
+ * synchronously — the primitive {@link YamlFormat.modify} is defined in
702
+ * terms of, so a synchronous host can call it without an Effect runtime.
682
703
  * Passing `value === undefined` removes the target key/element; a missing
683
704
  * insertion target appends after the last pair/element. Fails with
684
705
  * {@link YamlModificationError} on a fatal parse error or a structural
@@ -688,7 +709,10 @@ var YamlFormat = class YamlFormat {
688
709
  * sequence and any other non-null object as a block mapping over its own
689
710
  * enumerable string keys, recursively — the same lowering
690
711
  * {@link Yaml.stringify} applies to the same value, so `modify` and
691
- * `stringify` agree on what a given JavaScript value means. For a synthesized
712
+ * `stringify` agree on what a given JavaScript value means: a `bigint`
713
+ * renders bare (`a: 10`, never `a: "10"`), `null` renders as `null`, and a
714
+ * function or symbol fails `UnrepresentableValue` on both paths rather
715
+ * than landing in the document as its `String()` form. For a synthesized
692
716
  * subtree (an object/array value) only the surrounding document is
693
717
  * preserved byte-for-byte; the subtree carries no comments and takes the
694
718
  * stringifier's styles.
@@ -712,10 +736,13 @@ var YamlFormat = class YamlFormat {
712
736
  * whole document, which normalises line endings to LF, applies those
713
737
  * options, and renders the replacement in the stringifier's styles.
714
738
  *
715
- * Two replacement values have no finite rendering and fail typed rather
716
- * than hanging or overflowing the stack: one containing a circular
717
- * reference (`CircularReference`) and one nesting deeper than 256 levels
718
- * (`NestingDepthExceeded`).
739
+ * Three replacement values have no rendering and fail typed rather than
740
+ * hanging, overflowing the stack or corrupting the document: one
741
+ * containing a circular reference (`CircularReference`), one that would
742
+ * sit deeper than 256 levels in the output document
743
+ * (`NestingDepthExceeded`) and one containing a function or symbol
744
+ * (`UnrepresentableValue`). The depth is counted from the document root,
745
+ * so the same value can fit at `[]` and fail a few keys down.
719
746
  *
720
747
  * **Single-document contract.** A `path` carries no document index, so on
721
748
  * a multi-document stream there is no rule for which document it names —
@@ -740,63 +767,109 @@ var YamlFormat = class YamlFormat {
740
767
  * target instead.
741
768
  * @param options - Optional {@link YamlStringifyOptions} for the
742
769
  * re-stringify step.
743
- * @returns An `Effect` that succeeds with the edits to apply (via
744
- * `YamlEdit.applyAll`), or fails with {@link YamlModificationError}.
770
+ * @returns A `Result` succeeding with the edits to apply (via
771
+ * `YamlEdit.applyAll`), or failing with {@link YamlModificationError}.
745
772
  *
746
773
  * @remarks
747
774
  * `options` is a bare {@link YamlStringifyOptions} — it controls only the
748
775
  * internal re-stringify step, not a range (there is no range to restrict
749
776
  * for a path-targeted modification).
750
777
  */
751
- static modify = Effect.fn("YamlFormat.modify")(function* (text, path, value, options) {
752
- const { document: doc, documentCount } = composeFirstDocumentCounted(text, {});
753
- if (documentCount > 1) return yield* new YamlModificationError({
778
+ static modifyResult(text, path, value, options) {
779
+ const fail = (diagnostics) => Result.fail(new YamlModificationError({
754
780
  path,
755
- diagnostics: [YamlDiagnostic.fromRaw({
756
- code: "MultiDocumentStream",
757
- message: `Cannot modify a multi-document stream (${documentCount} documents): modify re-emits exactly one document`,
758
- offset: 0,
759
- length: 0
760
- }, text)]
761
- });
781
+ diagnostics
782
+ }));
783
+ const { document: doc, documentCount } = composeFirstDocumentCounted(text, {});
784
+ if (documentCount > 1) return fail([YamlDiagnostic.fromRaw({
785
+ code: "MultiDocumentStream",
786
+ message: `Cannot modify a multi-document stream (${documentCount} documents): modify re-emits exactly one document`,
787
+ offset: 0,
788
+ length: 0
789
+ }, text)]);
762
790
  const fatal = doc.errors.filter((e) => isFatalCode(e.code));
763
- if (fatal.length > 0) return yield* new YamlModificationError({
764
- path,
765
- diagnostics: fatal.map((e) => YamlDiagnostic.fromRaw(e, text))
766
- });
767
- if (doc.directives.length > 0) return yield* new YamlModificationError({
768
- path,
769
- diagnostics: [YamlDiagnostic.fromRaw({
770
- code: "DirectiveCarryingDocument",
771
- message: "Cannot modify a document carrying %YAML/%TAG directives: modify does not re-emit directive lines, and dropping a %TAG orphans the shorthand tags that depend on it",
772
- offset: 0,
773
- length: 0
774
- }, text)]
775
- });
791
+ if (fatal.length > 0) return fail(fatal.map((e) => YamlDiagnostic.fromRaw(e, text)));
792
+ if (doc.directives.length > 0) return fail([YamlDiagnostic.fromRaw({
793
+ code: "DirectiveCarryingDocument",
794
+ message: "Cannot modify a document carrying %YAML/%TAG directives: modify does not re-emit directive lines, and dropping a %TAG orphans the shorthand tags that depend on it",
795
+ offset: 0,
796
+ length: 0
797
+ }, text)]);
776
798
  const regional = tryRegionalScalarEdit(text, doc, path, value, options);
777
- if (regional !== void 0) return regional.map((e) => YamlEdit.make(e));
799
+ if (regional !== void 0) return Result.succeed(regional.map((e) => YamlEdit.make(e)));
778
800
  let newContents;
779
801
  try {
780
802
  newContents = modifyDocument(doc, path, value);
781
803
  } catch (err) {
782
804
  if (!(err instanceof ModifyFailure)) throw err;
783
- return yield* new YamlModificationError({
784
- path,
785
- diagnostics: [YamlDiagnostic.fromRaw({
786
- code: err.code,
787
- message: err.message,
788
- offset: err.offset,
789
- length: err.length
790
- }, text)]
791
- });
805
+ return fail([YamlDiagnostic.fromRaw({
806
+ code: err.code,
807
+ message: err.message,
808
+ offset: err.offset,
809
+ length: err.length
810
+ }, text)]);
792
811
  }
793
812
  const outputDoc = {
794
813
  ...doc,
795
814
  contents: newContents
796
815
  };
797
- const formatted = stringifyDocument(outputDoc, toStringifyInput(options));
798
- return computeEdits(text, withSourceBom(text, formatted)).map((e) => YamlEdit.make(e));
799
- });
816
+ let formatted;
817
+ try {
818
+ formatted = stringifyDocument(outputDoc, toStringifyInput(options));
819
+ } catch (defect) {
820
+ const error = stringifyDefectToError(defect, value);
821
+ if (error === void 0) throw defect;
822
+ return fail(error.diagnostics);
823
+ }
824
+ return Result.succeed(computeEdits(text, withSourceBom(text, formatted)).map((e) => YamlEdit.make(e)));
825
+ }
826
+ /**
827
+ * Compute the edits that insert, replace, or remove a value at `path`.
828
+ * Defined in terms of {@link YamlFormat.modifyResult}, which carries the
829
+ * full contract — value lowering, region-confined scalar replacement, the
830
+ * single-document contract and the directive refusal — behind a
831
+ * `YamlFormat.modify` span; synchronous callers can use that variant
832
+ * directly.
833
+ *
834
+ * @param text - The YAML source to modify.
835
+ * @param path - The location to insert, replace or remove.
836
+ * @param value - The JavaScript value to write; `undefined` removes the
837
+ * target instead.
838
+ * @param options - Optional {@link YamlStringifyOptions} for the
839
+ * re-stringify step.
840
+ * @returns An `Effect` that succeeds with the edits to apply (via
841
+ * `YamlEdit.applyAll`), or fails with {@link YamlModificationError}.
842
+ */
843
+ static modify = Effect.fn("YamlFormat.modify")((text, path, value, options) => Effect.fromResult(YamlFormat.modifyResult(text, path, value, options)));
844
+ /**
845
+ * Synchronous {@link YamlFormat.modifyToString}: modify `text` and apply the
846
+ * resulting edits in one step (`YamlEdit.applyAll ∘ modifyResult`),
847
+ * returning a `Result`. Inherits the {@link YamlFormat.modifyResult} error
848
+ * channel.
849
+ *
850
+ * @example
851
+ * ```ts
852
+ * import { YamlFormat } from "@effected/yaml";
853
+ * import { Result } from "effect";
854
+ *
855
+ * const result = YamlFormat.modifyToStringResult("port: 3000 # dev\n", ["port"], 8080);
856
+ * if (Result.isSuccess(result)) {
857
+ * console.log(result.success); // => "port: 8080 # dev\n"
858
+ * }
859
+ * ```
860
+ *
861
+ * @param text - The YAML source to modify.
862
+ * @param path - The location to insert, replace or remove.
863
+ * @param value - The JavaScript value to write; `undefined` removes the
864
+ * target instead.
865
+ * @param options - Optional {@link YamlStringifyOptions} for the
866
+ * re-stringify step.
867
+ * @returns A `Result` succeeding with the modified text, or failing with
868
+ * {@link YamlModificationError}.
869
+ */
870
+ static modifyToStringResult(text, path, value, options) {
871
+ return Result.map(YamlFormat.modifyResult(text, path, value, options), (edits) => YamlEdit.applyAll(text, edits));
872
+ }
800
873
  /**
801
874
  * Modify `text` and apply the resulting edits in one step
802
875
  * (`YamlEdit.applyAll ∘ modify`). Inherits the {@link YamlFormat.modify}
@@ -812,11 +885,12 @@ var YamlFormat = class YamlFormat {
812
885
  * re-stringify step.
813
886
  * @returns An `Effect` that succeeds with the modified text, or fails with
814
887
  * {@link YamlModificationError}.
888
+ *
889
+ * @remarks
890
+ * Defined in terms of {@link YamlFormat.modifyToStringResult}; it opens its
891
+ * own span, with no nested `YamlFormat.modify` child span.
815
892
  */
816
- static modifyToString = Effect.fn("YamlFormat.modifyToString")(function* (text, path, value, options) {
817
- const edits = yield* YamlFormat.modify(text, path, value, options);
818
- return YamlEdit.applyAll(text, edits);
819
- });
893
+ static modifyToString = Effect.fn("YamlFormat.modifyToString")((text, path, value, options) => Effect.fromResult(YamlFormat.modifyToStringResult(text, path, value, options)));
820
894
  };
821
895
 
822
896
  export { YamlFormat, YamlFormattingOptions, YamlModificationError };
package/index.d.ts CHANGED
@@ -36,11 +36,14 @@ export declare const YamlComposerErrorCode: Schema.Literals<readonly ["Undefined
36
36
  */
37
37
  export type YamlComposerErrorCode = typeof YamlComposerErrorCode.Type;
38
38
  /**
39
- * Error codes emitted by the stringifier (the circular-reference guard).
39
+ * Error codes emitted by the stringifier: `CircularReference` (the value or
40
+ * synthetic AST points back at itself) and `UnrepresentableValue` (a function
41
+ * or symbol, which has no YAML rendering and is never written as its
42
+ * `String()` form).
40
43
  *
41
44
  * @public
42
45
  */
43
- export declare const YamlStringifyErrorCode: Schema.Literals<readonly ["CircularReference"]>;
46
+ export declare const YamlStringifyErrorCode: Schema.Literals<readonly ["CircularReference", "UnrepresentableValue"]>;
44
47
  /**
45
48
  * The union of all stringifier-stage error code string literals.
46
49
  *
@@ -53,7 +56,7 @@ export type YamlStringifyErrorCode = typeof YamlStringifyErrorCode.Type;
53
56
  *
54
57
  * @public
55
58
  */
56
- export declare const YamlModifyErrorCode: Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded"]>;
59
+ export declare const YamlModifyErrorCode: Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded", "UnrepresentableValue"]>;
57
60
  /**
58
61
  * The union of all modify-stage error code string literals.
59
62
  *
@@ -66,7 +69,7 @@ export type YamlModifyErrorCode = typeof YamlModifyErrorCode.Type;
66
69
  *
67
70
  * @public
68
71
  */
69
- export declare const YamlErrorCode: Schema.Union<readonly [Schema.Literals<readonly ["UnexpectedCharacter", "UnterminatedString", "InvalidEscapeSequence", "InvalidUnicode", "UnterminatedBlockScalar", "UnterminatedFlowCollection", "InvalidDirective", "InvalidTagHandle", "InvalidAnchorName", "UnexpectedByteOrderMark"]>, Schema.Literals<readonly ["InvalidIndentation", "DuplicateKey", "UnexpectedToken", "MissingValue", "MissingKey", "TabIndentation", "InvalidBlockStructure", "MalformedFlowCollection", "NestingDepthExceeded"]>, Schema.Literals<readonly ["UndefinedAlias", "DuplicateAnchor", "CircularAlias", "UnresolvedTag", "InvalidTagValue", "AliasCountExceeded", "InvalidDirective"]>, Schema.Literals<readonly ["CircularReference"]>, Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded"]>]>;
72
+ export declare const YamlErrorCode: Schema.Union<readonly [Schema.Literals<readonly ["UnexpectedCharacter", "UnterminatedString", "InvalidEscapeSequence", "InvalidUnicode", "UnterminatedBlockScalar", "UnterminatedFlowCollection", "InvalidDirective", "InvalidTagHandle", "InvalidAnchorName", "UnexpectedByteOrderMark"]>, Schema.Literals<readonly ["InvalidIndentation", "DuplicateKey", "UnexpectedToken", "MissingValue", "MissingKey", "TabIndentation", "InvalidBlockStructure", "MalformedFlowCollection", "NestingDepthExceeded"]>, Schema.Literals<readonly ["UndefinedAlias", "DuplicateAnchor", "CircularAlias", "UnresolvedTag", "InvalidTagValue", "AliasCountExceeded", "InvalidDirective"]>, Schema.Literals<readonly ["CircularReference", "UnrepresentableValue"]>, Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded", "UnrepresentableValue"]>]>;
70
73
  /**
71
74
  * The union of all YAML error code string literals.
72
75
  *
@@ -74,7 +77,7 @@ export declare const YamlErrorCode: Schema.Union<readonly [Schema.Literals<reado
74
77
  */
75
78
  export type YamlErrorCode = typeof YamlErrorCode.Type;
76
79
  declare const YamlDiagnostic_base: Schema.Class<YamlDiagnostic, Schema.Struct<{
77
- readonly code: Schema.Union<readonly [Schema.Literals<readonly ["UnexpectedCharacter", "UnterminatedString", "InvalidEscapeSequence", "InvalidUnicode", "UnterminatedBlockScalar", "UnterminatedFlowCollection", "InvalidDirective", "InvalidTagHandle", "InvalidAnchorName", "UnexpectedByteOrderMark"]>, Schema.Literals<readonly ["InvalidIndentation", "DuplicateKey", "UnexpectedToken", "MissingValue", "MissingKey", "TabIndentation", "InvalidBlockStructure", "MalformedFlowCollection", "NestingDepthExceeded"]>, Schema.Literals<readonly ["UndefinedAlias", "DuplicateAnchor", "CircularAlias", "UnresolvedTag", "InvalidTagValue", "AliasCountExceeded", "InvalidDirective"]>, Schema.Literals<readonly ["CircularReference"]>, Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded"]>]>;
80
+ readonly code: Schema.Union<readonly [Schema.Literals<readonly ["UnexpectedCharacter", "UnterminatedString", "InvalidEscapeSequence", "InvalidUnicode", "UnterminatedBlockScalar", "UnterminatedFlowCollection", "InvalidDirective", "InvalidTagHandle", "InvalidAnchorName", "UnexpectedByteOrderMark"]>, Schema.Literals<readonly ["InvalidIndentation", "DuplicateKey", "UnexpectedToken", "MissingValue", "MissingKey", "TabIndentation", "InvalidBlockStructure", "MalformedFlowCollection", "NestingDepthExceeded"]>, Schema.Literals<readonly ["UndefinedAlias", "DuplicateAnchor", "CircularAlias", "UnresolvedTag", "InvalidTagValue", "AliasCountExceeded", "InvalidDirective"]>, Schema.Literals<readonly ["CircularReference", "UnrepresentableValue"]>, Schema.Literals<readonly ["EmptyDocument", "PathNotFound", "InvalidIndex", "NotNavigable", "MultiDocumentStream", "DirectiveCarryingDocument", "CircularReference", "NestingDepthExceeded", "UnrepresentableValue"]>]>;
78
81
  readonly message: Schema.String;
79
82
  readonly offset: Schema.Number;
80
83
  readonly length: Schema.Number;
@@ -288,11 +291,14 @@ declare const YamlStringifyError_base: Schema.Class<YamlStringifyError, Schema.T
288
291
  readonly value: Schema.Unknown;
289
292
  }>, import("effect/Cause").YieldableError>;
290
293
  /**
291
- * Stringification failure (the circular-reference guard), carrying structured
292
- * {@link YamlDiagnostic} entries and the offending value. Raised by
293
- * {@link Yaml.stringify}, `YamlDocument#stringify` and the encode direction of
294
- * the schema factories. The error itself has no `code` field: read the code
295
- * from the diagnostics — `error.diagnostics[0].code` is the primary failure.
294
+ * Stringification failure, carrying structured {@link YamlDiagnostic} entries
295
+ * and the offending value. Raised by {@link Yaml.stringify},
296
+ * `YamlDocument#stringify` and the encode direction of the schema factories
297
+ * on a circular reference (`CircularReference`), a value nested past the
298
+ * stringifier's recursion budget (`NestingDepthExceeded`) or a value with no
299
+ * YAML representation (`UnrepresentableValue` — a function or a symbol). The
300
+ * error itself has no `code` field: read the code from the diagnostics —
301
+ * `error.diagnostics[0].code` is the primary failure.
296
302
  *
297
303
  * @public
298
304
  */
@@ -384,10 +390,14 @@ export declare class Yaml {
384
390
  static readonly parseAll: (text: string, options?: YamlParseOptions | undefined) => Effect.Effect<readonly unknown[], YamlParseError, never>;
385
391
  /**
386
392
  * Stringify a plain JavaScript value as YAML. Fails with
387
- * {@link YamlStringifyError} on circular references (`CircularReference`)
388
- * or on a value nested deeper than the stringifier's recursion budget
389
- * (`NestingDepthExceeded`) — both surface through the typed error channel
390
- * rather than as an unhandled stack-overflow defect.
393
+ * {@link YamlStringifyError} on circular references (`CircularReference`),
394
+ * on a value nested deeper than the stringifier's recursion budget
395
+ * (`NestingDepthExceeded`) and on a function or symbol anywhere in the
396
+ * value (`UnrepresentableValue`) — all surface through the typed error
397
+ * channel, never as a stack-overflow defect and never as the function's
398
+ * source text or `Symbol(s)` written into the document as a string. A
399
+ * `bigint` is an integer and renders bare, exactly as `YamlFormat.modify`
400
+ * writes one; `null` and `undefined` both render as `null`.
391
401
  *
392
402
  * @remarks
393
403
  * A `"<<"` object key is emitted **quoted** (`'<<': …`). This is the
@@ -483,10 +493,11 @@ export declare class Yaml {
483
493
  * The pure counterpart to {@link Yaml.stringify} for config-time callers
484
494
  * that cannot `await`.
485
495
  *
486
- * Preserves the package contract — a circular reference (`CircularReference`)
487
- * or a value nested past the recursion budget (`NestingDepthExceeded`)
488
- * yields a `Failure` carrying a {@link YamlStringifyError} rather than a
489
- * thrown stack-overflow defect; the method never throws.
496
+ * Preserves the package contract — a circular reference (`CircularReference`),
497
+ * a value nested past the recursion budget (`NestingDepthExceeded`) or a
498
+ * function or symbol value (`UnrepresentableValue`) yields a `Failure`
499
+ * carrying a {@link YamlStringifyError} rather than a thrown defect; the
500
+ * method never throws.
490
501
  *
491
502
  * @example
492
503
  * ```ts
@@ -1036,11 +1047,39 @@ declare const YamlDocument_base: Schema.Class<YamlDocument, Schema.Struct<{
1036
1047
  * @public
1037
1048
  */
1038
1049
  export declare class YamlDocument extends YamlDocument_base {
1050
+ /**
1051
+ * Synchronous single-document parse, returning a `Result` instead of an
1052
+ * `Effect`. This is the primitive {@link YamlDocument.parse} is defined in
1053
+ * terms of — a synchronous host can call it without an Effect runtime.
1054
+ * Keeps the full AST, directives and recovered diagnostics; fails with the
1055
+ * aggregate {@link YamlParseError} when any fatal-code diagnostic is
1056
+ * present, and non-fatal diagnostics are data on the returned document.
1057
+ *
1058
+ * @example
1059
+ * ```ts
1060
+ * import { YamlDocument } from "@effected/yaml";
1061
+ * import { Result } from "effect";
1062
+ *
1063
+ * const result = YamlDocument.parseResult("name: Alice\n");
1064
+ * if (Result.isSuccess(result)) {
1065
+ * console.log(result.success.toValue()); // => { name: "Alice" }
1066
+ * }
1067
+ * ```
1068
+ *
1069
+ * @param text - The YAML source to parse.
1070
+ * @param options - Optional {@link YamlParseOptions}; defaults apply for
1071
+ * omitted fields.
1072
+ * @returns A `Result` succeeding with the {@link YamlDocument}, or failing
1073
+ * with {@link YamlParseError}.
1074
+ */
1075
+ static parseResult(text: string, options?: YamlParseOptions): Result.Result<YamlDocument, YamlParseError>;
1039
1076
  /**
1040
1077
  * Parse a single YAML document, keeping the full AST, directives and
1041
1078
  * recovered diagnostics. Fails with the aggregate {@link YamlParseError}
1042
1079
  * when any fatal-code diagnostic is present; non-fatal diagnostics are
1043
- * data on the returned document.
1080
+ * data on the returned document. Defined in terms of
1081
+ * {@link YamlDocument.parseResult} — synchronous callers can use that
1082
+ * variant directly.
1044
1083
  *
1045
1084
  * @param text - The YAML source to parse.
1046
1085
  * @param options - Optional {@link YamlParseOptions}; defaults apply for
@@ -1049,10 +1088,37 @@ export declare class YamlDocument extends YamlDocument_base {
1049
1088
  * with {@link YamlParseError}.
1050
1089
  */
1051
1090
  static readonly parse: (text: string, options?: YamlParseOptions | undefined) => Effect.Effect<YamlDocument, YamlParseError, never>;
1091
+ /**
1092
+ * Synchronous multi-document parse, returning a `Result` instead of an
1093
+ * `Effect` — the primitive {@link YamlDocument.parseAll} is defined in
1094
+ * terms of. Any fatal diagnostic in any document — or a stream-level
1095
+ * directive-placement error — fails the whole parse with one aggregate
1096
+ * {@link YamlParseError}.
1097
+ *
1098
+ * @example
1099
+ * ```ts
1100
+ * import { YamlDocument } from "@effected/yaml";
1101
+ * import { Result } from "effect";
1102
+ *
1103
+ * const result = YamlDocument.parseAllResult("a: 1\n---\nb: 2\n");
1104
+ * if (Result.isSuccess(result)) {
1105
+ * console.log(result.success.length); // => 2
1106
+ * }
1107
+ * ```
1108
+ *
1109
+ * @param text - The YAML stream to parse.
1110
+ * @param options - Optional {@link YamlParseOptions}; defaults apply for
1111
+ * omitted fields.
1112
+ * @returns A `Result` succeeding with one {@link YamlDocument} per
1113
+ * document, or failing with {@link YamlParseError}.
1114
+ */
1115
+ static parseAllResult(text: string, options?: YamlParseOptions): Result.Result<ReadonlyArray<YamlDocument>, YamlParseError>;
1052
1116
  /**
1053
1117
  * Parse a multi-document YAML stream into one {@link YamlDocument} per
1054
1118
  * document. Any fatal diagnostic in any document — or a stream-level
1055
- * directive-placement error — fails the whole Effect.
1119
+ * directive-placement error — fails the whole Effect. Defined in terms of
1120
+ * {@link YamlDocument.parseAllResult} — synchronous callers can use that
1121
+ * variant directly.
1056
1122
  *
1057
1123
  * @param text - The YAML stream to parse.
1058
1124
  * @param options - Optional {@link YamlParseOptions}; defaults apply for
@@ -1076,11 +1142,19 @@ export declare class YamlDocument extends YamlDocument_base {
1076
1142
  static schema(options?: YamlParseOptions): Schema.Codec<YamlDocument, string>;
1077
1143
  /**
1078
1144
  * Stringify this document (contents, directives and framing) as YAML.
1079
- * Fails with {@link YamlStringifyError} on circular references introduced
1080
- * into a synthetic AST (`CircularReference`) or on a synthetic AST nested
1081
- * deeper than the stringifier's recursion budget (`NestingDepthExceeded`)
1082
- * — both surface through the typed error channel rather than as an
1083
- * unhandled stack-overflow defect.
1145
+ * A parsed document always stringifies; the failures belong to hand-built
1146
+ * ASTs and surface as {@link YamlStringifyError} through the typed error
1147
+ * channel, never as an unhandled stack-overflow defect or as corrupted
1148
+ * output: a cycle in the AST fails `CircularReference` on the first
1149
+ * re-entry of an ancestor collection (the node path keeps the same
1150
+ * ancestor set the value path does), a tree nested deeper than the
1151
+ * stringifier's recursion budget fails `NestingDepthExceeded`, and a
1152
+ * scalar node holding anything but `null`, a boolean, a number, a bigint
1153
+ * or a string — a function, a symbol, an object — fails
1154
+ * `UnrepresentableValue` rather than being written as its `String()` form.
1155
+ * Defined in terms of {@link YamlDocument.stringifyResult} behind a
1156
+ * `YamlDocument.stringify` span — synchronous callers can use that variant
1157
+ * directly.
1084
1158
  *
1085
1159
  * @remarks
1086
1160
  * `YamlStringifyOptions.lineWidth` is not honored here: column-based
@@ -1099,6 +1173,38 @@ export declare class YamlDocument extends YamlDocument_base {
1099
1173
  * {@link YamlStringifyError}.
1100
1174
  */
1101
1175
  stringify(options?: YamlStringifyOptions): Effect.Effect<string, YamlStringifyError>;
1176
+ /**
1177
+ * Synchronous stringify, returning a `Result` instead of an `Effect` — the
1178
+ * primitive {@link YamlDocument.stringify} is defined in terms of, so a
1179
+ * synchronous host can round-trip a document parsed with
1180
+ * {@link YamlDocument.parseResult} without an Effect runtime. Fails with
1181
+ * {@link YamlStringifyError} exactly where `stringify` does: a hand-built
1182
+ * AST containing a cycle (`CircularReference`), one nested deeper than the
1183
+ * stringifier's recursion budget (`NestingDepthExceeded`) or one whose
1184
+ * scalar node holds a value with no YAML form (`UnrepresentableValue`).
1185
+ *
1186
+ * @remarks
1187
+ * `YamlStringifyOptions.lineWidth` is not honored here, for the reason
1188
+ * given on {@link YamlDocument.stringify}.
1189
+ *
1190
+ * @example
1191
+ * ```ts
1192
+ * import { YamlDocument } from "@effected/yaml";
1193
+ * import { Result } from "effect";
1194
+ *
1195
+ * const parsed = YamlDocument.parseResult("# keep\nname: Alice\n");
1196
+ * if (Result.isSuccess(parsed)) {
1197
+ * const text = parsed.success.stringifyResult();
1198
+ * if (Result.isSuccess(text)) console.log(text.success); // => "# keep\nname: Alice\n"
1199
+ * }
1200
+ * ```
1201
+ *
1202
+ * @param options - Optional {@link YamlStringifyOptions}; defaults apply for
1203
+ * omitted fields.
1204
+ * @returns A `Result` succeeding with the YAML text, or failing with
1205
+ * {@link YamlStringifyError}.
1206
+ */
1207
+ stringifyResult(options?: YamlStringifyOptions): Result.Result<string, YamlStringifyError>;
1102
1208
  /**
1103
1209
  * Reconstruct the plain JavaScript value of this document's contents,
1104
1210
  * resolving anchors and aliases. `null` for an empty document. Pure and
@@ -1315,7 +1421,9 @@ export declare class YamlFormat {
1315
1421
  */
1316
1422
  static formatToString(text: string, range?: YamlRangeLike, options?: YamlFormattingOptions): string;
1317
1423
  /**
1318
- * Compute the edits that insert, replace, or remove a value at `path`.
1424
+ * Compute the edits that insert, replace, or remove a value at `path`,
1425
+ * synchronously — the primitive {@link YamlFormat.modify} is defined in
1426
+ * terms of, so a synchronous host can call it without an Effect runtime.
1319
1427
  * Passing `value === undefined` removes the target key/element; a missing
1320
1428
  * insertion target appends after the last pair/element. Fails with
1321
1429
  * {@link YamlModificationError} on a fatal parse error or a structural
@@ -1325,7 +1433,10 @@ export declare class YamlFormat {
1325
1433
  * sequence and any other non-null object as a block mapping over its own
1326
1434
  * enumerable string keys, recursively — the same lowering
1327
1435
  * {@link Yaml.stringify} applies to the same value, so `modify` and
1328
- * `stringify` agree on what a given JavaScript value means. For a synthesized
1436
+ * `stringify` agree on what a given JavaScript value means: a `bigint`
1437
+ * renders bare (`a: 10`, never `a: "10"`), `null` renders as `null`, and a
1438
+ * function or symbol fails `UnrepresentableValue` on both paths rather
1439
+ * than landing in the document as its `String()` form. For a synthesized
1329
1440
  * subtree (an object/array value) only the surrounding document is
1330
1441
  * preserved byte-for-byte; the subtree carries no comments and takes the
1331
1442
  * stringifier's styles.
@@ -1349,10 +1460,13 @@ export declare class YamlFormat {
1349
1460
  * whole document, which normalises line endings to LF, applies those
1350
1461
  * options, and renders the replacement in the stringifier's styles.
1351
1462
  *
1352
- * Two replacement values have no finite rendering and fail typed rather
1353
- * than hanging or overflowing the stack: one containing a circular
1354
- * reference (`CircularReference`) and one nesting deeper than 256 levels
1355
- * (`NestingDepthExceeded`).
1463
+ * Three replacement values have no rendering and fail typed rather than
1464
+ * hanging, overflowing the stack or corrupting the document: one
1465
+ * containing a circular reference (`CircularReference`), one that would
1466
+ * sit deeper than 256 levels in the output document
1467
+ * (`NestingDepthExceeded`) and one containing a function or symbol
1468
+ * (`UnrepresentableValue`). The depth is counted from the document root,
1469
+ * so the same value can fit at `[]` and fail a few keys down.
1356
1470
  *
1357
1471
  * **Single-document contract.** A `path` carries no document index, so on
1358
1472
  * a multi-document stream there is no rule for which document it names —
@@ -1377,15 +1491,60 @@ export declare class YamlFormat {
1377
1491
  * target instead.
1378
1492
  * @param options - Optional {@link YamlStringifyOptions} for the
1379
1493
  * re-stringify step.
1380
- * @returns An `Effect` that succeeds with the edits to apply (via
1381
- * `YamlEdit.applyAll`), or fails with {@link YamlModificationError}.
1494
+ * @returns A `Result` succeeding with the edits to apply (via
1495
+ * `YamlEdit.applyAll`), or failing with {@link YamlModificationError}.
1382
1496
  *
1383
1497
  * @remarks
1384
1498
  * `options` is a bare {@link YamlStringifyOptions} — it controls only the
1385
1499
  * internal re-stringify step, not a range (there is no range to restrict
1386
1500
  * for a path-targeted modification).
1387
1501
  */
1502
+ static modifyResult(text: string, path: YamlPath, value: unknown, options?: YamlStringifyOptions): Result.Result<ReadonlyArray<YamlEdit>, YamlModificationError>;
1503
+ /**
1504
+ * Compute the edits that insert, replace, or remove a value at `path`.
1505
+ * Defined in terms of {@link YamlFormat.modifyResult}, which carries the
1506
+ * full contract — value lowering, region-confined scalar replacement, the
1507
+ * single-document contract and the directive refusal — behind a
1508
+ * `YamlFormat.modify` span; synchronous callers can use that variant
1509
+ * directly.
1510
+ *
1511
+ * @param text - The YAML source to modify.
1512
+ * @param path - The location to insert, replace or remove.
1513
+ * @param value - The JavaScript value to write; `undefined` removes the
1514
+ * target instead.
1515
+ * @param options - Optional {@link YamlStringifyOptions} for the
1516
+ * re-stringify step.
1517
+ * @returns An `Effect` that succeeds with the edits to apply (via
1518
+ * `YamlEdit.applyAll`), or fails with {@link YamlModificationError}.
1519
+ */
1388
1520
  static readonly modify: (text: string, path: YamlPath, value: unknown, options?: YamlStringifyOptions | undefined) => Effect.Effect<readonly YamlEdit[], YamlModificationError, never>;
1521
+ /**
1522
+ * Synchronous {@link YamlFormat.modifyToString}: modify `text` and apply the
1523
+ * resulting edits in one step (`YamlEdit.applyAll ∘ modifyResult`),
1524
+ * returning a `Result`. Inherits the {@link YamlFormat.modifyResult} error
1525
+ * channel.
1526
+ *
1527
+ * @example
1528
+ * ```ts
1529
+ * import { YamlFormat } from "@effected/yaml";
1530
+ * import { Result } from "effect";
1531
+ *
1532
+ * const result = YamlFormat.modifyToStringResult("port: 3000 # dev\n", ["port"], 8080);
1533
+ * if (Result.isSuccess(result)) {
1534
+ * console.log(result.success); // => "port: 8080 # dev\n"
1535
+ * }
1536
+ * ```
1537
+ *
1538
+ * @param text - The YAML source to modify.
1539
+ * @param path - The location to insert, replace or remove.
1540
+ * @param value - The JavaScript value to write; `undefined` removes the
1541
+ * target instead.
1542
+ * @param options - Optional {@link YamlStringifyOptions} for the
1543
+ * re-stringify step.
1544
+ * @returns A `Result` succeeding with the modified text, or failing with
1545
+ * {@link YamlModificationError}.
1546
+ */
1547
+ static modifyToStringResult(text: string, path: YamlPath, value: unknown, options?: YamlStringifyOptions): Result.Result<string, YamlModificationError>;
1389
1548
  /**
1390
1549
  * Modify `text` and apply the resulting edits in one step
1391
1550
  * (`YamlEdit.applyAll ∘ modify`). Inherits the {@link YamlFormat.modify}
@@ -1401,6 +1560,10 @@ export declare class YamlFormat {
1401
1560
  * re-stringify step.
1402
1561
  * @returns An `Effect` that succeeds with the modified text, or fails with
1403
1562
  * {@link YamlModificationError}.
1563
+ *
1564
+ * @remarks
1565
+ * Defined in terms of {@link YamlFormat.modifyToStringResult}; it opens its
1566
+ * own span, with no nested `YamlFormat.modify` child span.
1404
1567
  */
1405
1568
  static readonly modifyToString: (text: string, path: YamlPath, value: unknown, options?: YamlStringifyOptions | undefined) => Effect.Effect<string, YamlModificationError, never>;
1406
1569
  }
@@ -927,6 +927,7 @@ function shouldPreserveRaw(rawValue, value) {
927
927
  if (Number.isNaN(value) || !Number.isFinite(value)) return false;
928
928
  return rawValue !== String(value);
929
929
  }
930
+ if (typeof value === "bigint") return rawValue !== value.toString();
930
931
  return false;
931
932
  }
932
933
 
@@ -35,13 +35,18 @@ const YAML_COMPOSE_ERROR_CODES = [
35
35
  "InvalidDirective"
36
36
  ];
37
37
  /**
38
- * Error codes for the stringifier stage. The engine's only deliberate
39
- * stringify failure is the circular-reference guard (thrown as
40
- * `StringifyFailure`); the facade materializes it under this code so
41
- * `YamlStringifyError` carries structured diagnostics rather than a
42
- * `reason` string.
38
+ * Error codes for the stringifier stage. The engine's deliberate stringify
39
+ * failures are the circular-reference guard and the unrepresentable-value
40
+ * guard (a function or symbol on the value path, or a synthetic scalar node
41
+ * holding anything but `null`, a boolean, a number, a bigint or a string),
42
+ * both thrown as `StringifyFailure` carrying one of these codes; the facade
43
+ * materializes them so `YamlStringifyError` carries structured diagnostics
44
+ * rather than a `reason` string. `UnrepresentableValue` exists because the
45
+ * alternative — writing `String(value)` into the document — turned a function's
46
+ * source text or `Symbol(s)` into a quoted string that only surfaced on the
47
+ * next read.
43
48
  */
44
- const YAML_STRINGIFY_ERROR_CODES = ["CircularReference"];
49
+ const YAML_STRINGIFY_ERROR_CODES = ["CircularReference", "UnrepresentableValue"];
45
50
  /**
46
51
  * Error codes for the modify stage (`YamlFormat.modify`'s guards and path
47
52
  * navigation against an already-composed AST). Not raised by the
@@ -53,10 +58,11 @@ const YAML_STRINGIFY_ERROR_CODES = ["CircularReference"];
53
58
  * without its `%TAG` orphans every shorthand tag that depends on it — the
54
59
  * output would be unparseable — so modify fails typed rather than corrupting.
55
60
  *
56
- * `CircularReference` and `NestingDepthExceeded` are the two limits on the
57
- * replacement value itself: `modify` lowers a plain JavaScript value into
58
- * AST nodes, and an object graph that points back at itself or nests deeper
59
- * than `MAX_NESTING_DEPTH` has no finite YAML rendering. Both names are
61
+ * `CircularReference`, `NestingDepthExceeded` and `UnrepresentableValue` are
62
+ * the three limits on the replacement value itself: `modify` lowers a plain
63
+ * JavaScript value into AST nodes, and an object graph that points back at
64
+ * itself or nests deeper than `MAX_NESTING_DEPTH` has no finite YAML
65
+ * rendering, while a function or symbol has none at all. All three names are
60
66
  * deliberately the ones the stringify and compose stages already use for the
61
67
  * same conditions, so `YamlErrorCode` gains no member.
62
68
  */
@@ -68,7 +74,8 @@ const YAML_MODIFY_ERROR_CODES = [
68
74
  "MultiDocumentStream",
69
75
  "DirectiveCarryingDocument",
70
76
  "CircularReference",
71
- "NestingDepthExceeded"
77
+ "NestingDepthExceeded",
78
+ "UnrepresentableValue"
72
79
  ];
73
80
  /**
74
81
  * The single source of truth for which diagnostic codes are fatal to a
@@ -3,18 +3,32 @@ import { foldRenderedScalar, hasInteriorTrailingWhitespace, hasNewlineSpacesTab,
3
3
 
4
4
  // src/internal/stringifier.ts
5
5
  /**
6
- * Thrown by the stringifier on its failure paths (circular references). The
7
- * facade catches this and materializes the public stringify error.
6
+ * Thrown by the stringifier on its two deliberate failure paths: a circular
7
+ * reference (`CircularReference`) and a value with no YAML representation
8
+ * (`UnrepresentableValue` — a function or symbol on the value path, or a
9
+ * synthetic scalar node holding anything but `null`, a boolean, a number, a
10
+ * bigint or a string). The facade catches this and materializes the public
11
+ * stringify error under `code`.
8
12
  */
9
13
  var StringifyFailure = class extends Error {
14
+ code;
10
15
  reason;
11
- constructor(reason) {
16
+ constructor(code, reason) {
12
17
  super(reason);
13
18
  this.name = "StringifyFailure";
19
+ this.code = code;
14
20
  this.reason = reason;
15
21
  }
16
22
  };
17
23
  /**
24
+ * Build the unrepresentable-value failure for a value of `kind` (its
25
+ * `typeof`). Shared by the value path and the node path so the two report
26
+ * the same condition in the same words.
27
+ */
28
+ function unrepresentable(kind) {
29
+ return new StringifyFailure("UnrepresentableValue", `A ${kind} value has no YAML representation`);
30
+ }
31
+ /**
18
32
  * Thrown by the value-stringifier trio when its mutual recursion exceeds
19
33
  * {@link MAX_NESTING_DEPTH}. A deeply-nested acyclic value (e.g. a 50 000-deep
20
34
  * array) would otherwise overflow the call stack as an unhandled `RangeError`
@@ -305,7 +319,7 @@ function renderNumber(n) {
305
319
  */
306
320
  function detectCircular(value, seen) {
307
321
  if (value !== null && typeof value === "object") {
308
- if (seen.has(value)) throw new StringifyFailure("Circular reference detected");
322
+ if (seen.has(value)) throw new StringifyFailure("CircularReference", "Circular reference detected");
309
323
  }
310
324
  }
311
325
  /** Resolves optional stringify options into a fully-defaulted context. */
@@ -360,7 +374,7 @@ function stringifyLines(value, ctx, depth, allowFold = true) {
360
374
  ctx.seen.delete(value);
361
375
  }
362
376
  }
363
- return [renderDoubleQuoted(String(value))];
377
+ throw unrepresentable(typeof value);
364
378
  }
365
379
  /**
366
380
  * Stringifies a JavaScript array into YAML sequence lines (no leading indent).
@@ -603,8 +617,15 @@ function stripNodeComments(node) {
603
617
  function stringifyNodeLines(node, ctx, depth) {
604
618
  if (depth > 256) throw new StringifyDepthExceeded();
605
619
  if (node instanceof YamlScalar) return stringifyScalarNodeLines(node, ctx);
606
- if (node instanceof YamlMap) return stringifyMapNodeLines(node, ctx, depth);
607
- if (node instanceof YamlSeq) return stringifySeqNodeLines(node, ctx, depth);
620
+ if (node instanceof YamlMap || node instanceof YamlSeq) {
621
+ if (ctx.seen.has(node)) throw new StringifyFailure("CircularReference", "Circular reference detected in the document AST");
622
+ ctx.seen.add(node);
623
+ try {
624
+ return node instanceof YamlMap ? stringifyMapNodeLines(node, ctx, depth) : stringifySeqNodeLines(node, ctx, depth);
625
+ } finally {
626
+ ctx.seen.delete(node);
627
+ }
628
+ }
608
629
  if (node instanceof YamlAlias) return [`*${node.name}`];
609
630
  return ["null"];
610
631
  }
@@ -623,12 +644,13 @@ function stringifyScalarNodeLines(node, ctx) {
623
644
  }
624
645
  let lines;
625
646
  if (val === null || val === void 0) {
626
- if (isEmpty) lines = [""];
647
+ if (isEmpty && node.raw === void 0) lines = [""];
627
648
  else lines = ["null"];
628
649
  } else if (typeof val === "boolean") lines = [val ? "true" : "false"];
629
650
  else if (typeof val === "number") lines = [node.raw !== void 0 ? node.raw : renderNumber(val)];
630
651
  else if (typeof val === "string") lines = renderString(val, style, " ".repeat(ctx.indent), !!node.tag, ctx.forceDefaultStyles, node.chomp, ctx.parentPosition, ctx.quoteStyle, node.blockIndent, ctx.quoteCompat, ctx.inFlow).split("\n");
631
- else lines = [renderDoubleQuoted(String(val))];
652
+ else if (typeof val === "bigint") lines = [node.raw !== void 0 ? node.raw : val.toString()];
653
+ else throw unrepresentable(typeof val);
632
654
  if (node.tag) lines[0] = `${node.tag} ${lines[0]}`;
633
655
  if (node.anchor) lines[0] = `&${node.anchor} ${lines[0]}`;
634
656
  return lines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/yaml",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "private": false,
5
5
  "description": "Zero-dependency YAML parsing, editing, formatting and linting as Effect schemas.",
6
6
  "keywords": [