@chubbyts/chubbyts-decode-encode 2.5.2 → 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,9 @@
19
19
 
20
20
  ## Description
21
21
 
22
- A simple decode/encode solution for json / jsonx / url-encoded / xml / yaml.
22
+ Decode and encode request/response bodies by content type: json, jsonx, ndjson, url-encoded and yaml.
23
+
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`.
23
25
 
24
26
  ## Requirements
25
27
 
@@ -36,43 +38,65 @@ A simple decode/encode solution for json / jsonx / url-encoded / xml / yaml.
36
38
 
37
39
  Through [NPM](https://www.npmjs.com) as [@chubbyts/chubbyts-decode-encode][1].
38
40
 
39
- ```ts
40
- npm i @chubbyts/chubbyts-decode-encode@^2.5.2
41
+ ```sh
42
+ npm i @chubbyts/chubbyts-decode-encode@^2.7.0
41
43
  ```
42
44
 
43
45
  ## Usage
44
46
 
47
+ ### Data
48
+
49
+ Every type decoder returns and every type encoder accepts `Data`, the json data model:
50
+
51
+ ```ts
52
+ type Data = { [key: string]: Data } | Array<Data> | string | number | boolean | null;
53
+ ```
54
+
55
+ The matching type guards `isNull`, `isBoolean`, `isNumber`, `isString`, `isArray` and `isObject` (plain objects only) can be imported from `@chubbyts/chubbyts-decode-encode/data`.
56
+
57
+ ```ts
58
+ import { isObject } from '@chubbyts/chubbyts-decode-encode/data';
59
+
60
+ isObject({ key: 'value' }); // true
61
+ isObject(new Date()); // false
62
+ ```
63
+
45
64
  ### Decoder
46
65
 
47
66
  #### createDecoder
48
67
 
49
68
  ```ts
50
- import { createDecoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/decoder';
51
- import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/json-type-decoder';
69
+ import { createDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
70
+ import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/json-type-decoder';
52
71
 
53
72
  const decoder = createDecoder([createJsonTypeDecoder()]);
54
- const data = decoder.decode('{"key":"value"}', 'application/json'); // or with 3th argument, for example { user: 'username1' }
55
- // data: {key: "value"}
73
+ const data = decoder.decode('{"key":"value"}', 'application/json');
74
+ // data: { key: 'value' }
56
75
  const contentTypes = decoder.contentTypes;
57
76
  // contentTypes: ['application/json']
58
77
  ```
59
78
 
79
+ The `Decoder` type accepts an optional third argument `context` (`Record<string, unknown>`). The shipped decoder ignores it, it exists for custom `Decoder` implementations, for example one that decodes depending on the current user.
80
+
60
81
  #### createJsonTypeDecoder
61
82
 
62
83
  ```ts
63
- import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/json-type-decoder';
84
+ import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/json-type-decoder';
64
85
 
65
86
  const jsonTypeDecoder = createJsonTypeDecoder();
66
87
  const data = jsonTypeDecoder.decode('{"key":"value"}');
67
- // data: {key: "value"}
88
+ // data: { key: 'value' }
68
89
  const contentType = jsonTypeDecoder.contentType;
69
90
  // contentType: 'application/json'
91
+
92
+ // any other json based content type, like `application/problem+json` (RFC 9457)
93
+ const problemJsonTypeDecoder = createJsonTypeDecoder('application/problem+json');
70
94
  ```
71
95
 
72
96
  #### createJsonxTypeDecoder
73
97
 
74
98
  ```ts
75
- import { createJsonxTypeDecoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/jsonx-type-decoder';
99
+ import { createJsonxTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/jsonx-type-decoder';
76
100
 
77
101
  const jsonxTypeDecoder = createJsonxTypeDecoder();
78
102
  const data = jsonxTypeDecoder.decode(`
@@ -81,33 +105,67 @@ const data = jsonxTypeDecoder.decode(`
81
105
  <json:string name="key">value</json:string>
82
106
  </json:object>
83
107
  `);
84
- // data: {key: "value"}
108
+ // data: { key: 'value' }
85
109
  const contentType = jsonxTypeDecoder.contentType;
86
110
  // contentType: 'application/jsonx+xml'
87
111
  ```
88
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
+
89
137
  #### createUrlEncodedTypeDecoder
90
138
 
91
139
  ```ts
92
- import { createUrlEncodedTypeDecoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/url-encoded-type-decoder';
140
+ import { createUrlEncodedTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/url-encoded-type-decoder';
93
141
 
94
142
  const urlEncodedTypeDecoder = createUrlEncodedTypeDecoder();
95
143
  const data = urlEncodedTypeDecoder.decode('key=value');
96
- // data: {key: "value"}
144
+ // data: { key: 'value' }
97
145
  const contentType = urlEncodedTypeDecoder.contentType;
98
146
  // contentType: 'application/x-www-form-urlencoded'
99
147
  ```
100
148
 
149
+ Values are converted to their json type: `true` and `false` become booleans, `null` becomes `null` and a value becomes a number only if the number converts back to the exact same string, so `01` or `1.0` stay strings.
150
+
151
+ ```ts
152
+ const data = urlEncodedTypeDecoder.decode('a=1&b=true&c=null&d=01&e[]=x');
153
+ // data: { a: 1, b: true, c: null, d: '01', e: ['x'] }
154
+ ```
155
+
101
156
  #### createYamlTypeDecoder
102
157
 
103
158
  ```ts
104
- import { createYamlTypeDecoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/yaml-type-decoder';
159
+ import { createYamlTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/yaml-type-decoder';
105
160
 
106
161
  const yamlTypeDecoder = createYamlTypeDecoder();
107
162
  const data = yamlTypeDecoder.decode('key: value');
108
- // data: {key: "value"}
163
+ // data: { key: 'value' }
109
164
  const contentType = yamlTypeDecoder.contentType;
110
165
  // contentType: 'application/x-yaml'
166
+
167
+ // the registered media type `application/yaml` (RFC 9512)
168
+ const officialYamlTypeDecoder = createYamlTypeDecoder('application/yaml');
111
169
  ```
112
170
 
113
171
  ### Encoder
@@ -115,82 +173,161 @@ const contentType = yamlTypeDecoder.contentType;
115
173
  #### createEncoder
116
174
 
117
175
  ```ts
118
- import { createEncoder } from '@chubbyts/chubbyts-encode-encode/dist/encoder/encoder';
119
- import { createJsonTypeEncoder } from '@chubbyts/chubbyts-encode-encode/dist/encoder/json-type-encoder';
176
+ import { createEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
177
+ import { createJsonTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/json-type-encoder';
120
178
 
121
179
  const encoder = createEncoder([createJsonTypeEncoder()]);
122
- const encodedData = encoder.encode({key: "value"}, 'application/json'); // or with 3th argument, for example { user: 'username1' }
123
- // encodedData: {"key":"value"}
180
+ const encodedData = encoder.encode({ key: 'value' }, 'application/json');
181
+ // encodedData: '{"key":"value"}'
124
182
  const contentTypes = encoder.contentTypes;
125
183
  // contentTypes: ['application/json']
126
184
  ```
127
185
 
186
+ The `Encoder` type accepts an optional third argument `context` (`Record<string, unknown>`). The shipped encoder ignores it, it exists for custom `Encoder` implementations, for example one that encodes depending on the current user.
187
+
128
188
  #### createJsonTypeEncoder
129
189
 
130
190
  ```ts
131
- import { createJsonTypeEncoder } from '@chubbyts/chubbyts-encode-encode/dist/encoder/json-type-encoder';
191
+ import { createJsonTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/json-type-encoder';
132
192
 
133
193
  const jsonTypeEncoder = createJsonTypeEncoder();
134
- const encodedData = jsonTypeEncoder.encode({key: "value"});
135
- // encodedData: {"key":"value"}
194
+ const encodedData = jsonTypeEncoder.encode({ key: 'value' });
195
+ // encodedData: '{"key":"value"}'
136
196
  const contentType = jsonTypeEncoder.contentType;
137
- // contentTypes: 'application/json'
197
+ // contentType: 'application/json'
198
+
199
+ // pretty printed
200
+ const prettyJsonTypeEncoder = createJsonTypeEncoder(true);
201
+
202
+ // any other json based content type, like `application/problem+json` (RFC 9457)
203
+ const problemJsonTypeEncoder = createJsonTypeEncoder(false, 'application/problem+json');
138
204
  ```
139
205
 
140
206
  #### createJsonxTypeEncoder
141
207
 
142
208
  ```ts
143
- import { createJsonxTypeEncoder } from '@chubbyts/chubbyts-encode-encode/dist/encoder/jsonx-type-encoder';
209
+ import { createJsonxTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/jsonx-type-encoder';
144
210
 
145
- const jsonxTypeEncoder = createJsonxTypeEncoder();
146
- const encodedData = jsonxTypeEncoder.encode({key: "value"});
147
- // encodedData: <?xml version="1.0" encoding="UTF-8"?>
148
- // <json:object>
211
+ const jsonxTypeEncoder = createJsonxTypeEncoder(true); // pretty printed, defaults to false
212
+ const encodedData = jsonxTypeEncoder.encode({ key: 'value' });
213
+ // encodedData:
214
+ // <?xml version="1.0" encoding="UTF-8"?>
215
+ // <json:object xsi:schemaLocation="http://www.datapower.com/schemas/json jsonx.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:json="http://www.ibm.com/xmlns/prod/2009/jsonx">
149
216
  // <json:string name="key">value</json:string>
150
217
  // </json:object>
151
218
  const contentType = jsonxTypeEncoder.contentType;
152
- // contentTypes: 'application/jsonx+xml'
219
+ // contentType: 'application/jsonx+xml'
220
+ ```
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');
153
235
  ```
154
236
 
237
+ Every line, including the last one, is terminated by `\n`. Only arrays can be encoded, any other value throws an `EncodeError`.
238
+
155
239
  #### createUrlEncodedTypeEncoder
156
240
 
157
241
  ```ts
158
- import { createUrlEncodedTypeEncoder } from '@chubbyts/chubbyts-encode-encode/dist/encoder/url-encoded-type-encoder';
242
+ import { createUrlEncodedTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/url-encoded-type-encoder';
159
243
 
160
244
  const urlEncodedTypeEncoder = createUrlEncodedTypeEncoder();
161
- const encodedData = urlEncodedTypeEncoder.encode({key: "value"});
162
- // encodedData: key=value
245
+ const encodedData = urlEncodedTypeEncoder.encode({ a: 1, b: true, c: null, d: [1, 2] });
246
+ // encodedData: 'a=1&b=true&c=null&d[0]=1&d[1]=2'
163
247
  const contentType = urlEncodedTypeEncoder.contentType;
164
- // contentTypes: 'application/x-www-form-urlencoded'
248
+ // contentType: 'application/x-www-form-urlencoded'
165
249
  ```
166
250
 
167
251
  #### createYamlTypeEncoder
168
252
 
169
253
  ```ts
170
- import { createYamlTypeEncoder } from '@chubbyts/chubbyts-encode-encode/dist/encoder/yaml-type-encoder';
254
+ import { createYamlTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/yaml-type-encoder';
171
255
 
172
256
  const yamlTypeEncoder = createYamlTypeEncoder();
173
- const encodedData = yamlTypeEncoder.encode({key: "value"});
174
- // encodedData: key: value
257
+ const encodedData = yamlTypeEncoder.encode({ key: 'value' });
258
+ // encodedData: 'key: value\n'
175
259
  const contentType = yamlTypeEncoder.contentType;
176
- // contentTypes: 'application/x-yaml'
260
+ // contentType: 'application/x-yaml'
261
+
262
+ // the registered media type `application/yaml` (RFC 9512)
263
+ const officialYamlTypeEncoder = createYamlTypeEncoder('application/yaml');
264
+ ```
265
+
266
+ ### Custom type decoders / encoders
267
+
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:
269
+
270
+ ```ts
271
+ import type { Data } from '@chubbyts/chubbyts-decode-encode/data';
272
+ import { isString } from '@chubbyts/chubbyts-decode-encode/data';
273
+ import type { TypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
274
+ import { createDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
275
+ import type { TypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
276
+ import { EncodeError, createEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
277
+
278
+ const createTextTypeDecoder = (): TypeDecoder => ({
279
+ decode: (encodedData: string): Data => encodedData,
280
+ contentType: 'text/plain',
281
+ });
282
+
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',
292
+ });
293
+
294
+ const decoder = createDecoder([createTextTypeDecoder()]);
295
+ const encoder = createEncoder([createTextTypeEncoder()]);
296
+ ```
297
+
298
+ ### Errors
299
+
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`.
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.
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).
303
+
304
+ ```ts
305
+ import { DecodeError } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
306
+
307
+ try {
308
+ decoder.decode('{', 'application/json');
309
+ } catch (e) {
310
+ if (e instanceof DecodeError) {
311
+ // invalid request body
312
+ }
313
+ }
177
314
  ```
178
315
 
179
316
  ### Service factories (chubbyts-dic-config)
180
317
 
181
- 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/dist/service-factory`:
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`:
182
319
 
183
- * `typeDecodersServiceFactory`: all shipped type decoders (json, jsonx, urlencoded, yaml)
184
- * `typeEncodersServiceFactory`: all shipped type encoders (json, jsonx, urlencoded, 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
185
322
  * `decoderServiceFactory`: the decoder, built from the `typeDecoders` service if registered, the shipped type decoders otherwise
186
323
  * `encoderServiceFactory`: the encoder, built from the `typeEncoders` service if registered, the shipped type encoders otherwise
187
324
 
188
325
  ```ts
189
326
  import type { ConfigFactory } from '@chubbyts/chubbyts-dic-config/dist/dic-config';
190
327
  import { createContainerByConfigFactory } from '@chubbyts/chubbyts-dic-config/dist/dic-config';
191
- import type { Decoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/decoder';
192
- import type { Encoder } from '@chubbyts/chubbyts-decode-encode/dist/encoder/encoder';
193
- import { decoderServiceFactory, encoderServiceFactory } from '@chubbyts/chubbyts-decode-encode/dist/service-factory';
328
+ import type { Decoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
329
+ import type { Encoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
330
+ import { decoderServiceFactory, encoderServiceFactory } from '@chubbyts/chubbyts-decode-encode/service-factory';
194
331
 
195
332
  const container = createContainerByConfigFactory({
196
333
  debug: false,
@@ -206,17 +343,17 @@ const decoder = container.get<Decoder>('decoder');
206
343
  const encoder = container.get<Encoder>('encoder');
207
344
  ```
208
345
 
209
- 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)):
210
347
 
211
348
  ```ts
212
- import type { TypeDecoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/decoder';
213
- import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/dist/decoder/json-type-decoder';
349
+ import type { TypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
350
+ import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/json-type-decoder';
214
351
 
215
352
  const container = createContainerByConfigFactory({
216
353
  dependencies: {
217
354
  factories: new Map<string, ConfigFactory>([
218
355
  ['decoder', decoderServiceFactory()],
219
- ['typeDecoders', (): Array<TypeDecoder> => [createJsonTypeDecoder(), createCustomTypeDecoder()]],
356
+ ['typeDecoders', (): Array<TypeDecoder> => [createJsonTypeDecoder(), createTextTypeDecoder()]],
220
357
  ]),
221
358
  },
222
359
  })();
@@ -1,2 +1,2 @@
1
1
  import type { TypeDecoder } from './decoder.js';
2
- export declare const createJsonTypeDecoder: () => TypeDecoder;
2
+ export declare const createJsonTypeDecoder: (contentType?: string) => TypeDecoder;
@@ -1,6 +1,6 @@
1
1
  import { throwableToError } from '@chubbyts/chubbyts-throwable-to-error/dist/throwable-to-error';
2
2
  import { DecodeError } from './decoder.js';
3
- export const createJsonTypeDecoder = () => {
3
+ export const createJsonTypeDecoder = (contentType = 'application/json') => {
4
4
  return {
5
5
  decode: (encodedData) => {
6
6
  try {
@@ -11,6 +11,6 @@ export const createJsonTypeDecoder = () => {
11
11
  throw new DecodeError(error.message, error.stack);
12
12
  }
13
13
  },
14
- contentType: 'application/json',
14
+ contentType,
15
15
  };
16
16
  };
@@ -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
+ };
@@ -1,2 +1,2 @@
1
1
  import type { TypeDecoder } from './decoder.js';
2
- export declare const createYamlTypeDecoder: () => TypeDecoder;
2
+ export declare const createYamlTypeDecoder: (contentType?: string) => TypeDecoder;
@@ -1,7 +1,7 @@
1
1
  import { parse } from 'yaml';
2
2
  import { throwableToError } from '@chubbyts/chubbyts-throwable-to-error/dist/throwable-to-error';
3
3
  import { DecodeError } from './decoder.js';
4
- export const createYamlTypeDecoder = () => {
4
+ export const createYamlTypeDecoder = (contentType = 'application/x-yaml') => {
5
5
  return {
6
6
  decode: (encodedData) => {
7
7
  try {
@@ -12,6 +12,6 @@ export const createYamlTypeDecoder = () => {
12
12
  throw new DecodeError(error.message, error.stack);
13
13
  }
14
14
  },
15
- contentType: 'application/x-yaml',
15
+ contentType,
16
16
  };
17
17
  };
@@ -1,2 +1,2 @@
1
1
  import type { TypeEncoder } from './encoder.js';
2
- export declare const createJsonTypeEncoder: (prettyPrint?: boolean) => TypeEncoder;
2
+ export declare const createJsonTypeEncoder: (prettyPrint?: boolean, contentType?: string) => TypeEncoder;
@@ -1,6 +1,6 @@
1
- export const createJsonTypeEncoder = (prettyPrint = false) => {
1
+ export const createJsonTypeEncoder = (prettyPrint = false, contentType = 'application/json') => {
2
2
  return {
3
3
  encode: (data) => JSON.stringify(data, undefined, prettyPrint ? 4 : undefined),
4
- contentType: 'application/json',
4
+ contentType,
5
5
  };
6
6
  };
@@ -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
+ };
@@ -1,2 +1,2 @@
1
1
  import type { TypeEncoder } from './encoder.js';
2
- export declare const createYamlTypeEncoder: () => TypeEncoder;
2
+ export declare const createYamlTypeEncoder: (contentType?: string) => TypeEncoder;
@@ -1,7 +1,7 @@
1
1
  import { stringify } from 'yaml';
2
- export const createYamlTypeEncoder = () => {
2
+ export const createYamlTypeEncoder = (contentType = 'application/x-yaml') => {
3
3
  return {
4
4
  encode: (data) => stringify(data, { indent: 4 }),
5
- contentType: 'application/x-yaml',
5
+ contentType,
6
6
  };
7
7
  };
@@ -2,15 +2,28 @@ 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
- return [createJsonTypeDecoder(), createJsonxTypeDecoder(), createUrlEncodedTypeDecoder(), createYamlTypeDecoder()];
15
+ return [
16
+ createJsonTypeDecoder(),
17
+ createJsonxTypeDecoder(),
18
+ createNdjsonTypeDecoder(),
19
+ createNdjsonTypeDecoder('application/x-jsonl'),
20
+ createNdjsonTypeDecoder('text/jsonl'),
21
+ createNdjsonTypeDecoder('application/ndjson'),
22
+ createNdjsonTypeDecoder('application/x-ndjson'),
23
+ createUrlEncodedTypeDecoder(),
24
+ createYamlTypeDecoder(),
25
+ createYamlTypeDecoder('application/yaml'),
26
+ ];
14
27
  });
15
28
  export const decoderServiceFactory = createAbstractFactory((container, { resolveDependency }) => {
16
29
  return createDecoder(resolveDependency(container, 'typeDecoders', typeDecodersServiceFactory));
@@ -20,8 +33,14 @@ export const typeEncodersServiceFactory = createAbstractFactory((container) => {
20
33
  return [
21
34
  createJsonTypeEncoder(debug),
22
35
  createJsonxTypeEncoder(debug),
36
+ createNdjsonTypeEncoder(),
37
+ createNdjsonTypeEncoder('application/x-jsonl'),
38
+ createNdjsonTypeEncoder('text/jsonl'),
39
+ createNdjsonTypeEncoder('application/ndjson'),
40
+ createNdjsonTypeEncoder('application/x-ndjson'),
23
41
  createUrlEncodedTypeEncoder(),
24
42
  createYamlTypeEncoder(),
43
+ createYamlTypeEncoder('application/yaml'),
25
44
  ];
26
45
  });
27
46
  export const encoderServiceFactory = createAbstractFactory((container, { resolveDependency }) => {
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "@chubbyts/chubbyts-decode-encode",
3
3
  "type": "module",
4
- "version": "2.5.2",
5
- "description": "A simple decode/encode solution for json / jsonx / url-encoded / xml / 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
+ "content-type",
8
9
  "decode",
9
10
  "encode",
10
11
  "json",
11
12
  "jsonx",
13
+ "ndjson",
14
+ "serializer",
12
15
  "url-encoded",
13
16
  "xml",
14
17
  "yaml"
@@ -44,10 +47,15 @@
44
47
  "import": "./dist/encoder/index.js",
45
48
  "default": "./dist/encoder/index.js"
46
49
  },
50
+ "./dist/*": {
51
+ "types": "./dist/*.d.ts",
52
+ "import": "./dist/*.js",
53
+ "default": "./dist/*.js"
54
+ },
47
55
  "./*": {
48
- "types": "./*.d.ts",
49
- "import": "./*.js",
50
- "default": "./*.js"
56
+ "types": "./dist/*.d.ts",
57
+ "import": "./dist/*.js",
58
+ "default": "./dist/*.js"
51
59
  }
52
60
  },
53
61
  "engines": {