@chubbyts/chubbyts-decode-encode 2.6.0 → 2.7.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/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
## Description
|
|
21
21
|
|
|
22
|
-
Decode and encode request/response bodies by content type: json, jsonx, url-encoded and yaml.
|
|
22
|
+
Decode and encode request/response bodies by content type: json, jsonx, ndjson, url-encoded and yaml.
|
|
23
23
|
|
|
24
24
|
Strings are decoded into plain data (objects, arrays, strings, numbers, booleans and null) and encoded back. Each format can be registered under additional content types, for example `application/problem+json` or `application/yaml`.
|
|
25
25
|
|
|
@@ -39,7 +39,7 @@ Strings are decoded into plain data (objects, arrays, strings, numbers, booleans
|
|
|
39
39
|
Through [NPM](https://www.npmjs.com) as [@chubbyts/chubbyts-decode-encode][1].
|
|
40
40
|
|
|
41
41
|
```sh
|
|
42
|
-
npm i @chubbyts/chubbyts-decode-encode@^2.
|
|
42
|
+
npm i @chubbyts/chubbyts-decode-encode@^2.7.0
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
## Usage
|
|
@@ -110,6 +110,30 @@ const contentType = jsonxTypeDecoder.contentType;
|
|
|
110
110
|
// contentType: 'application/jsonx+xml'
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
#### createNdjsonTypeDecoder
|
|
114
|
+
|
|
115
|
+
[NDJSON](https://github.com/ndjson/ndjson-spec) / [JSON Lines](https://jsonlines.org): one json document per line, decoded into an array.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { createNdjsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/ndjson-type-decoder';
|
|
119
|
+
|
|
120
|
+
const ndjsonTypeDecoder = createNdjsonTypeDecoder();
|
|
121
|
+
const data = ndjsonTypeDecoder.decode('{"key":"value1"}\n{"key":"value2"}\n');
|
|
122
|
+
// data: [{ key: 'value1' }, { key: 'value2' }]
|
|
123
|
+
const contentType = ndjsonTypeDecoder.contentType;
|
|
124
|
+
// contentType: 'application/jsonl'
|
|
125
|
+
|
|
126
|
+
// any other ndjson / json lines content type, like `application/x-ndjson`
|
|
127
|
+
const xNdjsonTypeDecoder = createNdjsonTypeDecoder('application/x-ndjson');
|
|
128
|
+
|
|
129
|
+
// silently ignore empty lines
|
|
130
|
+
const lenientNdjsonTypeDecoder = createNdjsonTypeDecoder('application/jsonl', true);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`\n` and `\r\n` line endings are accepted, the line terminator after the last line is optional. Empty lines (including lines with only json whitespace: space, tab, `\r`) are not valid json and throw a `DecodeError`, unless `ignoreEmptyLines` (second argument) is `true`. The `DecodeError` message starts with the line number, for example `Line 3: ...`.
|
|
134
|
+
|
|
135
|
+
There is no registered media type for ndjson / json lines, so the service factories register all common ones: `application/jsonl`, `application/x-jsonl`, `text/jsonl`, `application/ndjson` and `application/x-ndjson`.
|
|
136
|
+
|
|
113
137
|
#### createUrlEncodedTypeDecoder
|
|
114
138
|
|
|
115
139
|
```ts
|
|
@@ -195,6 +219,23 @@ const contentType = jsonxTypeEncoder.contentType;
|
|
|
195
219
|
// contentType: 'application/jsonx+xml'
|
|
196
220
|
```
|
|
197
221
|
|
|
222
|
+
#### createNdjsonTypeEncoder
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { createNdjsonTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/ndjson-type-encoder';
|
|
226
|
+
|
|
227
|
+
const ndjsonTypeEncoder = createNdjsonTypeEncoder();
|
|
228
|
+
const encodedData = ndjsonTypeEncoder.encode([{ key: 'value1' }, { key: 'value2' }]);
|
|
229
|
+
// encodedData: '{"key":"value1"}\n{"key":"value2"}\n'
|
|
230
|
+
const contentType = ndjsonTypeEncoder.contentType;
|
|
231
|
+
// contentType: 'application/jsonl'
|
|
232
|
+
|
|
233
|
+
// any other ndjson / json lines content type, like `application/x-ndjson`
|
|
234
|
+
const xNdjsonTypeEncoder = createNdjsonTypeEncoder('application/x-ndjson');
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Every line, including the last one, is terminated by `\n`. Only arrays can be encoded, any other value throws an `EncodeError`.
|
|
238
|
+
|
|
198
239
|
#### createUrlEncodedTypeEncoder
|
|
199
240
|
|
|
200
241
|
```ts
|
|
@@ -224,43 +265,40 @@ const officialYamlTypeEncoder = createYamlTypeEncoder('application/yaml');
|
|
|
224
265
|
|
|
225
266
|
### Custom type decoders / encoders
|
|
226
267
|
|
|
227
|
-
A type decoder / encoder is a plain object with a `contentType` and a `decode` / `encode` function. This adds
|
|
268
|
+
A type decoder / encoder is a plain object with a `contentType` and a `decode` / `encode` function. This adds `text/plain`, where the body is a single string:
|
|
228
269
|
|
|
229
270
|
```ts
|
|
230
271
|
import type { Data } from '@chubbyts/chubbyts-decode-encode/data';
|
|
231
|
-
import {
|
|
272
|
+
import { isString } from '@chubbyts/chubbyts-decode-encode/data';
|
|
232
273
|
import type { TypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
|
|
233
|
-
import {
|
|
274
|
+
import { createDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
|
|
234
275
|
import type { TypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
|
|
235
|
-
import { createEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
|
|
276
|
+
import { EncodeError, createEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
|
|
236
277
|
|
|
237
|
-
const
|
|
238
|
-
decode: (encodedData: string): Data =>
|
|
239
|
-
|
|
240
|
-
return encodedData
|
|
241
|
-
.split('\n')
|
|
242
|
-
.filter((line) => line.trim() !== '')
|
|
243
|
-
.map((line) => JSON.parse(line));
|
|
244
|
-
} catch (e) {
|
|
245
|
-
throw new DecodeError(e instanceof Error ? e.message : String(e));
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
contentType: 'application/x-ndjson',
|
|
278
|
+
const createTextTypeDecoder = (): TypeDecoder => ({
|
|
279
|
+
decode: (encodedData: string): Data => encodedData,
|
|
280
|
+
contentType: 'text/plain',
|
|
249
281
|
});
|
|
250
282
|
|
|
251
|
-
const
|
|
252
|
-
encode: (data: Data): string =>
|
|
253
|
-
|
|
283
|
+
const createTextTypeEncoder = (): TypeEncoder => ({
|
|
284
|
+
encode: (data: Data): string => {
|
|
285
|
+
if (!isString(data)) {
|
|
286
|
+
throw new EncodeError(`Unsupported value: ${JSON.stringify(data)}, expected a string`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return data;
|
|
290
|
+
},
|
|
291
|
+
contentType: 'text/plain',
|
|
254
292
|
});
|
|
255
293
|
|
|
256
|
-
const decoder = createDecoder([
|
|
257
|
-
const encoder = createEncoder([
|
|
294
|
+
const decoder = createDecoder([createTextTypeDecoder()]);
|
|
295
|
+
const encoder = createEncoder([createTextTypeEncoder()]);
|
|
258
296
|
```
|
|
259
297
|
|
|
260
298
|
### Errors
|
|
261
299
|
|
|
262
300
|
* `decode` throws a `DecodeError` (`@chubbyts/chubbyts-decode-encode/decoder/decoder`) if the encoded data is invalid, for example a json syntax error. In a http context this usually maps to `400 Bad Request`.
|
|
263
|
-
* `encode` of the jsonx type
|
|
301
|
+
* `encode` of the jsonx and ndjson type encoders throws an `EncodeError` (`@chubbyts/chubbyts-decode-encode/encoder/encoder`) if a value cannot be represented, for ndjson anything but an array.
|
|
264
302
|
* `decode` / `encode` of the decoder / encoder throw an `Error` if the content type is not supported, the message lists the supported content types. In a http context this usually maps to `415 Unsupported Media Type` (decoder) or `406 Not Acceptable` (encoder).
|
|
265
303
|
|
|
266
304
|
```ts
|
|
@@ -279,8 +317,8 @@ try {
|
|
|
279
317
|
|
|
280
318
|
The package ships service factories (abstract factories built on [chubbyts-dic-config-factory][9]) for a [chubbyts-dic-config][8] (or any [chubbyts-dic-types][7] compatible) container within `@chubbyts/chubbyts-decode-encode/service-factory`:
|
|
281
319
|
|
|
282
|
-
* `typeDecodersServiceFactory`: all shipped type decoders (json, jsonx, urlencoded, yaml as `application/x-yaml` and `application/yaml`)
|
|
283
|
-
* `typeEncodersServiceFactory`: all shipped type encoders (json, jsonx, urlencoded, yaml as `application/x-yaml` and `application/yaml`), `config.debug` enables pretty printing for json / jsonx
|
|
320
|
+
* `typeDecodersServiceFactory`: all shipped type decoders (json, jsonx, ndjson as `application/jsonl`, `application/x-jsonl`, `text/jsonl`, `application/ndjson` and `application/x-ndjson`, urlencoded, yaml as `application/x-yaml` and `application/yaml`)
|
|
321
|
+
* `typeEncodersServiceFactory`: all shipped type encoders (json, jsonx, ndjson as `application/jsonl`, `application/x-jsonl`, `text/jsonl`, `application/ndjson` and `application/x-ndjson`, urlencoded, yaml as `application/x-yaml` and `application/yaml`), `config.debug` enables pretty printing for json / jsonx
|
|
284
322
|
* `decoderServiceFactory`: the decoder, built from the `typeDecoders` service if registered, the shipped type decoders otherwise
|
|
285
323
|
* `encoderServiceFactory`: the encoder, built from the `typeEncoders` service if registered, the shipped type encoders otherwise
|
|
286
324
|
|
|
@@ -305,7 +343,7 @@ const decoder = container.get<Decoder>('decoder');
|
|
|
305
343
|
const encoder = container.get<Encoder>('encoder');
|
|
306
344
|
```
|
|
307
345
|
|
|
308
|
-
To replace the type decoders / encoders, register a `typeDecoders` / `typeEncoders` service (`
|
|
346
|
+
To replace the type decoders / encoders, register a `typeDecoders` / `typeEncoders` service (`createTextTypeDecoder` from [Custom type decoders / encoders](#custom-type-decoders--encoders)):
|
|
309
347
|
|
|
310
348
|
```ts
|
|
311
349
|
import type { TypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
|
|
@@ -315,7 +353,7 @@ const container = createContainerByConfigFactory({
|
|
|
315
353
|
dependencies: {
|
|
316
354
|
factories: new Map<string, ConfigFactory>([
|
|
317
355
|
['decoder', decoderServiceFactory()],
|
|
318
|
-
['typeDecoders', (): Array<TypeDecoder> => [createJsonTypeDecoder(),
|
|
356
|
+
['typeDecoders', (): Array<TypeDecoder> => [createJsonTypeDecoder(), createTextTypeDecoder()]],
|
|
319
357
|
]),
|
|
320
358
|
},
|
|
321
359
|
})();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { throwableToError } from '@chubbyts/chubbyts-throwable-to-error/dist/throwable-to-error';
|
|
2
|
+
import { DecodeError } from './decoder.js';
|
|
3
|
+
// only json whitespace (RFC 8259), "\n" is the line separator, String.prototype.trim() would also strip a BOM, nbsp, ...
|
|
4
|
+
const EMPTY_LINE = /^[ \t\r]*$/;
|
|
5
|
+
const decodeLine = (line, index) => {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(line);
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
const error = throwableToError(e);
|
|
11
|
+
throw new DecodeError(`Line ${index + 1}: ${error.message}`, error.stack);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export const createNdjsonTypeDecoder = (contentType = 'application/jsonl', ignoreEmptyLines = false) => {
|
|
15
|
+
return {
|
|
16
|
+
decode: (encodedData) => {
|
|
17
|
+
const lines = encodedData.split('\n');
|
|
18
|
+
// the line terminator after the last line does not start another line
|
|
19
|
+
return (lines[lines.length - 1] === '' ? lines.slice(0, -1) : lines).flatMap((line, index) => {
|
|
20
|
+
if (EMPTY_LINE.test(line)) {
|
|
21
|
+
if (ignoreEmptyLines) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
throw new DecodeError(`Line ${index + 1}: empty line`);
|
|
25
|
+
}
|
|
26
|
+
return [decodeLine(line, index)];
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
contentType,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isArray } from '../data.js';
|
|
2
|
+
import { EncodeError } from './encoder.js';
|
|
3
|
+
export const createNdjsonTypeEncoder = (contentType = 'application/jsonl') => {
|
|
4
|
+
return {
|
|
5
|
+
encode: (data) => {
|
|
6
|
+
if (!isArray(data)) {
|
|
7
|
+
throw new EncodeError(`Unsupported value: ${JSON.stringify(data)}, expected an array`);
|
|
8
|
+
}
|
|
9
|
+
return data.map((item) => `${JSON.stringify(item)}\n`).join('');
|
|
10
|
+
},
|
|
11
|
+
contentType,
|
|
12
|
+
};
|
|
13
|
+
};
|
package/dist/service-factory.js
CHANGED
|
@@ -2,17 +2,24 @@ import { createAbstractFactory } from '@chubbyts/chubbyts-dic-config-factory/dis
|
|
|
2
2
|
import { createDecoder } from './decoder/decoder.js';
|
|
3
3
|
import { createJsonTypeDecoder } from './decoder/json-type-decoder.js';
|
|
4
4
|
import { createJsonxTypeDecoder } from './decoder/jsonx-type-decoder.js';
|
|
5
|
+
import { createNdjsonTypeDecoder } from './decoder/ndjson-type-decoder.js';
|
|
5
6
|
import { createUrlEncodedTypeDecoder } from './decoder/url-encoded-type-decoder.js';
|
|
6
7
|
import { createYamlTypeDecoder } from './decoder/yaml-type-decoder.js';
|
|
7
8
|
import { createEncoder } from './encoder/encoder.js';
|
|
8
9
|
import { createJsonTypeEncoder } from './encoder/json-type-encoder.js';
|
|
9
10
|
import { createJsonxTypeEncoder } from './encoder/jsonx-type-encoder.js';
|
|
11
|
+
import { createNdjsonTypeEncoder } from './encoder/ndjson-type-encoder.js';
|
|
10
12
|
import { createUrlEncodedTypeEncoder } from './encoder/url-encoded-type-encoder.js';
|
|
11
13
|
import { createYamlTypeEncoder } from './encoder/yaml-type-encoder.js';
|
|
12
14
|
export const typeDecodersServiceFactory = createAbstractFactory(() => {
|
|
13
15
|
return [
|
|
14
16
|
createJsonTypeDecoder(),
|
|
15
17
|
createJsonxTypeDecoder(),
|
|
18
|
+
createNdjsonTypeDecoder(),
|
|
19
|
+
createNdjsonTypeDecoder('application/x-jsonl'),
|
|
20
|
+
createNdjsonTypeDecoder('text/jsonl'),
|
|
21
|
+
createNdjsonTypeDecoder('application/ndjson'),
|
|
22
|
+
createNdjsonTypeDecoder('application/x-ndjson'),
|
|
16
23
|
createUrlEncodedTypeDecoder(),
|
|
17
24
|
createYamlTypeDecoder(),
|
|
18
25
|
createYamlTypeDecoder('application/yaml'),
|
|
@@ -26,6 +33,11 @@ export const typeEncodersServiceFactory = createAbstractFactory((container) => {
|
|
|
26
33
|
return [
|
|
27
34
|
createJsonTypeEncoder(debug),
|
|
28
35
|
createJsonxTypeEncoder(debug),
|
|
36
|
+
createNdjsonTypeEncoder(),
|
|
37
|
+
createNdjsonTypeEncoder('application/x-jsonl'),
|
|
38
|
+
createNdjsonTypeEncoder('text/jsonl'),
|
|
39
|
+
createNdjsonTypeEncoder('application/ndjson'),
|
|
40
|
+
createNdjsonTypeEncoder('application/x-ndjson'),
|
|
29
41
|
createUrlEncodedTypeEncoder(),
|
|
30
42
|
createYamlTypeEncoder(),
|
|
31
43
|
createYamlTypeEncoder('application/yaml'),
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chubbyts/chubbyts-decode-encode",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
5
|
-
"description": "Decode and encode request/response bodies by content type: json, jsonx, url-encoded and yaml.",
|
|
4
|
+
"version": "2.7.0",
|
|
5
|
+
"description": "Decode and encode request/response bodies by content type: json, jsonx, ndjson, url-encoded and yaml.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"chubbyts",
|
|
8
8
|
"content-type",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"encode",
|
|
11
11
|
"json",
|
|
12
12
|
"jsonx",
|
|
13
|
+
"ndjson",
|
|
13
14
|
"serializer",
|
|
14
15
|
"url-encoded",
|
|
15
16
|
"xml",
|