@tabnas/transduce 0.1.1 → 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.
Files changed (83) hide show
  1. package/README.md +18 -0
  2. package/dist/guard.d.ts +1 -3
  3. package/dist/guard.js +8 -9
  4. package/dist/guard.js.map +1 -1
  5. package/dist/lines.d.ts +1 -2
  6. package/dist/lines.js +46 -89
  7. package/dist/lines.js.map +1 -1
  8. package/dist/matcher.d.ts +1 -3
  9. package/dist/matcher.js +12 -13
  10. package/dist/matcher.js.map +1 -1
  11. package/dist/parser-source.d.ts +1 -2
  12. package/dist/parser-source.js +11 -13
  13. package/dist/parser-source.js.map +1 -1
  14. package/dist/route.d.ts +1 -32
  15. package/dist/route.js +8 -32
  16. package/dist/route.js.map +1 -1
  17. package/dist/routers.d.ts +2 -0
  18. package/dist/routers.js +24 -0
  19. package/dist/routers.js.map +1 -0
  20. package/dist/rule-events.d.ts +1 -4
  21. package/dist/rule-events.js +40 -44
  22. package/dist/rule-events.js.map +1 -1
  23. package/dist/scan.d.ts +1 -10
  24. package/dist/scan.js +4 -17
  25. package/dist/scan.js.map +1 -1
  26. package/dist/source.d.ts +1 -4
  27. package/dist/source.js +20 -22
  28. package/dist/source.js.map +1 -1
  29. package/dist/table-from-json.d.ts +1 -5
  30. package/dist/table-from-json.js +25 -29
  31. package/dist/table-from-json.js.map +1 -1
  32. package/dist/transduce.d.ts +23 -19
  33. package/dist/transduce.js +66 -56
  34. package/dist/transduce.js.map +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +12 -4
  37. package/src/guard.ts +10 -4
  38. package/src/lines.ts +45 -6
  39. package/src/matcher.ts +1 -6
  40. package/src/parser-source.ts +2 -4
  41. package/src/route.ts +18 -70
  42. package/src/routers.ts +64 -0
  43. package/src/rule-events.ts +17 -7
  44. package/src/scan.ts +1 -19
  45. package/src/source.ts +12 -6
  46. package/src/table-from-json.ts +20 -9
  47. package/src/transduce.ts +36 -19
  48. package/dist/datum.d.ts +0 -64
  49. package/dist/datum.js +0 -391
  50. package/dist/datum.js.map +0 -1
  51. package/dist/error.d.ts +0 -43
  52. package/dist/error.js +0 -169
  53. package/dist/error.js.map +0 -1
  54. package/dist/event.d.ts +0 -56
  55. package/dist/event.js +0 -101
  56. package/dist/event.js.map +0 -1
  57. package/dist/json.d.ts +0 -4
  58. package/dist/json.js +0 -92
  59. package/dist/json.js.map +0 -1
  60. package/dist/limits.d.ts +0 -36
  61. package/dist/limits.js +0 -133
  62. package/dist/limits.js.map +0 -1
  63. package/dist/selector.d.ts +0 -40
  64. package/dist/selector.js +0 -154
  65. package/dist/selector.js.map +0 -1
  66. package/dist/sink.d.ts +0 -29
  67. package/dist/sink.js +0 -184
  68. package/dist/sink.js.map +0 -1
  69. package/dist/table.d.ts +0 -81
  70. package/dist/table.js +0 -141
  71. package/dist/table.js.map +0 -1
  72. package/dist/value.d.ts +0 -16
  73. package/dist/value.js +0 -96
  74. package/dist/value.js.map +0 -1
  75. package/src/datum.ts +0 -407
  76. package/src/error.ts +0 -198
  77. package/src/event.ts +0 -134
  78. package/src/json.ts +0 -82
  79. package/src/limits.ts +0 -165
  80. package/src/selector.ts +0 -191
  81. package/src/sink.ts +0 -199
  82. package/src/table.ts +0 -213
  83. package/src/value.ts +0 -93
package/src/sink.ts DELETED
@@ -1,199 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // The push boundary between stages.
4
- //
5
- // A pipeline is a chain of sinks. The source calls the first sink once per
6
- // event, synchronously, inside the parse; each stage does its work and
7
- // calls the next. Nothing is queued between stages, so a slow writer at the
8
- // end slows the parser at the start: that is the backpressure, and it
9
- // costs no buffer. A stage that must stop early (a `take`) answers
10
- // `'stop'`, which the source turns into a cancelled parse. A stage that
11
- // fails throws a `Fail`; the source stops the parse and the failure
12
- // reaches the caller unchanged.
13
-
14
- import { Fail } from './error'
15
- import { JsonEvent } from './event'
16
- import { Path, Segment } from './selector'
17
-
18
- // What a stage wants next: `'continue'`, or `'stop'` (the stage has all it
19
- // needs; the source stops the parse, releases what it holds and reports
20
- // nothing further; not an error).
21
- export type Flow = 'continue' | 'stop'
22
-
23
- // A consumer of `JsonEvents/1`. `event` throws a `Fail` to fail the run.
24
- export interface Sink {
25
- event(ev: JsonEvent): Flow
26
- }
27
-
28
- // A sink that records every event, for tests and small results.
29
- export class EventRecorder implements Sink {
30
- readonly events: JsonEvent[] = []
31
-
32
- event(ev: JsonEvent): Flow {
33
- this.events.push(ev)
34
- return 'continue'
35
- }
36
- }
37
-
38
- // A sink made of a function. A function that returns nothing continues.
39
- export class FnSink implements Sink {
40
- private fn: (ev: JsonEvent) => Flow | void
41
-
42
- constructor(fn: (ev: JsonEvent) => Flow | void) {
43
- this.fn = fn
44
- }
45
-
46
- event(ev: JsonEvent): Flow {
47
- return this.fn(ev) || 'continue'
48
- }
49
- }
50
-
51
- // A sink that counts events and drops them: the cheapest consumer, for
52
- // measuring a source on its own.
53
- export class CountSink implements Sink {
54
- events = 0
55
-
56
- event(_ev: JsonEvent): Flow {
57
- this.events++
58
- return 'continue'
59
- }
60
- }
61
-
62
- // Replay a recording into a sink, stopping where the sink stops.
63
- export function replay(events: Iterable<JsonEvent>, sink: Sink): Flow {
64
- for (const ev of events) {
65
- if ('stop' === sink.event(ev)) return 'stop'
66
- }
67
- return 'continue'
68
- }
69
-
70
- // A container `TreeContract` has open: an object with the keys it has had
71
- // (the last of them the member whose value is due unless `keyDue`), or an
72
- // array with the index of the element due next.
73
- type Open =
74
- | { array: false; keys: Set<string>; last: string | null; keyDue: boolean }
75
- | { array: true; next: number }
76
-
77
- // A tree's events, held to their contract in front of a sink that takes
78
- // them as one (a render that writes a document from them): one root value,
79
- // and in each object a key and then its value, each key once. A value
80
- // walked from a parsed tree keeps it by construction. A parse streamed as
81
- // it proceeds may not: it hands on a member its grammar reads twice
82
- // (JSON's `{"a":1,"a":2}`, whose value keeps the last) where a tree has
83
- // one, and the rule-event adapter refuses most shapes it cannot follow but
84
- // not every one a grammar can produce. A repeated key in one object is
85
- // refused with `DUPLICATE_MEMBER`, and events no tree has (a value where a
86
- // key is due, a key outside an object, a close with nothing open, a second
87
- // root) with `STREAMABILITY_UNKNOWN`, each at the path of the object
88
- // concerned; the event is not passed on. Each open object keeps the keys
89
- // it has had, dropped when it closes.
90
- export class TreeContract<S extends Sink> implements Sink {
91
- readonly inner: S
92
- private open: Open[] = []
93
- private rootDone = false
94
-
95
- constructor(inner: S) {
96
- this.inner = inner
97
- }
98
-
99
- // The path of the value due next: the open containers, each by the member
100
- // or element open in it, and in the innermost object its last key when
101
- // that member's value is due.
102
- private path(): Path {
103
- const segments: Segment[] = []
104
- const last = this.open.length - 1
105
- this.open.forEach((open, i) => {
106
- if (open.array) {
107
- segments.push(open.next)
108
- } else if ((i !== last || !open.keyDue) && null != open.last) {
109
- // A container open inside this object is its last key's value,
110
- // whatever `keyDue` says: the member was counted as taken when its
111
- // value opened.
112
- segments.push(open.last)
113
- }
114
- })
115
- return new Path(segments)
116
- }
117
-
118
- private notATree(what: string): Fail {
119
- return new Fail(
120
- 'STREAMABILITY_UNKNOWN',
121
- `the stream holds ${what}, which a tree's events never do, so it is not a tree's`,
122
- ).atPath(this.path().toString())
123
- }
124
-
125
- // A value is complete: the next in its array is due, or the root is.
126
- private closed(): void {
127
- const top = this.open[this.open.length - 1]
128
- if (undefined === top) this.rootDone = true
129
- else if (top.array) top.next++
130
- }
131
-
132
- event(ev: JsonEvent): Flow {
133
- const top = this.open[this.open.length - 1]
134
- switch (ev.type) {
135
- case 'key': {
136
- if (undefined !== top && !top.array && top.keyDue) {
137
- if (top.keys.has(ev.key)) {
138
- const path = this.path()
139
- path.push(ev.key)
140
- throw new Fail(
141
- 'DUPLICATE_MEMBER',
142
- `member ${JSON.stringify(ev.key)} appears twice in one object, and a tree's ` +
143
- `events hold each key once`,
144
- ).atPath(path.toString())
145
- }
146
- top.keys.add(ev.key)
147
- top.last = ev.key
148
- top.keyDue = false
149
- } else if (undefined !== top && !top.array) {
150
- throw this.notATree('a key where a value is due')
151
- } else {
152
- throw this.notATree('a key outside an object')
153
- }
154
- break
155
- }
156
- case 'object_end':
157
- if (undefined !== top && !top.array && top.keyDue) {
158
- this.open.pop()
159
- this.closed()
160
- } else if (undefined !== top && !top.array) {
161
- throw this.notATree("an object's end where a value is due")
162
- } else {
163
- throw this.notATree("an object's end where none is due")
164
- }
165
- break
166
- case 'array_end':
167
- if (undefined !== top && top.array) {
168
- this.open.pop()
169
- this.closed()
170
- } else {
171
- throw this.notATree("an array's end where none is due")
172
- }
173
- break
174
- case 'end':
175
- if (0 < this.open.length) {
176
- throw this.notATree('its end inside an open container')
177
- }
178
- break
179
- default: {
180
- // A value: in an object, only once its key is in; at the root,
181
- // only once.
182
- if (undefined === top) {
183
- if (this.rootDone) throw this.notATree('a second root value')
184
- } else if (!top.array) {
185
- if (top.keyDue) throw this.notATree('a value where a key is due')
186
- top.keyDue = true
187
- }
188
- if ('object_start' === ev.type) {
189
- this.open.push({ array: false, keys: new Set(), last: null, keyDue: true })
190
- } else if ('array_start' === ev.type) {
191
- this.open.push({ array: true, next: 0 })
192
- } else {
193
- this.closed()
194
- }
195
- }
196
- }
197
- return this.inner.event(ev)
198
- }
199
- }
package/src/table.ts DELETED
@@ -1,213 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // `TableRows/1`: one schema, ordered finite rows, completion.
4
- //
5
- // The protocol is flat and budgeted: a row is a finite vector of cells,
6
- // already projected into schema order by the transducer, and a renderer
7
- // never sees a source path. Public columns carry a label; the source
8
- // binding (`BoundColumn`) stays on the transducer's side of the boundary.
9
-
10
- import { Datum, toText } from './datum'
11
- import { Fail } from './error'
12
- import { jsonNumber, jsonString } from './json'
13
- import { NODE_BYTES, utf8Bytes } from './limits'
14
- import { Segment, Selector } from './selector'
15
- import { Flow } from './sink'
16
-
17
- // One projected value. `missing`: the source had no value at the column's
18
- // path; not null, not the empty string, not zero: a policy maps or rejects
19
- // it later.
20
- export type Cell =
21
- | { readonly type: 'null' }
22
- | { readonly type: 'bool'; readonly value: boolean }
23
- | { readonly type: 'number'; readonly value: number; readonly lexeme: string | null }
24
- | { readonly type: 'string'; readonly value: string }
25
- | { readonly type: 'missing' }
26
-
27
- const NULL: Cell = Object.freeze({ type: 'null' })
28
- const MISSING: Cell = Object.freeze({ type: 'missing' })
29
-
30
- export const Cell = Object.freeze({
31
- null: NULL,
32
- missing: MISSING,
33
- bool(value: boolean): Cell {
34
- return { type: 'bool', value }
35
- },
36
- number(value: number, lexeme?: string | null): Cell {
37
- return { type: 'number', value, lexeme: lexeme ?? null }
38
- },
39
- string(value: string): Cell {
40
- return { type: 'string', value }
41
- },
42
-
43
- // From a retained value. A container is not a cell; it is serialized as
44
- // compact JSON text, the lossy but unambiguous choice the standard
45
- // binding makes and documents.
46
- fromDatum(d: Datum): Cell {
47
- switch (d.type) {
48
- case 'null':
49
- return NULL
50
- case 'bool':
51
- return { type: 'bool', value: d.value }
52
- case 'number':
53
- return { type: 'number', value: d.value, lexeme: d.lexeme }
54
- case 'string':
55
- return { type: 'string', value: d.value }
56
- default:
57
- return { type: 'string', value: toText(d) }
58
- }
59
- },
60
-
61
- // The bytes this cell retains, on the same basis as a datum's size.
62
- byteSize(c: Cell): number {
63
- switch (c.type) {
64
- case 'number':
65
- return NODE_BYTES + (null != c.lexeme ? utf8Bytes(c.lexeme) : 8)
66
- case 'string':
67
- return NODE_BYTES + utf8Bytes(c.value)
68
- default:
69
- return NODE_BYTES
70
- }
71
- },
72
-
73
- // The cell as JSON text; `missing` prints as `missing`.
74
- toText(c: Cell): string {
75
- switch (c.type) {
76
- case 'null':
77
- return 'null'
78
- case 'bool':
79
- return String(c.value)
80
- case 'number':
81
- return jsonNumber(c.value, c.lexeme)
82
- case 'string':
83
- return jsonString(c.value)
84
- case 'missing':
85
- return 'missing'
86
- }
87
- },
88
- })
89
-
90
- // What a renderer knows about a column.
91
- export type PublicColumn = { label: string }
92
-
93
- // One event of `TableRows/1`: the `schema` exactly once and first, a `row`
94
- // per row (each exactly as wide as the schema), the `end` exactly once and
95
- // last, and only after the source validated to its end.
96
- export type TableEvent =
97
- | { readonly type: 'schema'; readonly columns: readonly PublicColumn[] }
98
- | { readonly type: 'row'; readonly cells: readonly Cell[] }
99
- | { readonly type: 'end' }
100
-
101
- // A consumer of `TableRows/1`. Throws a `Fail` to fail the run.
102
- export interface TableSink {
103
- tableEvent(ev: TableEvent): Flow
104
- }
105
-
106
- // An owned recording of a table, for tests and small results.
107
- export class Table implements TableSink {
108
- columns: PublicColumn[] = []
109
- rows: Cell[][] = []
110
- ended = false
111
-
112
- tableEvent(ev: TableEvent): Flow {
113
- switch (ev.type) {
114
- case 'schema':
115
- this.columns = ev.columns.slice()
116
- break
117
- case 'row':
118
- this.rows.push(ev.cells.slice())
119
- break
120
- case 'end':
121
- this.ended = true
122
- break
123
- }
124
- return 'continue'
125
- }
126
- }
127
-
128
- // What to do when a row has no value at a column's path: deliver a
129
- // `missing` cell (the default; the renderer's policy decides), deliver
130
- // `null`, or fail the run with `MISSING_VALUE`.
131
- export type MissingPolicy = 'missing' | 'null' | 'error'
132
-
133
- // A column as the transducer binds it: the public label, and the source
134
- // path projected from each row. Never crosses into a renderer.
135
- export type BoundColumn = {
136
- label: string
137
- source: Segment[]
138
- missing: MissingPolicy
139
- }
140
-
141
- export function boundColumn(
142
- label: string,
143
- source: Segment[],
144
- missing: MissingPolicy = 'missing',
145
- ): BoundColumn {
146
- return { label, source, missing }
147
- }
148
-
149
- // Maps one metadata descriptor to a bound column; throws a `Fail` for a
150
- // descriptor it cannot read.
151
- export type ColumnMapper = (meta: Datum) => BoundColumn
152
-
153
- // Where a table's columns come from: declared by the caller (`static`),
154
- // selected from the source and mapped one descriptor at a time
155
- // (`metadata`; the metadata must complete before the first row begins),
156
- // or the first row's member names in its order (`infer`; data-dependent: a
157
- // later row's extra members are dropped, its absent ones are `missing`).
158
- export type Schema =
159
- | { type: 'static'; columns: BoundColumn[] }
160
- | { type: 'metadata'; columns: Selector; column: ColumnMapper }
161
- | { type: 'infer' }
162
-
163
- export const Schema = Object.freeze({
164
- static(columns: BoundColumn[]): Schema {
165
- return { type: 'static', columns }
166
- },
167
- fromMetadata(columns: Selector, column: ColumnMapper = columnFromMeta): Schema {
168
- return { type: 'metadata', columns, column }
169
- },
170
- infer(): Schema {
171
- return { type: 'infer' }
172
- },
173
- })
174
-
175
- // A table transducer's source binding: the schema, and the rows (each
176
- // location `rows` names is one row).
177
- export type TableBinding = {
178
- schema: Schema
179
- rows: Selector
180
- }
181
-
182
- // The standard mapping from a metadata descriptor to a column: the spec's
183
- // `column-from-meta`, reading `title` and a `path` of segments.
184
- export function columnFromMeta(meta: Datum): BoundColumn {
185
- if ('object' !== meta.type) {
186
- throw Fail.input('a column descriptor is not an object')
187
- }
188
- const title = meta.members.get('title')
189
- if (undefined === title || 'string' !== title.type) {
190
- throw Fail.input('a column descriptor has no string "title"')
191
- }
192
- const label = title.value
193
- const path = meta.members.get('path')
194
- if (undefined === path || 'array' !== path.type) {
195
- throw Fail.input(`column ${JSON.stringify(label)} has no "path" array`)
196
- }
197
- const source: Segment[] = path.items.map((seg) => {
198
- if ('string' === seg.type) return seg.value
199
- if (
200
- 'number' === seg.type &&
201
- seg.value >= 0 &&
202
- Number.isInteger(seg.value) &&
203
- seg.value <= 0xffffffff
204
- ) {
205
- return seg.value
206
- }
207
- throw Fail.input(
208
- `column ${JSON.stringify(label)} has a path segment that is neither a string nor a ` +
209
- `non-negative integer: ${toText(seg)}`,
210
- )
211
- })
212
- return boundColumn(label, source)
213
- }
package/src/value.ts DELETED
@@ -1,93 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // Reading the engine's values: what a tabnas parse returns in TypeScript.
4
- //
5
- // The engine builds plain JavaScript values: `null` and `undefined`,
6
- // booleans, numbers, strings, arrays and objects. A few grammars hand over
7
- // host types, which read as the scalars they stand for: a boxed `String`
8
- // (the engine's text annotation), a `BigInt`, and a TOML `Date`, which
9
- // reads as the source text it carries (`__toml__.src`), as the Rust
10
- // runtime's date does.
11
- //
12
- // An object's member order is its insertion order. A plain object
13
- // enumerates integer-like keys first whatever order they were written in,
14
- // so the engine records the order on the node under `map.ordered` (read
15
- // with `KEY_ORDER`); the sources switch that on, and `engineKeys` reads
16
- // it, falling back to `Object.keys`.
17
-
18
- // The engine's key-order side channel (`keyOrder` in @tabnas/parser).
19
- const KEY_ORDER = Symbol.for('tabnas.keyOrder')
20
-
21
- export type EngineScalar =
22
- | { type: 'null' }
23
- | { type: 'bool'; value: boolean }
24
- | { type: 'number'; value: number }
25
- | { type: 'string'; value: string }
26
-
27
- const NULL: EngineScalar = { type: 'null' }
28
-
29
- // The value as a scalar, or `undefined` when it is a container.
30
- export function engineScalar(v: unknown): EngineScalar | undefined {
31
- switch (typeof v) {
32
- case 'undefined':
33
- return NULL
34
- case 'boolean':
35
- return { type: 'bool', value: v }
36
- case 'number':
37
- return { type: 'number', value: v }
38
- case 'bigint':
39
- return { type: 'number', value: Number(v) }
40
- case 'string':
41
- return { type: 'string', value: v }
42
- case 'object':
43
- if (null === v) return NULL
44
- if (Array.isArray(v)) return undefined
45
- if (v instanceof String) return { type: 'string', value: v.valueOf() }
46
- if (v instanceof Number) return { type: 'number', value: v.valueOf() }
47
- if (v instanceof Boolean) return { type: 'bool', value: v.valueOf() }
48
- if (v instanceof Date) {
49
- const src = (v as any).__toml__?.src
50
- return { type: 'string', value: 'string' === typeof src ? src : v.toJSON() ?? 'null' }
51
- }
52
- return undefined
53
- default:
54
- // A function or a symbol has no JSON form.
55
- return NULL
56
- }
57
- }
58
-
59
- // Whether the value is a container: an array, or an object that is no
60
- // scalar's host type.
61
- export function isEngineContainer(v: unknown): boolean {
62
- return null !== v && 'object' === typeof v && undefined === engineScalar(v)
63
- }
64
-
65
- // Whether the value is a map (a container that is not an array).
66
- export function isEngineMap(v: unknown): v is Record<string, unknown> {
67
- return isEngineContainer(v) && !Array.isArray(v)
68
- }
69
-
70
- // A map's member names, in insertion order: the engine's recorded order
71
- // where it kept one, each name once and only while the map still holds it,
72
- // then any member the record missed.
73
- export function engineKeys(v: unknown): string[] {
74
- if (null === v || 'object' !== typeof v) return []
75
- const own = Object.keys(v as object)
76
- const order: unknown = (v as any)[KEY_ORDER]
77
- if (!Array.isArray(order) || 0 === order.length) return own
78
- const has = Object.prototype.hasOwnProperty
79
- const seen = new Set<string>()
80
- const out: string[] = []
81
- for (const k of order) {
82
- const key = String(k)
83
- if (seen.has(key) || !has.call(v, key)) continue
84
- seen.add(key)
85
- out.push(key)
86
- }
87
- if (out.length !== own.length) {
88
- for (const k of own) {
89
- if (!seen.has(k)) out.push(k)
90
- }
91
- }
92
- return out
93
- }