@effected/toml 0.1.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.
@@ -0,0 +1,133 @@
1
+ import { isRawTomlError } from "./internal/diagnostics.js";
2
+ import { isGuardExceeded } from "./internal/limits.js";
3
+ import { TomlExpression } from "./TomlNode.js";
4
+ import { parseExpressions } from "./internal/parser.js";
5
+ import { analyze, buildValue } from "./internal/semantic.js";
6
+ import { TomlDiagnostic } from "./TomlDiagnostic.js";
7
+ import { TomlParseError } from "./Toml.js";
8
+ import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
9
+
10
+ //#region src/TomlDocument.ts
11
+ /**
12
+ * Materialize an engine throw into the typed error: `RawTomlError` becomes a
13
+ * positioned diagnostic and a `GuardExceeded` depth trip becomes a
14
+ * `NestingDepthExceeded` diagnostic. Anything else is a genuine defect and
15
+ * rethrows.
16
+ */
17
+ const materializeError = (text, defect) => {
18
+ if (isRawTomlError(defect)) return new TomlParseError({ diagnostics: [TomlDiagnostic.fromRaw(text, defect.diagnostic)] });
19
+ if (isGuardExceeded(defect)) return new TomlParseError({ diagnostics: [TomlDiagnostic.fromRaw(text, {
20
+ code: "NestingDepthExceeded",
21
+ message: defect.message,
22
+ offset: defect.offset,
23
+ length: 0
24
+ })] });
25
+ throw defect;
26
+ };
27
+ /**
28
+ * A parsed TOML document that never loses a byte: the `source` text, the
29
+ * linear {@link (TomlExpression:type)} CST whose spans tile the source
30
+ * exactly, and
31
+ * any semantic violations as {@link TomlDiagnostic} data.
32
+ *
33
+ * @remarks
34
+ * `parse` fails typed only on lex/parse errors; a syntactically valid but
35
+ * semantically illegal document (say, a duplicate key) still parses with the
36
+ * violation recorded in `diagnostics`, so the text stays inspectable and
37
+ * editable. `stringify` reconstructs the source by concatenating expression
38
+ * spans — that the result equals `source` is the span-bookkeeping proof, held
39
+ * byte-exact across the full toml-test corpus. `toValue` refuses on a
40
+ * non-empty `diagnostics`.
41
+ *
42
+ * Construct via {@link TomlDocument.parse}; `TomlDocument.make` is for
43
+ * synthetic documents.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * import { TomlDocument } from "@effected/toml";
48
+ * import { Effect } from "effect";
49
+ *
50
+ * const program = Effect.gen(function* () {
51
+ * const doc = yield* TomlDocument.parse('name = "Alice"\n');
52
+ * doc.stringify(); // 'name = "Alice"\n' — byte-exact
53
+ * return yield* doc.toValue(); // { name: "Alice" }
54
+ * });
55
+ * ```
56
+ *
57
+ * @public
58
+ */
59
+ var TomlDocument = class TomlDocument extends Schema.Class("TomlDocument")({
60
+ source: Schema.String,
61
+ expressions: Schema.Array(TomlExpression),
62
+ diagnostics: Schema.Array(TomlDiagnostic)
63
+ }) {
64
+ /**
65
+ * Parse TOML text into a lossless document. Fails with
66
+ * {@link TomlParseError} only on lex/parse errors — including a
67
+ * nesting-depth bomb, which surfaces as a `NestingDepthExceeded`
68
+ * diagnostic, never an unhandled defect. Semantic violations do not fail:
69
+ * they land in `diagnostics` as data (first violation wins, so there is at
70
+ * most one today; the array shape is the contract).
71
+ */
72
+ static parse = Effect.fn("TomlDocument.parse")(function* (text) {
73
+ const expressions = yield* Effect.try({
74
+ try: () => parseExpressions(text),
75
+ catch: (defect) => materializeError(text, defect)
76
+ });
77
+ const diagnostics = [];
78
+ try {
79
+ analyze(expressions);
80
+ } catch (defect) {
81
+ if (!isRawTomlError(defect)) throw defect;
82
+ diagnostics.push(TomlDiagnostic.fromRaw(text, defect.diagnostic));
83
+ }
84
+ return TomlDocument.make({
85
+ source: text,
86
+ expressions,
87
+ diagnostics
88
+ });
89
+ });
90
+ /**
91
+ * A `Schema<TomlDocument, string>` decoding TOML text into a full document
92
+ * (source, expressions, diagnostics) and encoding a document back to its
93
+ * byte-exact text.
94
+ *
95
+ * Schema-producing: each call returns a fresh schema whose derivation
96
+ * caches are not shared across calls; bind the result to a `const` on hot
97
+ * paths.
98
+ */
99
+ static schema() {
100
+ return Schema.String.pipe(Schema.decodeTo(Schema.instanceOf(TomlDocument), SchemaTransformation.transformOrFail({
101
+ decode: (input) => TomlDocument.parse(input).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(input), { message: error.message }))),
102
+ encode: (doc) => Effect.succeed(doc.stringify())
103
+ })));
104
+ }
105
+ /**
106
+ * Materialize the document's plain JavaScript value. Fails with
107
+ * {@link TomlParseError} carrying the stored `diagnostics` when the parse
108
+ * recorded semantic violations; otherwise builds the value from the
109
+ * expression list (already validated, so the defensive materialization
110
+ * wrapper is belt-and-suspenders).
111
+ */
112
+ toValue() {
113
+ if (this.diagnostics.length > 0) return Effect.fail(new TomlParseError({ diagnostics: this.diagnostics }));
114
+ return Effect.try({
115
+ try: () => buildValue(this.expressions),
116
+ catch: (defect) => materializeError(this.source, defect)
117
+ });
118
+ }
119
+ /**
120
+ * Reconstruct the document text by concatenating each expression's source
121
+ * span in order. The expression spans tile the source exactly, so the
122
+ * result equals `source` byte-for-byte — the round-trip contract this
123
+ * class exists to prove. Pure and total.
124
+ */
125
+ stringify() {
126
+ let out = "";
127
+ for (const expression of this.expressions) out += this.source.slice(expression.offset, expression.offset + expression.length);
128
+ return out;
129
+ }
130
+ };
131
+
132
+ //#endregion
133
+ export { TomlDocument };
package/TomlEdit.js ADDED
@@ -0,0 +1,52 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/TomlEdit.ts
4
+ /**
5
+ * A range within a TOML document, expressed as a zero-based character
6
+ * `offset` and a `length` in UTF-16 code units. Pass to `TomlFormat.format`
7
+ * to restrict formatting to the expressions intersecting a region.
8
+ *
9
+ * @public
10
+ */
11
+ var TomlRange = class extends Schema.Class("TomlRange")({
12
+ offset: Schema.Number,
13
+ length: Schema.Number
14
+ }) {};
15
+ /**
16
+ * A non-mutating text edit: replace the span `[offset, offset + length)` with
17
+ * `content`. Set `length` to `0` to insert, `content` to `""` to delete.
18
+ *
19
+ * @remarks
20
+ * Structurally identical to `@effected/jsonc`'s and `@effected/yaml`'s edit
21
+ * shapes (same field names, types and semantics) per the jsonc/yaml parity
22
+ * convention, so consumer code can be written once over "a document codec's
23
+ * Edit/Range/Path".
24
+ *
25
+ * @public
26
+ */
27
+ var TomlEdit = class extends Schema.Class("TomlEdit")({
28
+ offset: Schema.Number,
29
+ length: Schema.Number,
30
+ content: Schema.String
31
+ }) {
32
+ /**
33
+ * Apply `edits` to `text`, producing a new string. Edits are applied in
34
+ * reverse-offset order so earlier offsets stay valid; the input `edits`
35
+ * array is not mutated. Overlapping edits are a programmer error and throw
36
+ * as a defect — `TomlFormat` never produces them.
37
+ */
38
+ static applyAll(text, edits) {
39
+ const sorted = [...edits].sort((a, b) => b.offset - a.offset);
40
+ for (let i = 0; i + 1 < sorted.length; i++) {
41
+ const upper = sorted[i];
42
+ const lower = sorted[i + 1];
43
+ if (lower.offset + lower.length > upper.offset) throw new Error(`TomlEdit.applyAll received overlapping edits at offsets ${lower.offset} and ${upper.offset} — overlapping edits are a programmer error`);
44
+ }
45
+ let result = text;
46
+ for (const edit of sorted) result = result.slice(0, edit.offset) + edit.content + result.slice(edit.offset + edit.length);
47
+ return result;
48
+ }
49
+ };
50
+
51
+ //#endregion
52
+ export { TomlEdit, TomlRange };