Skip to content

Commit 5eec19f

Browse files
committedFeb 3, 2025·
Added replace() + tests.
1 parent d0ec000 commit 5eec19f

File tree

2 files changed

+377
-107
lines changed

2 files changed

+377
-107
lines changed
 

‎src/filters/replace.js

+32-107
Original file line numberDiff line numberDiff line change
@@ -2,116 +2,41 @@
22

33
'use strict';
44

5-
const FilterBase = require('./filter-base');
6-
const withParser = require('../utils/with-parser');
5+
const {none, isMany, getManyValues, combineManyMut, many} = require('stream-chain');
76

8-
class Replace extends FilterBase {
9-
static make(options) {
10-
return new Replace(options);
11-
}
7+
const {filterBase, makeStackDiffer} = require('./filter-base.js');
8+
const withParser = require('../utils/with-parser.js');
129

13-
static withParser(options) {
14-
return withParser(Replace.make, options);
15-
}
10+
const defaultReplacement = () => none;
1611

17-
_checkChunk(chunk) {
18-
switch (chunk.name) {
19-
case 'startKey':
20-
if (this._allowEmptyReplacement) {
21-
this._transform = this._skipKeyChunks;
22-
return true;
23-
}
24-
break;
25-
case 'keyValue':
26-
if (this._allowEmptyReplacement) return true;
27-
break;
28-
case 'startObject':
29-
case 'startArray':
30-
case 'startString':
31-
case 'startNumber':
32-
case 'nullValue':
33-
case 'trueValue':
34-
case 'falseValue':
35-
case 'stringValue':
36-
case 'numberValue':
37-
if (this._filter(this._stack, chunk)) {
38-
let replacement = this._replacement(this._stack, chunk);
39-
if (this._allowEmptyReplacement) {
40-
if (replacement.length) {
41-
const key = this._stack[this._stack.length - 1];
42-
if (typeof key == 'string') {
43-
if (this._streamKeys) {
44-
this.push({name: 'startKey'});
45-
this.push({name: 'stringChunk', value: key});
46-
this.push({name: 'endKey'});
47-
}
48-
this.push({name: 'keyValue', value: key});
49-
}
50-
}
51-
} else {
52-
if (!replacement.length) replacement = FilterBase.defaultReplacement;
53-
}
54-
replacement.forEach(value => this.push(value));
55-
switch (chunk.name) {
56-
case 'startObject':
57-
case 'startArray':
58-
this._transform = this._skipObject;
59-
this._depth = 1;
60-
break;
61-
case 'startString':
62-
this._transform = this._skipString;
63-
break;
64-
case 'startNumber':
65-
this._transform = this._skipNumber;
66-
break;
67-
case 'nullValue':
68-
case 'trueValue':
69-
case 'falseValue':
70-
case 'stringValue':
71-
case 'numberValue':
72-
this._transform = this._once ? this._pass : this._check;
73-
break;
74-
}
75-
return true;
76-
}
77-
break;
78-
}
79-
// issue a key, if needed
80-
if (this._allowEmptyReplacement) {
81-
const key = this._stack[this._stack.length - 1];
82-
if (typeof key == 'string') {
83-
switch (chunk.name) {
84-
case 'startObject':
85-
case 'startArray':
86-
case 'startString':
87-
case 'startNumber':
88-
case 'nullValue':
89-
case 'trueValue':
90-
case 'falseValue':
91-
case 'stringValue':
92-
case 'numberValue':
93-
if (this._streamKeys) {
94-
this.push({name: 'startKey'});
95-
this.push({name: 'stringChunk', value: key});
96-
this.push({name: 'endKey'});
97-
}
98-
this.push({name: 'keyValue', value: key});
99-
break;
100-
}
101-
}
102-
}
103-
this.push(chunk);
104-
return false;
12+
const replace = options => {
13+
let replacementValue = options?.replacement,
14+
replacement = defaultReplacement;
15+
switch (typeof replacementValue) {
16+
case 'function':
17+
replacement = replacementValue;
18+
break;
19+
case 'object':
20+
if (Array.isArray(replacementValue)) replacementValue = many(replacementValue);
21+
replacement = () => replacementValue;
22+
break;
10523
}
106-
107-
_skipKeyChunks(chunk, _, callback) {
108-
if (chunk.name === 'endKey') {
109-
this._transform = this._check;
24+
const stackDiffer = makeStackDiffer();
25+
return filterBase({
26+
specialAction: 'reject',
27+
defaultAction: 'accept-token',
28+
transition(stack, chunk, action, options) {
29+
if (action !== 'reject' && action !== 'reject-value') return stackDiffer(stack, chunk, options);
30+
let replacementTokens = replacement(stack, chunk, options);
31+
if (Array.isArray(replacementTokens)) replacementTokens = many(replacementTokens);
32+
if (replacementTokens === none || (isMany(replacementTokens) && !getManyValues(replacementTokens).length)) return none;
33+
return combineManyMut(stackDiffer(stack, null, options), replacementTokens);
11034
}
111-
callback(null);
112-
}
113-
}
114-
Replace.replace = Replace.make;
115-
Replace.make.Constructor = Replace;
35+
})(options);
36+
};
37+
38+
module.exports = replace;
39+
module.exports.replace = replace;
11640

117-
module.exports = Replace;
41+
module.exports.withParser = options => withParser(replace, Object.assign({packKeys: true}, options));
42+
module.exports.withParserAsStream = options => withParser.asStream(replace, Object.assign({packKeys: true}, options));

‎tests/test-replace.mjs

+345
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
'use strict';
2+
3+
import test from 'tape-six';
4+
import chain, {none, many} from 'stream-chain';
5+
6+
import replace from '../src/filters/replace.js';
7+
import streamArray from '../src/streamers/stream-array.js';
8+
9+
import readString from './read-string.mjs';
10+
11+
test.asPromise('replace', (t, resolve, reject) => {
12+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
13+
pipeline = chain([readString(JSON.stringify(input)), replace.withParser({packValues: false, filter: stack => stack[0] % 2})]),
14+
expected = [
15+
'startArray',
16+
'startObject',
17+
'startKey',
18+
'stringChunk',
19+
'endKey',
20+
'keyValue',
21+
'startObject',
22+
'endObject',
23+
'endObject',
24+
// 'nullValue', // removed
25+
'startObject',
26+
'startKey',
27+
'stringChunk',
28+
'endKey',
29+
'keyValue',
30+
'nullValue',
31+
'endObject',
32+
// 'nullValue', // removed
33+
'startObject',
34+
'startKey',
35+
'stringChunk',
36+
'endKey',
37+
'keyValue',
38+
'startString',
39+
'stringChunk',
40+
'endString',
41+
'endObject',
42+
'endArray'
43+
],
44+
result = [];
45+
46+
pipeline.on('data', chunk => result.push(chunk.name));
47+
pipeline.on('error', reject);
48+
pipeline.on('end', () => {
49+
t.deepEqual(result, expected);
50+
resolve();
51+
});
52+
});
53+
54+
const nullToken = {name: 'nullValue', value: null};
55+
56+
test.asPromise('replace: nulls for arrays', (t, resolve, reject) => {
57+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
58+
pipeline = chain([
59+
readString(JSON.stringify(input)),
60+
replace.withParser({
61+
packValues: false,
62+
filter: stack => stack[0] % 2,
63+
replacement: stack => (stack.length && typeof stack[stack.length - 1] == 'number' ? nullToken : none)
64+
})
65+
]),
66+
expected = [
67+
'startArray',
68+
'startObject',
69+
'startKey',
70+
'stringChunk',
71+
'endKey',
72+
'keyValue',
73+
'startObject',
74+
'endObject',
75+
'endObject',
76+
'nullValue',
77+
'startObject',
78+
'startKey',
79+
'stringChunk',
80+
'endKey',
81+
'keyValue',
82+
'nullValue',
83+
'endObject',
84+
'nullValue',
85+
'startObject',
86+
'startKey',
87+
'stringChunk',
88+
'endKey',
89+
'keyValue',
90+
'startString',
91+
'stringChunk',
92+
'endString',
93+
'endObject',
94+
'endArray'
95+
],
96+
result = [];
97+
98+
pipeline.on('data', chunk => result.push(chunk.name));
99+
pipeline.on('error', reject);
100+
pipeline.on('end', () => {
101+
t.deepEqual(result, expected);
102+
resolve();
103+
});
104+
});
105+
106+
test.asPromise('replace: no streaming', (t, resolve, reject) => {
107+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
108+
pipeline = chain([readString(JSON.stringify(input)), replace.withParser({streamValues: false, filter: stack => stack[0] % 2, streamValues: false})]),
109+
expected = [
110+
'startArray',
111+
'startObject',
112+
'keyValue',
113+
'startObject',
114+
'endObject',
115+
'endObject',
116+
// 'nullValue', // removed
117+
'startObject',
118+
'keyValue',
119+
'nullValue',
120+
'endObject',
121+
// 'nullValue', // removed
122+
'startObject',
123+
'keyValue',
124+
'stringValue',
125+
'endObject',
126+
'endArray'
127+
],
128+
result = [];
129+
130+
pipeline.on('data', chunk => result.push(chunk.name));
131+
pipeline.on('error', reject);
132+
pipeline.on('end', () => {
133+
t.deepEqual(result, expected);
134+
resolve();
135+
});
136+
});
137+
138+
test.asPromise('replace: objects', (t, resolve, reject) => {
139+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
140+
pipeline = chain([readString(JSON.stringify(input)), replace.withParser({filter: stack => stack[0] % 2}), streamArray()]),
141+
expected = [{a: {}}, {c: null}, {e: 'e'}],
142+
result = [];
143+
144+
pipeline.on('data', chunk => result.push(chunk.value));
145+
pipeline.on('error', reject);
146+
pipeline.on('end', () => {
147+
t.deepEqual(result, expected);
148+
resolve();
149+
});
150+
});
151+
152+
test.asPromise('replace: objects with a string filter', (t, resolve, reject) => {
153+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
154+
pipeline = chain([readString(JSON.stringify(input)), replace.withParser({filter: '1'}), streamArray()]),
155+
expected = [{a: {}}, {c: null}, {d: 1}, {e: 'e'}],
156+
result = [];
157+
158+
pipeline.on('data', chunk => result.push(chunk.value));
159+
pipeline.on('error', reject);
160+
pipeline.on('end', () => {
161+
t.deepEqual(result, expected);
162+
resolve();
163+
});
164+
});
165+
166+
test.asPromise('replace: objects with a RegExp filter', (t, resolve, reject) => {
167+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
168+
pipeline = chain([readString(JSON.stringify(input)), replace.withParser({filter: /\b[1-5]\.[a-d]\b/, replacement: () => nullToken}), streamArray()]),
169+
expected = [{a: {}}, {b: null}, {c: null}, {d: null}, {e: 'e'}],
170+
result = [];
171+
172+
pipeline.on('data', chunk => result.push(chunk.value));
173+
pipeline.on('error', reject);
174+
pipeline.on('end', () => {
175+
t.deepEqual(result, expected);
176+
resolve();
177+
});
178+
});
179+
180+
test.asPromise('replace: empty', (t, resolve, reject) => {
181+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
182+
pipeline = chain([readString(JSON.stringify(input)), replace.withParser({filter: stack => stack.length, replacement: () => nullToken}), streamArray()]),
183+
expected = [null, null, null, null, null],
184+
result = [];
185+
186+
pipeline.on('data', chunk => result.push(chunk.value));
187+
pipeline.on('error', reject);
188+
pipeline.on('end', () => {
189+
t.deepEqual(result, expected);
190+
resolve();
191+
});
192+
});
193+
194+
test.asPromise('replace: objects once w/ RegExp filter', (t, resolve, reject) => {
195+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
196+
pipeline = chain([
197+
readString(JSON.stringify(input)),
198+
replace.withParser({filter: /\b[1-5]\.[a-d]\b/, once: true, replacement: () => nullToken}),
199+
streamArray()
200+
]),
201+
expected = [{a: {}}, {b: null}, {c: null}, {d: 1}, {e: 'e'}],
202+
result = [];
203+
204+
pipeline.on('data', chunk => result.push(chunk.value));
205+
pipeline.on('error', reject);
206+
pipeline.on('end', () => {
207+
t.deepEqual(result, expected);
208+
resolve();
209+
});
210+
});
211+
212+
test.asPromise('replace: many', (t, resolve, reject) => {
213+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
214+
pipeline = chain([
215+
readString(JSON.stringify(input)),
216+
replace.withParser({
217+
filter: /^\d\.\w\b/,
218+
replacement: many([{name: 'startNumber'}, {name: 'numberChunk', value: '0'}, {name: 'endNumber'}, {name: 'numberValue', value: '0'}])
219+
}),
220+
streamArray()
221+
]),
222+
expected = [{a: 0}, {b: 0}, {c: 0}, {d: 0}, {e: 0}],
223+
result = [];
224+
225+
pipeline.on('data', chunk => result.push(chunk.value));
226+
pipeline.on('error', reject);
227+
pipeline.on('end', () => {
228+
t.deepEqual(result, expected);
229+
resolve();
230+
});
231+
});
232+
233+
test.asPromise('replace: array', (t, resolve, reject) => {
234+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
235+
pipeline = chain([
236+
readString(JSON.stringify(input)),
237+
replace.withParser({
238+
filter: /^\d\.\w\b/,
239+
replacement: [{name: 'startNumber'}, {name: 'numberChunk', value: '0'}, {name: 'endNumber'}, {name: 'numberValue', value: '0'}]
240+
}),
241+
streamArray()
242+
]),
243+
expected = [{a: 0}, {b: 0}, {c: 0}, {d: 0}, {e: 0}],
244+
result = [];
245+
246+
pipeline.on('data', chunk => result.push(chunk.value));
247+
pipeline.on('error', reject);
248+
pipeline.on('end', () => {
249+
t.deepEqual(result, expected);
250+
resolve();
251+
});
252+
});
253+
254+
test.asPromise('replace: string', (t, resolve, reject) => {
255+
const replacement = (_stack, chunk) => [
256+
{name: 'startString'},
257+
{name: 'stringChunk', value: chunk.name},
258+
{name: 'endString'},
259+
{name: 'stringValue', value: chunk.name}
260+
];
261+
262+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
263+
pipeline = chain([
264+
readString(JSON.stringify(input)),
265+
replace.withParser({
266+
filter: /^\d\.\w\b/,
267+
replacement
268+
}),
269+
streamArray()
270+
]),
271+
expected = [{a: 'startObject'}, {b: 'startArray'}, {c: 'nullValue'}, {d: 'startNumber'}, {e: 'startString'}],
272+
result = [];
273+
274+
pipeline.on('data', chunk => result.push(chunk.value));
275+
pipeline.on('error', reject);
276+
pipeline.on('end', () => {
277+
t.deepEqual(result, expected);
278+
resolve();
279+
});
280+
});
281+
282+
test.asPromise('replace: empty replacement', (t, resolve, reject) => {
283+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
284+
pipeline = chain([
285+
readString(JSON.stringify(input)),
286+
replace.withParser({
287+
filter: /^\d\.\w\b/,
288+
replacement: () => none
289+
}),
290+
streamArray()
291+
]),
292+
expected = [{}, {}, {}, {}, {}],
293+
result = [];
294+
295+
pipeline.on('data', chunk => result.push(chunk.value));
296+
pipeline.on('error', reject);
297+
pipeline.on('end', () => {
298+
t.deepEqual(result, expected);
299+
resolve();
300+
});
301+
});
302+
303+
test.asPromise('replace: null replacement', (t, resolve, reject) => {
304+
const input = [{a: {}}, {b: []}, {c: null}, {d: 1}, {e: 'e'}],
305+
pipeline = chain([
306+
readString(JSON.stringify(input)),
307+
replace.withParser({
308+
filter: /^\d\.\w\b/,
309+
replacement: () => nullToken
310+
}),
311+
streamArray()
312+
]),
313+
expected = [{a: null}, {b: null}, {c: null}, {d: null}, {e: null}],
314+
result = [];
315+
316+
pipeline.on('data', chunk => result.push(chunk.value));
317+
pipeline.on('error', reject);
318+
pipeline.on('end', () => {
319+
t.deepEqual(result, expected);
320+
resolve();
321+
});
322+
});
323+
324+
test.asPromise('replace: bug63', (t, resolve, reject) => {
325+
const input = [true, 42, {a: true, b: 42, c: 'hello'}, 'hello'],
326+
pipeline = chain([
327+
readString(JSON.stringify(input)),
328+
replace.withParser({
329+
packValues: true,
330+
streamValues: false,
331+
filter: '2.b',
332+
replacement: {name: 'numberValue', value: '0'}
333+
}),
334+
streamArray()
335+
]),
336+
expected = [true, 42, {a: true, b: 0, c: 'hello'}, 'hello'],
337+
result = [];
338+
339+
pipeline.on('data', chunk => result.push(chunk.value));
340+
pipeline.on('error', reject);
341+
pipeline.on('end', () => {
342+
t.deepEqual(result, expected);
343+
resolve();
344+
});
345+
});

0 commit comments

Comments
 (0)
Please sign in to comment.