@tabnas/transduce 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/transduce.d.ts +1 -1
- package/dist/transduce.js +1 -1
- package/package.json +3 -1
- package/src/capability.ts +52 -0
- package/src/datum.ts +407 -0
- package/src/error.ts +198 -0
- package/src/event.ts +134 -0
- package/src/guard.ts +112 -0
- package/src/json.ts +82 -0
- package/src/limits.ts +165 -0
- package/src/lines.ts +1247 -0
- package/src/matcher.ts +287 -0
- package/src/parser-source.ts +225 -0
- package/src/route.ts +275 -0
- package/src/rule-events.ts +661 -0
- package/src/scan.ts +83 -0
- package/src/selector.ts +191 -0
- package/src/sink.ts +199 -0
- package/src/source.ts +135 -0
- package/src/table-from-json.ts +296 -0
- package/src/table.ts +213 -0
- package/src/transduce.ts +72 -0
- package/src/tsconfig.json +16 -0
- package/src/value.ts +93 -0
package/src/scan.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
2
|
+
|
|
3
|
+
// `scan-emit`: declarative state evolution over a stream.
|
|
4
|
+
//
|
|
5
|
+
// A pure step function takes the state and one item and returns the next
|
|
6
|
+
// state with the items to emit; a finish function turns the final state
|
|
7
|
+
// into the closing items. The operator owns the state and the iteration;
|
|
8
|
+
// the functions own nothing. This is what the DSL's `scan-emit` lowers to,
|
|
9
|
+
// and the table transducer is one instance of it.
|
|
10
|
+
|
|
11
|
+
import { Fail } from './error'
|
|
12
|
+
import { Flow } from './sink'
|
|
13
|
+
|
|
14
|
+
// The result of one step: the next state and what to emit for it.
|
|
15
|
+
export type Transition<S, O> = { state: S; outputs: O[] }
|
|
16
|
+
|
|
17
|
+
export const Transition = Object.freeze({
|
|
18
|
+
of<S, O>(state: S, outputs: O[]): Transition<S, O> {
|
|
19
|
+
return { state, outputs }
|
|
20
|
+
},
|
|
21
|
+
// A step that emits nothing.
|
|
22
|
+
stay<S, O>(state: S): Transition<S, O> {
|
|
23
|
+
return { state, outputs: [] }
|
|
24
|
+
},
|
|
25
|
+
// A step that emits one item.
|
|
26
|
+
emit<S, O>(state: S, output: O): Transition<S, O> {
|
|
27
|
+
return { state, outputs: [output] }
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// The operator. Feed items with `item`, then call `finish` exactly once
|
|
32
|
+
// when the input completed successfully; never call it after a failure or
|
|
33
|
+
// a cancellation. A step or the finish throws a `Fail` to fail the run; a
|
|
34
|
+
// step that failed leaves the operator without a state, so a later item or
|
|
35
|
+
// `finish` is a `PROTOCOL_ORDER_ERROR`.
|
|
36
|
+
export class ScanEmit<S, I, O> {
|
|
37
|
+
private state: { value: S } | null
|
|
38
|
+
private step: (state: S, item: I) => Transition<S, O>
|
|
39
|
+
private finisher: ((state: S) => O[]) | null
|
|
40
|
+
private out: (output: O) => Flow
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
initial: S,
|
|
44
|
+
step: (state: S, item: I) => Transition<S, O>,
|
|
45
|
+
finish: (state: S) => O[],
|
|
46
|
+
out: (output: O) => Flow,
|
|
47
|
+
) {
|
|
48
|
+
this.state = { value: initial }
|
|
49
|
+
this.step = step
|
|
50
|
+
this.finisher = finish
|
|
51
|
+
this.out = out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// One input item. Its outputs go downstream before this returns.
|
|
55
|
+
item(item: I): Flow {
|
|
56
|
+
const state = this.state
|
|
57
|
+
if (null === state) {
|
|
58
|
+
throw Fail.protocol('scan-emit received an item after it finished')
|
|
59
|
+
}
|
|
60
|
+
this.state = null
|
|
61
|
+
const transition = this.step(state.value, item)
|
|
62
|
+
this.state = { value: transition.state }
|
|
63
|
+
for (const o of transition.outputs) {
|
|
64
|
+
if ('stop' === this.out(o)) return 'stop'
|
|
65
|
+
}
|
|
66
|
+
return 'continue'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The input completed: emit the closing items.
|
|
70
|
+
finish(): Flow {
|
|
71
|
+
const state = this.state
|
|
72
|
+
const finish = this.finisher
|
|
73
|
+
if (null === state || null === finish) {
|
|
74
|
+
throw Fail.protocol('scan-emit finished twice')
|
|
75
|
+
}
|
|
76
|
+
this.state = null
|
|
77
|
+
this.finisher = null
|
|
78
|
+
for (const o of finish(state.value)) {
|
|
79
|
+
if ('stop' === this.out(o)) return 'stop'
|
|
80
|
+
}
|
|
81
|
+
return 'continue'
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/selector.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
2
|
+
|
|
3
|
+
// Selectors: reusable descriptions of where in a document to look.
|
|
4
|
+
//
|
|
5
|
+
// A selector is data, never code: it is built from constructors or from
|
|
6
|
+
// validated path segments (`Selector.fromSegments`), and a matcher
|
|
7
|
+
// interprets it. A concrete `Path` names one location; a `Selector` may
|
|
8
|
+
// name many (`each_index`, `each_member`). Both print in jq syntax, which
|
|
9
|
+
// is what the rest of the fleet prints and accepts.
|
|
10
|
+
|
|
11
|
+
import { jsonString } from './json'
|
|
12
|
+
|
|
13
|
+
// One step of a concrete path: a string is a member's key, a non-negative
|
|
14
|
+
// integer an element's index.
|
|
15
|
+
export type Segment = string | number
|
|
16
|
+
|
|
17
|
+
// One key as jq writes it: bare when it is an ASCII identifier, a JSON
|
|
18
|
+
// string otherwise.
|
|
19
|
+
export function keyText(key: string): string {
|
|
20
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? '.' + key : '.' + jsonString(key)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A concrete location in a document.
|
|
24
|
+
export class Path {
|
|
25
|
+
readonly segments: Segment[]
|
|
26
|
+
|
|
27
|
+
constructor(segments: Segment[] = []) {
|
|
28
|
+
this.segments = segments
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static root(): Path {
|
|
32
|
+
return new Path([])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
push(seg: Segment): void {
|
|
36
|
+
this.segments.push(seg)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pop(): Segment | undefined {
|
|
40
|
+
return this.segments.pop()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
depth(): number {
|
|
44
|
+
return this.segments.length
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clone(): Path {
|
|
48
|
+
return new Path(this.segments.slice())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// jq syntax: `.a[0]."b-c"`, and `.` for the root.
|
|
52
|
+
toString(): string {
|
|
53
|
+
return pathText(this.segments)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Segments in jq syntax.
|
|
58
|
+
export function pathText(segments: readonly Segment[]): string {
|
|
59
|
+
if (0 === segments.length) return '.'
|
|
60
|
+
let out = ''
|
|
61
|
+
for (const seg of segments) {
|
|
62
|
+
out += 'number' === typeof seg ? `[${seg}]` : keyText(seg)
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// One step of a selector.
|
|
68
|
+
export type Step =
|
|
69
|
+
// The member with this name, inside an object.
|
|
70
|
+
| { readonly type: 'property'; readonly name: string }
|
|
71
|
+
// The element at this position, inside an array.
|
|
72
|
+
| { readonly type: 'index'; readonly index: number }
|
|
73
|
+
// Every element of an array.
|
|
74
|
+
| { readonly type: 'each_index' }
|
|
75
|
+
// Every member value of an object.
|
|
76
|
+
| { readonly type: 'each_member' }
|
|
77
|
+
|
|
78
|
+
const EACH_INDEX: Step = Object.freeze({ type: 'each_index' })
|
|
79
|
+
const EACH_MEMBER: Step = Object.freeze({ type: 'each_member' })
|
|
80
|
+
|
|
81
|
+
// A description of locations: the root, narrowed step by step. Immutable:
|
|
82
|
+
// every builder returns a new selector.
|
|
83
|
+
export class Selector {
|
|
84
|
+
readonly steps: readonly Step[]
|
|
85
|
+
|
|
86
|
+
constructor(steps: readonly Step[] = []) {
|
|
87
|
+
this.steps = Object.freeze(steps.slice())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The document itself.
|
|
91
|
+
static root(): Selector {
|
|
92
|
+
return new Selector([])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
property(name: string): Selector {
|
|
96
|
+
return new Selector([...this.steps, { type: 'property', name }])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
index(index: number): Selector {
|
|
100
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
101
|
+
throw new TypeError('an index step is a non-negative integer: ' + index)
|
|
102
|
+
}
|
|
103
|
+
return new Selector([...this.steps, { type: 'index', index }])
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
eachIndex(): Selector {
|
|
107
|
+
return new Selector([...this.steps, EACH_INDEX])
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
eachMember(): Selector {
|
|
111
|
+
return new Selector([...this.steps, EACH_MEMBER])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// `this`, then `other` below every location `this` names.
|
|
115
|
+
compose(other: Selector): Selector {
|
|
116
|
+
return new Selector([...this.steps, ...other.steps])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A selector naming exactly one location: `as-path` over data.
|
|
120
|
+
static fromSegments(segments: readonly Segment[]): Selector {
|
|
121
|
+
let s = Selector.root()
|
|
122
|
+
for (const seg of segments) {
|
|
123
|
+
s = 'number' === typeof seg ? s.index(seg) : s.property(seg)
|
|
124
|
+
}
|
|
125
|
+
return s
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isRoot(): boolean {
|
|
129
|
+
return 0 === this.steps.length
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Whether the selector can name more than one location.
|
|
133
|
+
isMulti(): boolean {
|
|
134
|
+
return this.steps.some((s) => 'each_index' === s.type || 'each_member' === s.type)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Whether this selector names a location strictly inside a location
|
|
138
|
+
// `other` names, or the same one: the test a router uses to refuse
|
|
139
|
+
// overlapping captures.
|
|
140
|
+
mayOverlap(other: Selector): boolean {
|
|
141
|
+
const [short, long] =
|
|
142
|
+
this.steps.length <= other.steps.length ? [this, other] : [other, this]
|
|
143
|
+
for (let i = 0; i < short.steps.length; i++) {
|
|
144
|
+
if (!stepMayMatchSame(short.steps[i], long.steps[i])) return false
|
|
145
|
+
}
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
equals(other: Selector): boolean {
|
|
150
|
+
return this.toString() === other.toString()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// jq syntax: `.response.records[*]`, `."odd key"`, `[3]`, `[]` for
|
|
154
|
+
// every member, `.` for the root.
|
|
155
|
+
toString(): string {
|
|
156
|
+
if (0 === this.steps.length) return '.'
|
|
157
|
+
let out = ''
|
|
158
|
+
for (const step of this.steps) {
|
|
159
|
+
switch (step.type) {
|
|
160
|
+
case 'property':
|
|
161
|
+
out += keyText(step.name)
|
|
162
|
+
break
|
|
163
|
+
case 'index':
|
|
164
|
+
out += `[${step.index}]`
|
|
165
|
+
break
|
|
166
|
+
case 'each_index':
|
|
167
|
+
out += '[*]'
|
|
168
|
+
break
|
|
169
|
+
case 'each_member':
|
|
170
|
+
out += '[]'
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return out
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stepMayMatchSame(a: Step, b: Step): boolean {
|
|
179
|
+
switch (a.type) {
|
|
180
|
+
case 'property':
|
|
181
|
+
return (
|
|
182
|
+
('property' === b.type && a.name === b.name) || 'each_member' === b.type
|
|
183
|
+
)
|
|
184
|
+
case 'each_member':
|
|
185
|
+
return 'property' === b.type || 'each_member' === b.type
|
|
186
|
+
case 'index':
|
|
187
|
+
return ('index' === b.type && a.index === b.index) || 'each_index' === b.type
|
|
188
|
+
case 'each_index':
|
|
189
|
+
return 'index' === b.type || 'each_index' === b.type
|
|
190
|
+
}
|
|
191
|
+
}
|
package/src/sink.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
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/source.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
2
|
+
|
|
3
|
+
// Sources: where `JsonEvents/1` come from.
|
|
4
|
+
//
|
|
5
|
+
// - `ValueSource` walks a parsed engine value. Always correct, retains the
|
|
6
|
+
// whole value: the fallback for every grammar.
|
|
7
|
+
// - `ParserSource` drives a tabnas parse of one text and, in incremental
|
|
8
|
+
// mode, turns its rule events into source events as they happen, for the
|
|
9
|
+
// grammars the differential suite has verified (`capability`); in
|
|
10
|
+
// materialize mode it parses and walks.
|
|
11
|
+
// - `LinesSource` reads JSON Lines or CSV a record (or a chunk of records)
|
|
12
|
+
// at a time, bounding memory whatever the input's size.
|
|
13
|
+
//
|
|
14
|
+
// Every source emits through `Guarded`, which enforces the source limits,
|
|
15
|
+
// polls the abort flag and counts the source metrics.
|
|
16
|
+
|
|
17
|
+
import { Fail, engineCode, engineDetail } from './error'
|
|
18
|
+
import { Ev } from './event'
|
|
19
|
+
import { AbortFlag } from './limits'
|
|
20
|
+
import { Selector } from './selector'
|
|
21
|
+
import { Flow, Sink } from './sink'
|
|
22
|
+
import { engineKeys, engineScalar } from './value'
|
|
23
|
+
|
|
24
|
+
// Which arrays the incremental source empties as it streams them: none
|
|
25
|
+
// (`never`), the array whose elements the selector names (`under`: a
|
|
26
|
+
// trailing `[*]` names the elements; without one the selector names the
|
|
27
|
+
// array), or every array (`all_arrays`). Pruning alters the value the
|
|
28
|
+
// engine returns, which the incremental source discards; it is never
|
|
29
|
+
// applied in materialize mode.
|
|
30
|
+
export type Prune =
|
|
31
|
+
| { type: 'never' }
|
|
32
|
+
| { type: 'under'; selector: Selector }
|
|
33
|
+
| { type: 'all_arrays' }
|
|
34
|
+
|
|
35
|
+
export const Prune = Object.freeze({
|
|
36
|
+
never(): Prune {
|
|
37
|
+
return { type: 'never' }
|
|
38
|
+
},
|
|
39
|
+
under(selector: Selector): Prune {
|
|
40
|
+
return { type: 'under', selector }
|
|
41
|
+
},
|
|
42
|
+
allArrays(): Prune {
|
|
43
|
+
return { type: 'all_arrays' }
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// How `ParserSource` produces its events: parse the whole text, then walk
|
|
48
|
+
// the value (`materialize`, sound for every grammar), or emit from the
|
|
49
|
+
// engine's rule events as the parse proceeds (`incremental`, sound for the
|
|
50
|
+
// grammars `capability.incremental` lists).
|
|
51
|
+
export type SourceMode = { type: 'materialize' } | { type: 'incremental'; prune: Prune }
|
|
52
|
+
|
|
53
|
+
export const SourceMode = Object.freeze({
|
|
54
|
+
materialize(): SourceMode {
|
|
55
|
+
return { type: 'materialize' }
|
|
56
|
+
},
|
|
57
|
+
incremental(prune: Prune = Prune.never()): SourceMode {
|
|
58
|
+
return { type: 'incremental', prune }
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Something that can drive a sink with one document's events. `run`
|
|
63
|
+
// returns `'stop'` when the sink stopped it (the document was not
|
|
64
|
+
// validated past that point) and throws a `Fail` on failure.
|
|
65
|
+
export interface Source {
|
|
66
|
+
run(sink: Sink): Flow
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The engine's cancel code: what a cancelled parse reports, whether the
|
|
70
|
+
// canceller is this crate's abort or a guard of the grammar's own.
|
|
71
|
+
const CANCEL = 'cancel'
|
|
72
|
+
|
|
73
|
+
// Map an engine error to a failure. A cancel while the caller's flag is set
|
|
74
|
+
// is `ABORTED`. A cancel otherwise is a guard the GRAMMAR installed, so
|
|
75
|
+
// the message says so instead of "parse cancelled", which would read as
|
|
76
|
+
// the caller's doing; it stays `INPUT_INVALID`, because the document is
|
|
77
|
+
// what the grammar refused. Any other error is the input's, with the
|
|
78
|
+
// engine's code and position. A thrown value that is not an engine error
|
|
79
|
+
// (a grammar's plugin threw) is the input's too, without a position.
|
|
80
|
+
export function engineFailure(error: unknown, abort: AbortFlag): Fail {
|
|
81
|
+
if (error instanceof Fail) return error
|
|
82
|
+
if (CANCEL !== engineCode(error)) return Fail.fromTabnas(error)
|
|
83
|
+
if (abort.isAborted()) return Fail.aborted()
|
|
84
|
+
const fail = Fail.fromTabnas(error)
|
|
85
|
+
fail.message =
|
|
86
|
+
`the grammar stopped the parse with a guard of its own (${engineCode(error)}: ` +
|
|
87
|
+
`${engineDetail(error)}); a grammar may refuse nesting or size below this crate's Limits`
|
|
88
|
+
return fail
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Emit one engine value's events (without `end`). `undefined` is `null`,
|
|
92
|
+
// as the engine serializes it; an object's members are in insertion order.
|
|
93
|
+
export function walkValue(value: unknown, sink: Sink): Flow {
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
if ('stop' === sink.event(Ev.arrayStart)) return 'stop'
|
|
96
|
+
for (const item of value) {
|
|
97
|
+
if ('stop' === walkValue(item, sink)) return 'stop'
|
|
98
|
+
}
|
|
99
|
+
return sink.event(Ev.arrayEnd)
|
|
100
|
+
}
|
|
101
|
+
const scalar = engineScalar(value)
|
|
102
|
+
if (undefined !== scalar) {
|
|
103
|
+
switch (scalar.type) {
|
|
104
|
+
case 'null':
|
|
105
|
+
return sink.event(Ev.null)
|
|
106
|
+
case 'bool':
|
|
107
|
+
return sink.event(Ev.bool(scalar.value))
|
|
108
|
+
case 'number':
|
|
109
|
+
return sink.event(Ev.number(scalar.value))
|
|
110
|
+
case 'string':
|
|
111
|
+
return sink.event(Ev.string(scalar.value))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if ('stop' === sink.event(Ev.objectStart)) return 'stop'
|
|
115
|
+
for (const k of engineKeys(value)) {
|
|
116
|
+
if ('stop' === sink.event(Ev.key(k))) return 'stop'
|
|
117
|
+
if ('stop' === walkValue((value as any)[k], sink)) return 'stop'
|
|
118
|
+
}
|
|
119
|
+
return sink.event(Ev.objectEnd)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Emit a parsed engine value as events, ending with `end`. Applies no
|
|
123
|
+
// limits: wrap the sink in `Guarded` for those.
|
|
124
|
+
export class ValueSource implements Source {
|
|
125
|
+
readonly value: unknown
|
|
126
|
+
|
|
127
|
+
constructor(value: unknown) {
|
|
128
|
+
this.value = value
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
run(sink: Sink): Flow {
|
|
132
|
+
if ('stop' === walkValue(this.value, sink)) return 'stop'
|
|
133
|
+
return sink.event(Ev.end)
|
|
134
|
+
}
|
|
135
|
+
}
|