@e22m4u/js-trie-router 0.7.3 → 0.7.5

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
@@ -20,6 +20,7 @@ HTTP маршрутизатор для Node.js на основе
20
20
  - [Параметры маршрутизатора](#параметры-маршрутизатора)
21
21
  - [Контекст запроса](#контекст-запроса)
22
22
  - [Отправка ответа](#отправка-ответа)
23
+ - [Парсинг тела запроса](#парсинг-тела-запроса)
23
24
  - [Жизненный цикл](#жизненный-цикл)
24
25
  - [Хуки маршрута](#хуки-маршрута)
25
26
  - [Глобальные хуки](#глобальные-хуки)
@@ -219,6 +220,99 @@ router.defineRoute({
219
220
  });
220
221
  ```
221
222
 
223
+ ### Парсинг тела запроса
224
+
225
+ Для разбора тела входящего запроса отслеживается заголовок `Content-Type`,
226
+ определяющий формат передаваемых данных. По умолчанию маршрутизатор включает
227
+ парсеры для следующих форматов:
228
+
229
+ - `application/json` разбирается как *JSON*;
230
+ - `text/plain` преобразуется в строку;
231
+
232
+ Если входящий запрос содержит данные, но для его формата не найден подходящий
233
+ парсер, маршрутизатор прервет обработку запроса и вернет ошибку
234
+ *415 Unsupported Media Type*.
235
+
236
+ ```json
237
+ {
238
+ "error": {
239
+ "message": "Media type \"application/octet-stream\" is not supported."
240
+ }
241
+ }
242
+ ```
243
+
244
+ Чтобы избежать появления ошибки для форматов, которые предполагается
245
+ обрабатывать особым способом, предусмотрен параметр маршрутизатора
246
+ `ignoredMediaTypes` для игнорирования указанных медиа-типов. Параметр
247
+ позволяет пропустить встроенный парсинг, как это сделано в примере ниже.
248
+
249
+ ```js
250
+ import {TrieRouter, HttpMethod} from '@e22m4u/js-trie-router';
251
+
252
+ // создание экземпляра маршрутизатора
253
+ // с указанием исключаемых медиа-типов
254
+ const router = new TrieRouter({
255
+ ignoredMediaTypes: [
256
+ 'application/octet-stream',
257
+ 'multipart/form-data'
258
+ ],
259
+ });
260
+
261
+ // регистрация маршрута для обработки файлов
262
+ router.defineRoute({
263
+ method: HttpMethod.POST,
264
+ path: '/upload',
265
+ handler(ctx) {
266
+ // свойство body остается пустым, далее
267
+ // выполняется доступ к нативному потоку
268
+ const stream = ctx.request;
269
+ return 'OK';
270
+ }
271
+ });
272
+ ```
273
+
274
+ #### Регистрация пользовательского парсера
275
+
276
+ Для расширения поддерживаемых форматов предусмотрена возможность регистрации
277
+ пользовательской функции парсинга для определенного медиа-типа. Управление
278
+ такими функциями выполняется сервисом `RequestBodyParser`, который доступен
279
+ через глобальный контейнер маршрутизатора.
280
+
281
+ ```js
282
+ // доступ к сервису через маршрутизатор
283
+ const bodyParser = router.getService(RequestBodyParser);
284
+ // bodyParser.defineParser(mediaType, parserFn); см. далее
285
+ ```
286
+
287
+ Регистрируемая функция принимает извлеченные данные в виде строки и возвращает
288
+ преобразованный результат. Итоговое значение впоследствии будет доступно
289
+ в контексте обработки запроса.
290
+
291
+ ```js
292
+ import queryString from 'querystring';
293
+ import {TrieRouter, HttpMethod, RequestBodyParser} from '@e22m4u/js-trie-router';
294
+
295
+ const router = new TrieRouter();
296
+ const bodyParser = router.getService(RequestBodyParser);
297
+
298
+ // регистрация парсера для обработки данных формы
299
+ bodyParser.defineParser(
300
+ 'application/x-www-form-urlencoded',
301
+ (input) => queryString.parse(input),
302
+ );
303
+
304
+ // определение маршрута
305
+ router.defineRoute({
306
+ method: HttpMethod.POST,
307
+ path: '/submit',
308
+ handler(ctx) {
309
+ // свойство содержит результат
310
+ // работы новой парсер-функции
311
+ return ctx.body;
312
+ }
313
+ });
314
+ ```
315
+
222
316
  ### Жизненный цикл
223
317
 
224
318
  Для понимания того, как маршрутизатор обрабатывает входящий запрос, ниже
@@ -881,7 +881,7 @@ function patchHeaders(response) {
881
881
  );
882
882
  }
883
883
  const key = name.toLowerCase();
884
- this._headers[key] = String(value);
884
+ this._headers[key] = Array.isArray(value) ? [...value.map(String)] : String(value);
885
885
  return this;
886
886
  }, "value")
887
887
  });
@@ -2156,23 +2156,23 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
2156
2156
  * Set parser.
2157
2157
  *
2158
2158
  * @param {string} mediaType
2159
- * @param {Function} parser
2159
+ * @param {Function} parserFn
2160
2160
  * @returns {this}
2161
2161
  */
2162
- defineParser(mediaType, parser) {
2162
+ defineParser(mediaType, parserFn) {
2163
2163
  if (!mediaType || typeof mediaType !== "string") {
2164
2164
  throw new import_js_format19.InvalidArgumentError(
2165
2165
  'Parameter "mediaType" must be a non-empty String, but %v was given.',
2166
2166
  mediaType
2167
2167
  );
2168
2168
  }
2169
- if (!parser || typeof parser !== "function") {
2169
+ if (!parserFn || typeof parserFn !== "function") {
2170
2170
  throw new import_js_format19.InvalidArgumentError(
2171
- 'Parameter "parser" must be a Function, but %v was given.',
2172
- parser
2171
+ 'Parameter "parserFn" must be a Function, but %v was given.',
2172
+ parserFn
2173
2173
  );
2174
2174
  }
2175
- this._parsers[mediaType.toLowerCase()] = parser;
2175
+ this._parsers[mediaType.toLowerCase()] = parserFn;
2176
2176
  return this;
2177
2177
  }
2178
2178
  /**
@@ -2203,14 +2203,14 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
2203
2203
  mediaType
2204
2204
  );
2205
2205
  }
2206
- const parser = this._parsers[mediaType.toLowerCase()];
2207
- if (!parser) {
2206
+ const parserFn = this._parsers[mediaType.toLowerCase()];
2207
+ if (!parserFn) {
2208
2208
  throw new import_js_format19.InvalidArgumentError(
2209
2209
  "Media type %v does not have a parser.",
2210
2210
  mediaType
2211
2211
  );
2212
2212
  }
2213
- return parser;
2213
+ return parserFn;
2214
2214
  }
2215
2215
  /**
2216
2216
  * Remove parser.
@@ -2264,8 +2264,8 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
2264
2264
  debug("Media type %v is ignored.", mediaType);
2265
2265
  return;
2266
2266
  }
2267
- const parser = this._parsers[mediaTypeLc];
2268
- if (!parser) {
2267
+ const parserFn = this._parsers[mediaTypeLc];
2268
+ if (!parserFn) {
2269
2269
  throw createError(
2270
2270
  import_http_errors2.default.UnsupportedMediaType,
2271
2271
  "Media type %v is not supported.",
@@ -2278,7 +2278,7 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
2278
2278
  return fetchRequestBody(request, bodyBytesLimit).then((rawBody) => {
2279
2279
  if (rawBody != null) {
2280
2280
  debug("Read %v bytes.", Buffer.byteLength(rawBody, "utf8"));
2281
- return parser(rawBody);
2281
+ return parserFn(rawBody);
2282
2282
  }
2283
2283
  debug("Request body has no content.");
2284
2284
  return rawBody;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e22m4u/js-trie-router",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "HTTP маршрутизатор для Node.js на основе префиксного дерева",
5
5
  "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
6
6
  "license": "MIT",
@@ -15,9 +15,9 @@ export declare class RequestBodyParser extends DebuggableService {
15
15
  * Define parser.
16
16
  *
17
17
  * @param mediaType
18
- * @param parser
18
+ * @param parserFn
19
19
  */
20
- defineParser(mediaType: string, parser: BodyParserFunction): this;
20
+ defineParser(mediaType: string, parserFn: BodyParserFunction): this;
21
21
 
22
22
  /**
23
23
  * Has parser.
@@ -29,10 +29,10 @@ export class RequestBodyParser extends DebuggableService {
29
29
  * Set parser.
30
30
  *
31
31
  * @param {string} mediaType
32
- * @param {Function} parser
32
+ * @param {Function} parserFn
33
33
  * @returns {this}
34
34
  */
35
- defineParser(mediaType, parser) {
35
+ defineParser(mediaType, parserFn) {
36
36
  if (!mediaType || typeof mediaType !== 'string') {
37
37
  throw new InvalidArgumentError(
38
38
  'Parameter "mediaType" must be a non-empty String, ' +
@@ -40,13 +40,13 @@ export class RequestBodyParser extends DebuggableService {
40
40
  mediaType,
41
41
  );
42
42
  }
43
- if (!parser || typeof parser !== 'function') {
43
+ if (!parserFn || typeof parserFn !== 'function') {
44
44
  throw new InvalidArgumentError(
45
- 'Parameter "parser" must be a Function, but %v was given.',
46
- parser,
45
+ 'Parameter "parserFn" must be a Function, but %v was given.',
46
+ parserFn,
47
47
  );
48
48
  }
49
- this._parsers[mediaType.toLowerCase()] = parser;
49
+ this._parsers[mediaType.toLowerCase()] = parserFn;
50
50
  return this;
51
51
  }
52
52
 
@@ -81,14 +81,14 @@ export class RequestBodyParser extends DebuggableService {
81
81
  mediaType,
82
82
  );
83
83
  }
84
- const parser = this._parsers[mediaType.toLowerCase()];
85
- if (!parser) {
84
+ const parserFn = this._parsers[mediaType.toLowerCase()];
85
+ if (!parserFn) {
86
86
  throw new InvalidArgumentError(
87
87
  'Media type %v does not have a parser.',
88
88
  mediaType,
89
89
  );
90
90
  }
91
- return parser;
91
+ return parserFn;
92
92
  }
93
93
 
94
94
  /**
@@ -155,8 +155,8 @@ export class RequestBodyParser extends DebuggableService {
155
155
  }
156
156
  // если парсер для текущего медиа типа
157
157
  // не определен, то выбрасывается ошибка
158
- const parser = this._parsers[mediaTypeLc];
159
- if (!parser) {
158
+ const parserFn = this._parsers[mediaTypeLc];
159
+ if (!parserFn) {
160
160
  throw createError(
161
161
  HttpErrors.UnsupportedMediaType,
162
162
  'Media type %v is not supported.',
@@ -173,7 +173,7 @@ export class RequestBodyParser extends DebuggableService {
173
173
  return fetchRequestBody(request, bodyBytesLimit).then(rawBody => {
174
174
  if (rawBody != null) {
175
175
  debug('Read %v bytes.', Buffer.byteLength(rawBody, 'utf8'));
176
- return parser(rawBody);
176
+ return parserFn(rawBody);
177
177
  }
178
178
  debug('Request body has no content.');
179
179
  return rawBody;
@@ -30,11 +30,11 @@ describe('RequestBodyParser', function () {
30
30
  throwable('text/plain')();
31
31
  });
32
32
 
33
- it('should require the parameter "parser" to be a Function', function () {
33
+ it('should require the parameter "parserFn" to be a Function', function () {
34
34
  const S = new RequestBodyParser();
35
35
  const throwable = v => () => S.defineParser('str', v);
36
36
  const error = v =>
37
- format('Parameter "parser" must be a Function, but %s was given.', v);
37
+ format('Parameter "parserFn" must be a Function, but %s was given.', v);
38
38
  expect(throwable('str')).to.throw(error('"str"'));
39
39
  expect(throwable('')).to.throw(error('""'));
40
40
  expect(throwable(10)).to.throw(error('10'));
@@ -72,7 +72,9 @@ function patchHeaders(response) {
72
72
  );
73
73
  }
74
74
  const key = name.toLowerCase();
75
- this._headers[key] = String(value);
75
+ this._headers[key] = Array.isArray(value)
76
+ ? [...value.map(String)]
77
+ : String(value);
76
78
  return this;
77
79
  },
78
80
  });
@@ -79,6 +79,22 @@ describe('createResponseMock', function () {
79
79
  expect(ret).to.be.eq(res);
80
80
  expect(res._headers['num']).to.be.eq('10');
81
81
  });
82
+
83
+ it('should not stringify an array value', function () {
84
+ const res = createResponseMock();
85
+ expect(res._headers['key']).to.be.eq(undefined);
86
+ const ret = res.setHeader('key', ['foo', 'bar']);
87
+ expect(ret).to.be.eq(res);
88
+ expect(res._headers['key']).to.be.eql(['foo', 'bar']);
89
+ });
90
+
91
+ it('should stringify an array elements', function () {
92
+ const res = createResponseMock();
93
+ expect(res._headers['key']).to.be.eq(undefined);
94
+ const ret = res.setHeader('key', [1, 2]);
95
+ expect(ret).to.be.eq(res);
96
+ expect(res._headers['key']).to.be.eql(['1', '2']);
97
+ });
82
98
  });
83
99
 
84
100
  describe('getHeader', function () {