@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.6.0
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 [NDJSON](https://github.com/ndjson/ndjson-spec), one json document per line:
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 { isArray } from '@chubbyts/chubbyts-decode-encode/data';
272
+ import { isString } from '@chubbyts/chubbyts-decode-encode/data';
232
273
  import type { TypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
233
- import { DecodeError, createDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
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 createNdjsonTypeDecoder = (): TypeDecoder => ({
238
- decode: (encodedData: string): Data => {
239
- try {
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 createNdjsonTypeEncoder = (): TypeEncoder => ({
252
- encode: (data: Data): string => (isArray(data) ? data : [data]).map((item) => JSON.stringify(item)).join('\n'),
253
- contentType: 'application/x-ndjson',
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([createNdjsonTypeDecoder()]);
257
- const encoder = createEncoder([createNdjsonTypeEncoder()]);
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 encoder throws an `EncodeError` (`@chubbyts/chubbyts-decode-encode/encoder/encoder`) if a value cannot be represented.
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 (`createNdjsonTypeDecoder` from [Custom type decoders / encoders](#custom-type-decoders--encoders)):
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(), createNdjsonTypeDecoder()]],
356
+ ['typeDecoders', (): Array<TypeDecoder> => [createJsonTypeDecoder(), createTextTypeDecoder()]],
319
357
  ]),
320
358
  },
321
359
  })();
@@ -0,0 +1,2 @@
1
+ import type { TypeDecoder } from './decoder.js';
2
+ export declare const createNdjsonTypeDecoder: (contentType?: string, ignoreEmptyLines?: boolean) => TypeDecoder;
@@ -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,2 @@
1
+ import type { TypeEncoder } from './encoder.js';
2
+ export declare const createNdjsonTypeEncoder: (contentType?: string) => TypeEncoder;
@@ -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
+ };
@@ -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.6.0",
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",