@e22m4u/js-trie-router 0.5.13 → 0.6.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.
@@ -65,7 +65,6 @@ __export(index_exports, {
65
65
  isResponseSent: () => isResponseSent,
66
66
  isWritableStream: () => isWritableStream,
67
67
  mergeDeep: () => mergeDeep,
68
- normalizePath: () => normalizePath,
69
68
  parseContentType: () => parseContentType,
70
69
  parseCookieString: () => parseCookieString,
71
70
  parseJsonBody: () => parseJsonBody,
@@ -207,16 +206,6 @@ function toPascalCase(input) {
207
206
  }
208
207
  __name(toPascalCase, "toPascalCase");
209
208
 
210
- // src/utils/normalize-path.js
211
- function normalizePath(value, noStartingSlash = false) {
212
- if (typeof value !== "string") {
213
- return "/";
214
- }
215
- const res = value.trim().replace(/\/+/g, "/").replace(/(^\/|\/$)/g, "");
216
- return noStartingSlash ? res : "/" + res;
217
- }
218
- __name(normalizePath, "normalizePath");
219
-
220
209
  // src/utils/is-response-sent.js
221
210
  var import_js_format3 = require("@e22m4u/js-format");
222
211
  function isResponseSent(response) {
@@ -1006,9 +995,15 @@ function validateRouteDefinition(routeDef) {
1006
995
  routeDef.method
1007
996
  );
1008
997
  }
1009
- if (!routeDef.path || typeof routeDef.path !== "string") {
998
+ if (typeof routeDef.path !== "string") {
999
+ throw new import_js_format12.InvalidArgumentError(
1000
+ 'Option "path" must be a String, but %v was given.',
1001
+ routeDef.path
1002
+ );
1003
+ }
1004
+ if (!routeDef.path.startsWith("/")) {
1010
1005
  throw new import_js_format12.InvalidArgumentError(
1011
- 'Option "path" must be a non-empty String, but %v was given.',
1006
+ 'Option "path" must start with "/", but %v was given.',
1012
1007
  routeDef.path
1013
1008
  );
1014
1009
  }
@@ -1508,7 +1503,7 @@ var _RouteRegistry = class _RouteRegistry extends DebuggableService {
1508
1503
  if (onDefineRouteHooks.length) {
1509
1504
  debug('Invoking %v "onDefineRoute" hook(s).', onDefineRouteHooks.length);
1510
1505
  for (const hook of onDefineRouteHooks) {
1511
- const hookResult = hook({ ...routeDef });
1506
+ const hookResult = hook({ ...routeDef }, this.container);
1512
1507
  if (hookResult !== void 0 && !(hookResult !== null && typeof hookResult === "object" && !Array.isArray(hookResult))) {
1513
1508
  throw new import_js_format16.InvalidArgumentError(
1514
1509
  'Hook "onDefineRoute" must return an Object or undefined, but %v was given.',
@@ -1774,16 +1769,22 @@ function validateRouterBranchDefinition(branchDef) {
1774
1769
  branchDef.method
1775
1770
  );
1776
1771
  }
1777
- if (!branchDef.path || typeof branchDef.path !== "string") {
1772
+ if (branchDef.handler !== void 0) {
1773
+ throw new import_js_format18.InvalidArgumentError(
1774
+ 'Option "handler" is not supported for the router branch, but %v was given.',
1775
+ branchDef.handler
1776
+ );
1777
+ }
1778
+ if (typeof branchDef.path !== "string") {
1778
1779
  throw new import_js_format18.InvalidArgumentError(
1779
- 'Option "path" must be a non-empty String, but %v was given.',
1780
+ 'Option "path" must be a String, but %v was given.',
1780
1781
  branchDef.path
1781
1782
  );
1782
1783
  }
1783
- if (branchDef.handler !== void 0) {
1784
+ if (!branchDef.path.startsWith("/")) {
1784
1785
  throw new import_js_format18.InvalidArgumentError(
1785
- 'Option "handler" is not supported for the router branch, but %v was given.',
1786
- branchDef.handler
1786
+ 'Option "path" must start with "/", but %v was given.',
1787
+ branchDef.path
1787
1788
  );
1788
1789
  }
1789
1790
  if (branchDef.preHandler !== void 0) {
@@ -1836,8 +1837,11 @@ function mergeRouterBranchDefinitions(firstDef, secondDef) {
1836
1837
  validateRouterBranchDefinition(firstDef);
1837
1838
  validateRouterBranchDefinition(secondDef);
1838
1839
  const mergedDef = {};
1839
- const path = (firstDef.path || "") + "/" + (secondDef.path || "");
1840
- mergedDef.path = normalizePath(path);
1840
+ let fullPath = "/" + (firstDef.path || "");
1841
+ if (secondDef.path && secondDef.path !== "/") {
1842
+ fullPath += "/" + secondDef.path;
1843
+ }
1844
+ mergedDef.path = fullPath.replace(/\/+/g, "/");
1841
1845
  if (firstDef.preHandler || secondDef.preHandler) {
1842
1846
  mergedDef.preHandler = [firstDef.preHandler, secondDef.preHandler].flat().filter(Boolean);
1843
1847
  }
@@ -1945,7 +1949,7 @@ var _RouterBranch = class _RouterBranch extends DebuggableService {
1945
1949
  validateRouterBranchDefinition(branchDef);
1946
1950
  this._definition = cloneDeep(branchDef);
1947
1951
  }
1948
- this.ctorDebug("Created a branch %v.", normalizePath(branchDef.path, true));
1952
+ this.ctorDebug("Created a branch %v.", branchDef.path);
1949
1953
  this.ctorDebug("Branch path is %v.", this._definition.path);
1950
1954
  }
1951
1955
  /**
@@ -2001,21 +2005,27 @@ var _DataSender = class _DataSender extends DebuggableService {
2001
2005
  return;
2002
2006
  }
2003
2007
  if (isReadableStream(data)) {
2004
- response.setHeader("Content-Type", "application/octet-stream");
2008
+ if (!response.getHeader("content-type")) {
2009
+ response.setHeader("content-type", "application/octet-stream");
2010
+ }
2005
2011
  data.pipe(response);
2006
2012
  debug("Sending response with a Stream.");
2007
2013
  return;
2008
2014
  }
2009
2015
  let debugMsg;
2010
2016
  switch (typeof data) {
2011
- case "object":
2012
- case "boolean":
2013
2017
  case "number":
2018
+ case "boolean":
2019
+ case "object":
2014
2020
  if (Buffer.isBuffer(data)) {
2015
- response.setHeader("content-type", "application/octet-stream");
2021
+ if (!response.getHeader("content-type")) {
2022
+ response.setHeader("content-type", "application/octet-stream");
2023
+ }
2016
2024
  debugMsg = "Buffer has been sent as binary data.";
2017
2025
  } else {
2018
- response.setHeader("content-type", "application/json");
2026
+ if (!response.getHeader("content-type")) {
2027
+ response.setHeader("content-type", "application/json");
2028
+ }
2019
2029
  debugMsg = (0, import_js_format20.format)(
2020
2030
  "%v has been sent as JSON.",
2021
2031
  toPascalCase(typeof data)
@@ -2024,7 +2034,9 @@ var _DataSender = class _DataSender extends DebuggableService {
2024
2034
  }
2025
2035
  break;
2026
2036
  default:
2027
- response.setHeader("content-type", "text/plain");
2037
+ if (!response.getHeader("content-type")) {
2038
+ response.setHeader("content-type", "text/plain");
2039
+ }
2028
2040
  debugMsg = "Response data has been sent as plain text.";
2029
2041
  data = String(data);
2030
2042
  break;
@@ -2130,7 +2142,7 @@ var _TrieRouter = class _TrieRouter extends DebuggableService {
2130
2142
  * ```
2131
2143
  * const router = new TrieRouter();
2132
2144
  * router.defineRoute({
2133
- * method: HttpMethod.GET, // Request method.
2145
+ * method: HttpMethod.GET, // Request method.
2134
2146
  * path: '/', // Path template.
2135
2147
  * handler: ctx => 'Hello world!', // Request handler.
2136
2148
  * });
@@ -2330,7 +2342,6 @@ var TrieRouter = _TrieRouter;
2330
2342
  isResponseSent,
2331
2343
  isWritableStream,
2332
2344
  mergeDeep,
2333
- normalizePath,
2334
2345
  parseContentType,
2335
2346
  parseCookieString,
2336
2347
  parseJsonBody,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e22m4u/js-trie-router",
3
- "version": "0.5.13",
3
+ "version": "0.6.0",
4
4
  "description": "HTTP маршрутизатор для Node.js на основе префиксного дерева",
5
5
  "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
6
6
  "license": "MIT",
@@ -39,8 +39,8 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@e22m4u/js-debug": "~0.4.1",
42
- "@e22m4u/js-format": "~0.3.2",
43
- "@e22m4u/js-path-trie": "~0.1.1",
42
+ "@e22m4u/js-format": "~0.4.0",
43
+ "@e22m4u/js-path-trie": "~0.2.0",
44
44
  "@e22m4u/js-service": "~0.5.1",
45
45
  "debug": "~4.4.3",
46
46
  "http-errors": "~2.0.1",
@@ -56,18 +56,18 @@
56
56
  "c8": "~10.1.3",
57
57
  "chai": "~6.2.2",
58
58
  "chai-as-promised": "~8.0.2",
59
- "esbuild": "~0.27.2",
59
+ "esbuild": "~0.27.3",
60
60
  "eslint": "~9.39.2",
61
61
  "eslint-config-prettier": "~10.1.8",
62
62
  "eslint-plugin-chai-expect": "~3.1.0",
63
63
  "eslint-plugin-import": "~2.32.0",
64
- "eslint-plugin-jsdoc": "~62.5.2",
64
+ "eslint-plugin-jsdoc": "~62.6.0",
65
65
  "eslint-plugin-mocha": "~11.2.0",
66
66
  "globals": "~17.3.0",
67
67
  "husky": "~9.1.7",
68
68
  "mocha": "~11.7.5",
69
69
  "prettier": "~3.8.1",
70
- "rimraf": "~6.1.2",
70
+ "rimraf": "~6.1.3",
71
71
  "typescript": "~5.9.3"
72
72
  }
73
73
  }
@@ -3,7 +3,7 @@ import {ROOT_PATH} from '../constants.js';
3
3
  import {mergeRouterBranchDefinitions} from './merge-router-branch-definitions.js';
4
4
 
5
5
  describe('mergeRouterBranchDefinitions', function () {
6
- it('should validate the "firstDef" parameter', function () {
6
+ it('should require the "firstDef" parameter to be an Object', function () {
7
7
  const throwable = () =>
8
8
  mergeRouterBranchDefinitions(123, {path: ROOT_PATH});
9
9
  expect(throwable).to.throw(
@@ -11,7 +11,7 @@ describe('mergeRouterBranchDefinitions', function () {
11
11
  );
12
12
  });
13
13
 
14
- it('should validate the "secondDef" parameter', function () {
14
+ it('should require the "secondDef" parameter to be an Object', function () {
15
15
  const throwable = () =>
16
16
  mergeRouterBranchDefinitions({path: ROOT_PATH}, 123);
17
17
  expect(throwable).to.throw(
@@ -20,11 +20,25 @@ describe('mergeRouterBranchDefinitions', function () {
20
20
  });
21
21
 
22
22
  it('should concatenate the "path" option with the correct order', function () {
23
- const res = mergeRouterBranchDefinitions({path: 'foo'}, {path: 'bar'});
23
+ const res = mergeRouterBranchDefinitions({path: '/foo'}, {path: '/bar'});
24
24
  expect(res).to.be.eql({path: '/foo/bar'});
25
25
  });
26
26
 
27
- it('should not duplicate slashes in the "path" option', function () {
27
+ it('should keep a trailing slash from the first definition', function () {
28
+ const res1 = mergeRouterBranchDefinitions({path: '/foo/'}, {path: '/'});
29
+ expect(res1).to.be.eql({path: '/foo/'});
30
+ const res2 = mergeRouterBranchDefinitions({path: '/foo/'}, {path: '/bar'});
31
+ expect(res2).to.be.eql({path: '/foo/bar'});
32
+ });
33
+
34
+ it('should keep a trailing slash from the second definition', function () {
35
+ const res1 = mergeRouterBranchDefinitions({path: '/'}, {path: '/foo/'});
36
+ expect(res1).to.be.eql({path: '/foo/'});
37
+ const res2 = mergeRouterBranchDefinitions({path: '/foo'}, {path: '/bar/'});
38
+ expect(res2).to.be.eql({path: '/foo/bar/'});
39
+ });
40
+
41
+ it('should not duplicate slashes from the "path" option', function () {
28
42
  const res = mergeRouterBranchDefinitions({path: '/'}, {path: '/'});
29
43
  expect(res).to.be.eql({path: '/'});
30
44
  });
@@ -1,4 +1,4 @@
1
- import {mergeDeep, normalizePath} from '../utils/index.js';
1
+ import {mergeDeep} from '../utils/index.js';
2
2
  import {validateRouterBranchDefinition} from './validate-router-branch-definition.js';
3
3
 
4
4
  /**
@@ -13,8 +13,11 @@ export function mergeRouterBranchDefinitions(firstDef, secondDef) {
13
13
  validateRouterBranchDefinition(secondDef);
14
14
  const mergedDef = {};
15
15
  // path
16
- const path = (firstDef.path || '') + '/' + (secondDef.path || '');
17
- mergedDef.path = normalizePath(path);
16
+ let fullPath = '/' + (firstDef.path || '');
17
+ if (secondDef.path && secondDef.path !== '/') {
18
+ fullPath += '/' + secondDef.path;
19
+ }
20
+ mergedDef.path = fullPath.replace(/\/+/g, '/');
18
21
  // pre-handler
19
22
  if (firstDef.preHandler || secondDef.preHandler) {
20
23
  mergedDef.preHandler = [firstDef.preHandler, secondDef.preHandler]
@@ -2,7 +2,6 @@ import {Route} from '../route/index.js';
2
2
  import {cloneDeep} from '../utils/index.js';
3
3
  import {TrieRouter} from '../trie-router.js';
4
4
  import {InvalidArgumentError} from '@e22m4u/js-format';
5
- import {normalizePath} from '../utils/normalize-path.js';
6
5
  import {DebuggableService} from '../debuggable-service.js';
7
6
  import {validateRouteDefinition} from '../route/validate-route-definition.js';
8
7
  import {mergeRouterBranchDefinitions} from './merge-router-branch-definitions.js';
@@ -120,7 +119,7 @@ export class RouterBranch extends DebuggableService {
120
119
  validateRouterBranchDefinition(branchDef);
121
120
  this._definition = cloneDeep(branchDef);
122
121
  }
123
- this.ctorDebug('Created a branch %v.', normalizePath(branchDef.path, true));
122
+ this.ctorDebug('Created a branch %v.', branchDef.path);
124
123
  this.ctorDebug('Branch path is %v.', this._definition.path);
125
124
  }
126
125
 
@@ -14,8 +14,8 @@ describe('RouterBranch', function () {
14
14
 
15
15
  it('should merge a parent definition with a given definition', function () {
16
16
  const router = new TrieRouter();
17
- const parent = router.createBranch({path: 'foo'});
18
- const S = new RouterBranch(router, {path: 'bar'}, parent);
17
+ const parent = router.createBranch({path: '/foo'});
18
+ const S = new RouterBranch(router, {path: '/bar'}, parent);
19
19
  expect(S.getDefinition().path).to.be.eq('/foo/bar');
20
20
  });
21
21
  });
@@ -70,10 +70,10 @@ describe('RouterBranch', function () {
70
70
  describe('defineRoute', function () {
71
71
  it('should return a Route instance', function () {
72
72
  const router = new TrieRouter();
73
- const S = new RouterBranch(router, {path: 'foo'});
73
+ const S = new RouterBranch(router, {path: '/foo'});
74
74
  const res = S.defineRoute({
75
75
  method: HttpMethod.GET,
76
- path: 'bar',
76
+ path: '/bar',
77
77
  handler: () => undefined,
78
78
  });
79
79
  expect(res).to.be.instanceOf(Route);
@@ -81,10 +81,10 @@ describe('RouterBranch', function () {
81
81
 
82
82
  it('should combine a branch path with a route path', function () {
83
83
  const router = new TrieRouter();
84
- const S = new RouterBranch(router, {path: 'foo'});
84
+ const S = new RouterBranch(router, {path: '/foo'});
85
85
  const res = S.defineRoute({
86
86
  method: HttpMethod.GET,
87
- path: 'bar',
87
+ path: '/bar',
88
88
  handler: () => undefined,
89
89
  });
90
90
  expect(res.path).to.be.eq('/foo/bar');
@@ -94,15 +94,15 @@ describe('RouterBranch', function () {
94
94
  describe('createBranch', function () {
95
95
  it('should return a RouterBranch instance', function () {
96
96
  const router = new TrieRouter();
97
- const S = new RouterBranch(router, {path: 'foo'});
98
- const res = S.createBranch({path: 'bar'});
97
+ const S = new RouterBranch(router, {path: '/foo'});
98
+ const res = S.createBranch({path: '/bar'});
99
99
  expect(res).to.be.instanceOf(RouterBranch);
100
100
  });
101
101
 
102
102
  it('should combine a current path with a new path', function () {
103
103
  const router = new TrieRouter();
104
- const S = new RouterBranch(router, {path: 'foo'});
105
- const res = S.createBranch({path: 'bar'});
104
+ const S = new RouterBranch(router, {path: '/foo'});
105
+ const res = S.createBranch({path: '/bar'});
106
106
  expect(res.getDefinition().path).to.be.eq('/foo/bar');
107
107
  });
108
108
  });
@@ -19,12 +19,6 @@ export function validateRouterBranchDefinition(branchDef) {
19
19
  branchDef.method,
20
20
  );
21
21
  }
22
- if (!branchDef.path || typeof branchDef.path !== 'string') {
23
- throw new InvalidArgumentError(
24
- 'Option "path" must be a non-empty String, but %v was given.',
25
- branchDef.path,
26
- );
27
- }
28
22
  if (branchDef.handler !== undefined) {
29
23
  throw new InvalidArgumentError(
30
24
  'Option "handler" is not supported for the router branch, ' +
@@ -32,6 +26,18 @@ export function validateRouterBranchDefinition(branchDef) {
32
26
  branchDef.handler,
33
27
  );
34
28
  }
29
+ if (typeof branchDef.path !== 'string') {
30
+ throw new InvalidArgumentError(
31
+ 'Option "path" must be a String, but %v was given.',
32
+ branchDef.path,
33
+ );
34
+ }
35
+ if (!branchDef.path.startsWith('/')) {
36
+ throw new InvalidArgumentError(
37
+ 'Option "path" must start with "/", but %v was given.',
38
+ branchDef.path,
39
+ );
40
+ }
35
41
  if (branchDef.preHandler !== undefined) {
36
42
  if (Array.isArray(branchDef.preHandler)) {
37
43
  branchDef.preHandler.forEach(preHandler => {
@@ -1,7 +1,7 @@
1
1
  import {expect} from 'chai';
2
2
  import {format} from '@e22m4u/js-format';
3
- import {validateRouterBranchDefinition} from './validate-router-branch-definition.js';
4
3
  import {ROOT_PATH} from '../constants.js';
4
+ import {validateRouterBranchDefinition} from './validate-router-branch-definition.js';
5
5
 
6
6
  describe('validateRouterBranchDefinition', function () {
7
7
  it('should require the "routeDef" parameter to be an Object', function () {
@@ -33,11 +33,10 @@ describe('validateRouterBranchDefinition', function () {
33
33
  );
34
34
  });
35
35
 
36
- it('should require the "path" option to be a non-empty String', function () {
36
+ it('should require the "path" option to be a String', function () {
37
37
  const throwable = v => () => validateRouterBranchDefinition({path: v});
38
38
  const error = v =>
39
- format('Option "path" must be a non-empty String, but %s was given.', v);
40
- expect(throwable('')).to.throw(error('""'));
39
+ format('Option "path" must be a String, but %s was given.', v);
41
40
  expect(throwable(10)).to.throw(error('10'));
42
41
  expect(throwable(0)).to.throw(error('0'));
43
42
  expect(throwable(true)).to.throw(error('true'));
@@ -47,7 +46,17 @@ describe('validateRouterBranchDefinition', function () {
47
46
  expect(throwable(undefined)).to.throw(error('undefined'));
48
47
  expect(throwable(null)).to.throw(error('null'));
49
48
  expect(throwable(() => undefined)).to.throw(error('Function'));
50
- throwable('str')();
49
+ throwable('/path')();
50
+ });
51
+
52
+ it('should require the "path" option to start with a forward slash', function () {
53
+ const throwable = v => () => validateRouterBranchDefinition({path: v});
54
+ const error = s =>
55
+ format('Option "path" must start with "/", but %s was given.', s);
56
+ expect(throwable('path')).to.throw(error('"path"'));
57
+ expect(throwable('')).to.throw(error('""'));
58
+ throwable('/path')();
59
+ throwable('/')();
51
60
  });
52
61
 
53
62
  it('should throw an error if the "handler" option is provided', function () {
@@ -62,7 +71,7 @@ describe('validateRouterBranchDefinition', function () {
62
71
  );
63
72
  });
64
73
 
65
- it('should require the "preHandler" option to be a Function or an Array of Function', function () {
74
+ it('should require the "preHandler" option to be a Function or an Array', function () {
66
75
  const throwable = v => () =>
67
76
  validateRouterBranchDefinition({
68
77
  path: ROOT_PATH,
@@ -108,7 +117,7 @@ describe('validateRouterBranchDefinition', function () {
108
117
  throwable(() => undefined)();
109
118
  });
110
119
 
111
- it('should require the "postHandler" option to be a Function or an Array of Function', function () {
120
+ it('should require the "postHandler" option to be a Function or an Array', function () {
112
121
  const throwable = v => () =>
113
122
  validateRouterBranchDefinition({
114
123
  path: ROOT_PATH,
@@ -1,5 +1,6 @@
1
1
  import {Callable} from '../types.js';
2
2
  import {RouteDefinition} from '../route/index.js';
3
+ import {ServiceContainer} from '@e22m4u/js-service';
3
4
  import {RequestContext} from '../request-context.js';
4
5
  import {DebuggableService} from '../debuggable-service.js';
5
6
 
@@ -43,6 +44,7 @@ export type PostHandlerHook = (ctx: RequestContext, data: unknown) => unknown;
43
44
  */
44
45
  export type OnDefineRouteHook = (
45
46
  routeDef: RouteDefinition,
47
+ container: ServiceContainer,
46
48
  ) => RouteDefinition | undefined;
47
49
 
48
50
  /**
@@ -63,11 +63,7 @@ describe('Route', function () {
63
63
  handler: () => 'Ok',
64
64
  });
65
65
  const error = v =>
66
- format(
67
- 'Option "path" must be a non-empty String, but %s was given.',
68
- v,
69
- );
70
- expect(throwable('')).to.throw(error('""'));
66
+ format('Option "path" must be a String, but %s was given.', v);
71
67
  expect(throwable(10)).to.throw(error('10'));
72
68
  expect(throwable(0)).to.throw(error('0'));
73
69
  expect(throwable(true)).to.throw(error('true'));
@@ -77,7 +73,22 @@ describe('Route', function () {
77
73
  expect(throwable(undefined)).to.throw(error('undefined'));
78
74
  expect(throwable(null)).to.throw(error('null'));
79
75
  expect(throwable(() => undefined)).to.throw(error('Function'));
80
- throwable('str')();
76
+ throwable('/path')();
77
+ });
78
+
79
+ it('should require the "path" option to start with a forward slash', function () {
80
+ const throwable = v => () =>
81
+ new Route({
82
+ method: HttpMethod.GET,
83
+ path: v,
84
+ handler: () => 'Ok',
85
+ });
86
+ const error = s =>
87
+ format('Option "path" must start with "/", but %s was given.', s);
88
+ expect(throwable('path')).to.throw(error('"path"'));
89
+ expect(throwable('')).to.throw(error('""'));
90
+ throwable('/path')();
91
+ throwable('/')();
81
92
  });
82
93
 
83
94
  it('should require the "handler" option to be a Function', function () {
@@ -351,7 +362,7 @@ describe('Route', function () {
351
362
 
352
363
  describe('path', function () {
353
364
  it('should return a value of the "path" option', function () {
354
- const value = 'myPath';
365
+ const value = '/myPath';
355
366
  const route = new Route({
356
367
  method: HttpMethod.GET,
357
368
  path: value,
@@ -18,9 +18,15 @@ export function validateRouteDefinition(routeDef) {
18
18
  routeDef.method,
19
19
  );
20
20
  }
21
- if (!routeDef.path || typeof routeDef.path !== 'string') {
21
+ if (typeof routeDef.path !== 'string') {
22
22
  throw new InvalidArgumentError(
23
- 'Option "path" must be a non-empty String, but %v was given.',
23
+ 'Option "path" must be a String, but %v was given.',
24
+ routeDef.path,
25
+ );
26
+ }
27
+ if (!routeDef.path.startsWith('/')) {
28
+ throw new InvalidArgumentError(
29
+ 'Option "path" must start with "/", but %v was given.',
24
30
  routeDef.path,
25
31
  );
26
32
  }
@@ -51,7 +51,7 @@ describe('validateRouteDefinition', function () {
51
51
  throwable(HttpMethod.GET)();
52
52
  });
53
53
 
54
- it('should require the "path" option to be a non-empty String', function () {
54
+ it('should require the "path" option to be a String', function () {
55
55
  const throwable = v => () =>
56
56
  validateRouteDefinition({
57
57
  method: HttpMethod.GET,
@@ -59,8 +59,7 @@ describe('validateRouteDefinition', function () {
59
59
  handler: () => undefined,
60
60
  });
61
61
  const error = v =>
62
- format('Option "path" must be a non-empty String, but %s was given.', v);
63
- expect(throwable('')).to.throw(error('""'));
62
+ format('Option "path" must be a String, but %s was given.', v);
64
63
  expect(throwable(10)).to.throw(error('10'));
65
64
  expect(throwable(0)).to.throw(error('0'));
66
65
  expect(throwable(true)).to.throw(error('true'));
@@ -70,7 +69,22 @@ describe('validateRouteDefinition', function () {
70
69
  expect(throwable(undefined)).to.throw(error('undefined'));
71
70
  expect(throwable(null)).to.throw(error('null'));
72
71
  expect(throwable(() => undefined)).to.throw(error('Function'));
73
- throwable('str')();
72
+ throwable('/path')();
73
+ });
74
+
75
+ it('should require the "path" option to start with a forward slash', function () {
76
+ const throwable = v => () =>
77
+ validateRouteDefinition({
78
+ method: HttpMethod.GET,
79
+ path: v,
80
+ handler: () => undefined,
81
+ });
82
+ const error = s =>
83
+ format('Option "path" must start with "/", but %s was given.', s);
84
+ expect(throwable('path')).to.throw(error('"path"'));
85
+ expect(throwable('')).to.throw(error('""'));
86
+ throwable('/path')();
87
+ throwable('/')();
74
88
  });
75
89
 
76
90
  it('should require the "handler" option to be a Function', function () {
@@ -50,7 +50,7 @@ export class RouteRegistry extends DebuggableService {
50
50
  if (onDefineRouteHooks.length) {
51
51
  debug('Invoking %v "onDefineRoute" hook(s).', onDefineRouteHooks.length);
52
52
  for (const hook of onDefineRouteHooks) {
53
- const hookResult = hook({...routeDef});
53
+ const hookResult = hook({...routeDef}, this.container);
54
54
  // если возвращаемое значение хука не является
55
55
  // объектом и undefined, то выбрасывается ошибка
56
56
  if (
@@ -52,7 +52,8 @@ describe('RouteRegistry', function () {
52
52
  expect(res.value).to.be.eq(route);
53
53
  });
54
54
 
55
- it('should invoke "onDefineRoute" hooks in correct order', function () {
55
+ it('should invoke "onDefineRoute" hooks with arguments and correct order', function () {
56
+ const S = new RouteRegistry();
56
57
  const routeDef = {
57
58
  method: HttpMethod.GET,
58
59
  path: '/myPath',
@@ -61,13 +62,14 @@ describe('RouteRegistry', function () {
61
62
  const order = [];
62
63
  const onDefineRouteHook1 = (...args) => {
63
64
  order.push(1);
64
- expect(args).to.be.eql([routeDef]);
65
+ expect(args[0]).to.be.eql(routeDef);
66
+ expect(args[1]).to.be.eq(S.container);
65
67
  };
66
68
  const onDefineRouteHook2 = (...args) => {
67
69
  order.push(2);
68
- expect(args).to.be.eql([routeDef]);
70
+ expect(args[0]).to.be.eql(routeDef);
71
+ expect(args[1]).to.be.eq(S.container);
69
72
  };
70
- const S = new RouteRegistry();
71
73
  const hooksRegistry = S.getService(RouterHookRegistry);
72
74
  hooksRegistry.addHook(RouterHookType.ON_DEFINE_ROUTE, onDefineRouteHook1);
73
75
  hooksRegistry.addHook(RouterHookType.ON_DEFINE_ROUTE, onDefineRouteHook2);
@@ -76,6 +78,7 @@ describe('RouteRegistry', function () {
76
78
  });
77
79
 
78
80
  it('should allow override the route definition by "onDefineRoute" hooks', function () {
81
+ const S = new RouteRegistry();
79
82
  const routeDef = {
80
83
  method: HttpMethod.GET,
81
84
  path: '/myPath',
@@ -90,7 +93,6 @@ describe('RouteRegistry', function () {
90
93
  order.push(2);
91
94
  return {...def, path: def.path + '/2'};
92
95
  };
93
- const S = new RouteRegistry();
94
96
  const hooksRegistry = S.getService(RouterHookRegistry);
95
97
  hooksRegistry.addHook(RouterHookType.ON_DEFINE_ROUTE, onDefineRouteHook1);
96
98
  hooksRegistry.addHook(RouterHookType.ON_DEFINE_ROUTE, onDefineRouteHook2);
@@ -15,15 +15,14 @@ export class DataSender extends DebuggableService {
15
15
  */
16
16
  send(response, data) {
17
17
  const debug = this.getDebuggerFor(this.send);
18
- // если ответ контроллера является объектом
19
- // ServerResponse, или имеются отправленные
20
- // заголовки, то считаем, что контроллер
21
- // уже отправил ответ самостоятельно
18
+ // если ответ контроллера является объектом ServerResponse,
19
+ // или имеются отправленные заголовки, то предполагается,
20
+ // что контроллер уже отправил ответ самостоятельно
22
21
  if (data === response || response.headersSent) {
23
22
  debug('Skipping response because headers have already been sent.');
24
23
  return;
25
24
  }
26
- // если ответ контроллера пуст, то отправляем
25
+ // если ответ контроллера пуст, то отправляется
27
26
  // статус 204 "No Content"
28
27
  if (data == null) {
29
28
  response.statusCode = 204;
@@ -32,9 +31,13 @@ export class DataSender extends DebuggableService {
32
31
  return;
33
32
  }
34
33
  // если ответ контроллера является стримом,
35
- // то отправляем его как бинарные данные
34
+ // то поток отправляет бинарные данные
36
35
  if (isReadableStream(data)) {
37
- response.setHeader('Content-Type', 'application/octet-stream');
36
+ // если заголовок "content-type" не определен ранее,
37
+ // то устанавливается заголовок потоковых данных
38
+ if (!response.getHeader('content-type')) {
39
+ response.setHeader('content-type', 'application/octet-stream');
40
+ }
38
41
  data.pipe(response);
39
42
  debug('Sending response with a Stream.');
40
43
  return;
@@ -43,16 +46,25 @@ export class DataSender extends DebuggableService {
43
46
  // нужного заголовка в зависимости от их типа
44
47
  let debugMsg;
45
48
  switch (typeof data) {
46
- case 'object':
47
- case 'boolean':
48
49
  case 'number':
50
+ case 'boolean':
51
+ case 'object':
52
+ // для бинарных данных предусмотрен специальный "content-type",
53
+ // который устанавливается автоматически, если не был определен
54
+ // ранее (к примеру, в обработчике маршрута)
49
55
  if (Buffer.isBuffer(data)) {
50
- // тип Buffer отправляется
51
- // как бинарные данные
52
- response.setHeader('content-type', 'application/octet-stream');
56
+ if (!response.getHeader('content-type')) {
57
+ response.setHeader('content-type', 'application/octet-stream');
58
+ }
53
59
  debugMsg = 'Buffer has been sent as binary data.';
54
- } else {
55
- response.setHeader('content-type', 'application/json');
60
+ }
61
+ // объекты, массивы, числа и логические значения
62
+ // отправляются в виде JSON строки, с соответствующим
63
+ // заголовком "content-type" (если не был определен)
64
+ else {
65
+ if (!response.getHeader('content-type')) {
66
+ response.setHeader('content-type', 'application/json');
67
+ }
56
68
  debugMsg = format(
57
69
  '%v has been sent as JSON.',
58
70
  toPascalCase(typeof data),
@@ -61,7 +73,9 @@ export class DataSender extends DebuggableService {
61
73
  }
62
74
  break;
63
75
  default:
64
- response.setHeader('content-type', 'text/plain');
76
+ if (!response.getHeader('content-type')) {
77
+ response.setHeader('content-type', 'text/plain');
78
+ }
65
79
  debugMsg = 'Response data has been sent as plain text.';
66
80
  data = String(data);
67
81
  break;
@@ -5,7 +5,7 @@ import {createResponseMock} from '../utils/index.js';
5
5
 
6
6
  describe('DataSender', function () {
7
7
  describe('send', function () {
8
- it('does nothing if the data is the given response', function (done) {
8
+ it('should not send response when the data is the server response', function (done) {
9
9
  const res = createResponseMock();
10
10
  const writable = new Writable();
11
11
  writable._write = function () {
@@ -21,7 +21,7 @@ describe('DataSender', function () {
21
21
  setTimeout(() => done(), 5);
22
22
  });
23
23
 
24
- it('does nothing if response headers already sent', function (done) {
24
+ it('should not send response when response headers already sent', function (done) {
25
25
  const res = createResponseMock();
26
26
  res._headersSent = true;
27
27
  const writable = new Writable();
@@ -38,7 +38,7 @@ describe('DataSender', function () {
38
38
  setTimeout(() => done(), 5);
39
39
  });
40
40
 
41
- it('sends 204 if no data', function (done) {
41
+ it('should send 204 status code when the data is undefined', function (done) {
42
42
  const res = createResponseMock();
43
43
  res.on('data', () => done(new Error('Should not be called')));
44
44
  res.on('error', e => done(e));
@@ -51,7 +51,20 @@ describe('DataSender', function () {
51
51
  expect(result).to.be.undefined;
52
52
  });
53
53
 
54
- it('sends the given readable stream as binary data', function (done) {
54
+ it('should send 204 status code when the data is null', function (done) {
55
+ const res = createResponseMock();
56
+ res.on('data', () => done(new Error('Should not be called')));
57
+ res.on('error', e => done(e));
58
+ res.on('end', () => {
59
+ expect(res.statusCode).to.be.eq(204);
60
+ done();
61
+ });
62
+ const S = new DataSender();
63
+ const result = S.send(res, null);
64
+ expect(result).to.be.undefined;
65
+ });
66
+
67
+ it('should send the readable stream as a binary data', function (done) {
55
68
  const data = 'text';
56
69
  const stream = new Readable();
57
70
  stream._read = () => {};
@@ -77,9 +90,15 @@ describe('DataSender', function () {
77
90
  S.send(res, stream);
78
91
  });
79
92
 
80
- it('sends the given Buffer as binary data', function (done) {
81
- const data = Buffer.from('text');
93
+ it('should allow override the "content-type" header for a readable stream', function (done) {
94
+ const data = 'text';
95
+ const stream = new Readable();
96
+ stream._read = () => {};
97
+ stream.push(data);
98
+ stream.push(null);
82
99
  const res = createResponseMock();
100
+ const contentType = 'custom/type';
101
+ res.setHeader('content-type', contentType);
83
102
  const writable = new Writable();
84
103
  const chunks = [];
85
104
  writable._write = function (chunk, encoding, done) {
@@ -87,10 +106,33 @@ describe('DataSender', function () {
87
106
  done();
88
107
  };
89
108
  writable._final = function (callback) {
90
- const sentData = Buffer.concat(chunks);
91
- expect(sentData).to.be.eql(sentData);
109
+ const sentData = Buffer.concat(chunks).toString('utf-8');
110
+ expect(sentData).to.be.eq(data);
92
111
  const ct = res.getHeader('content-type');
93
- expect(ct).to.be.eq('application/octet-stream');
112
+ expect(ct).to.be.eq(contentType);
113
+ callback();
114
+ done();
115
+ };
116
+ res.pipe(writable);
117
+ const S = new DataSender();
118
+ S.send(res, stream);
119
+ });
120
+
121
+ it('should send the number value as a JSON string', function (done) {
122
+ const data = 10;
123
+ const res = createResponseMock();
124
+ const writable = new Writable();
125
+ const chunks = [];
126
+ writable._write = function (chunk, encoding, done) {
127
+ chunks.push(chunk);
128
+ done();
129
+ };
130
+ writable._final = function (callback) {
131
+ const sentJson = Buffer.concat(chunks).toString('utf-8');
132
+ const sentData = JSON.parse(sentJson);
133
+ expect(sentData).to.be.eql(data);
134
+ const ct = res.getHeader('content-type');
135
+ expect(ct).to.be.eq('application/json');
94
136
  callback();
95
137
  done();
96
138
  };
@@ -99,9 +141,11 @@ describe('DataSender', function () {
99
141
  S.send(res, data);
100
142
  });
101
143
 
102
- it('sends the given string as plain text', function (done) {
103
- const data = 'text';
144
+ it('should allow override the "content-type" header for a number value', function (done) {
145
+ const data = 10;
104
146
  const res = createResponseMock();
147
+ const contentType = 'custom/type';
148
+ res.setHeader('content-type', contentType);
105
149
  const writable = new Writable();
106
150
  const chunks = [];
107
151
  writable._write = function (chunk, encoding, done) {
@@ -109,10 +153,11 @@ describe('DataSender', function () {
109
153
  done();
110
154
  };
111
155
  writable._final = function (callback) {
112
- const sentData = Buffer.concat(chunks).toString('utf-8');
113
- expect(sentData).to.be.eq(data);
156
+ const sentJson = Buffer.concat(chunks).toString('utf-8');
157
+ const sentData = JSON.parse(sentJson);
158
+ expect(sentData).to.be.eql(data);
114
159
  const ct = res.getHeader('content-type');
115
- expect(ct).to.be.eq('text/plain');
160
+ expect(ct).to.be.eq(contentType);
116
161
  callback();
117
162
  done();
118
163
  };
@@ -121,8 +166,8 @@ describe('DataSender', function () {
121
166
  S.send(res, data);
122
167
  });
123
168
 
124
- it('sends the given object as JSON', function (done) {
125
- const data = {foo: 'bar'};
169
+ it('should send the boolean value as a JSON string', function (done) {
170
+ const data = true;
126
171
  const res = createResponseMock();
127
172
  const writable = new Writable();
128
173
  const chunks = [];
@@ -144,9 +189,11 @@ describe('DataSender', function () {
144
189
  S.send(res, data);
145
190
  });
146
191
 
147
- it('sends the given boolean as JSON', function (done) {
192
+ it('should allow override the "content-type" header for a boolean value', function (done) {
148
193
  const data = true;
149
194
  const res = createResponseMock();
195
+ const contentType = 'custom/type';
196
+ res.setHeader('content-type', contentType);
150
197
  const writable = new Writable();
151
198
  const chunks = [];
152
199
  writable._write = function (chunk, encoding, done) {
@@ -158,7 +205,7 @@ describe('DataSender', function () {
158
205
  const sentData = JSON.parse(sentJson);
159
206
  expect(sentData).to.be.eql(data);
160
207
  const ct = res.getHeader('content-type');
161
- expect(ct).to.be.eq('application/json');
208
+ expect(ct).to.be.eq(contentType);
162
209
  callback();
163
210
  done();
164
211
  };
@@ -167,8 +214,54 @@ describe('DataSender', function () {
167
214
  S.send(res, data);
168
215
  });
169
216
 
170
- it('sends the given number as JSON', function (done) {
171
- const data = 10;
217
+ it('should send the Buffer as a binary data', function (done) {
218
+ const data = Buffer.from('text');
219
+ const res = createResponseMock();
220
+ const writable = new Writable();
221
+ const chunks = [];
222
+ writable._write = function (chunk, encoding, done) {
223
+ chunks.push(chunk);
224
+ done();
225
+ };
226
+ writable._final = function (callback) {
227
+ const sentData = Buffer.concat(chunks);
228
+ expect(sentData).to.be.eql(sentData);
229
+ const ct = res.getHeader('content-type');
230
+ expect(ct).to.be.eq('application/octet-stream');
231
+ callback();
232
+ done();
233
+ };
234
+ res.pipe(writable);
235
+ const S = new DataSender();
236
+ S.send(res, data);
237
+ });
238
+
239
+ it('should allow override the "content-type" header for a Buffer', function (done) {
240
+ const data = Buffer.from('text');
241
+ const res = createResponseMock();
242
+ const contentType = 'custom/type';
243
+ res.setHeader('content-type', contentType);
244
+ const writable = new Writable();
245
+ const chunks = [];
246
+ writable._write = function (chunk, encoding, done) {
247
+ chunks.push(chunk);
248
+ done();
249
+ };
250
+ writable._final = function (callback) {
251
+ const sentData = Buffer.concat(chunks);
252
+ expect(sentData).to.be.eql(sentData);
253
+ const ct = res.getHeader('content-type');
254
+ expect(ct).to.be.eq(contentType);
255
+ callback();
256
+ done();
257
+ };
258
+ res.pipe(writable);
259
+ const S = new DataSender();
260
+ S.send(res, data);
261
+ });
262
+
263
+ it('should send the object value as a JSON string', function (done) {
264
+ const data = {foo: 'bar'};
172
265
  const res = createResponseMock();
173
266
  const writable = new Writable();
174
267
  const chunks = [];
@@ -189,5 +282,76 @@ describe('DataSender', function () {
189
282
  const S = new DataSender();
190
283
  S.send(res, data);
191
284
  });
285
+
286
+ it('should allow override the "content-type" header for an object value', function (done) {
287
+ const data = {foo: 'bar'};
288
+ const res = createResponseMock();
289
+ const contentType = 'custom/type';
290
+ res.setHeader('content-type', contentType);
291
+ const writable = new Writable();
292
+ const chunks = [];
293
+ writable._write = function (chunk, encoding, done) {
294
+ chunks.push(chunk);
295
+ done();
296
+ };
297
+ writable._final = function (callback) {
298
+ const sentJson = Buffer.concat(chunks).toString('utf-8');
299
+ const sentData = JSON.parse(sentJson);
300
+ expect(sentData).to.be.eql(data);
301
+ const ct = res.getHeader('content-type');
302
+ expect(ct).to.be.eq(contentType);
303
+ callback();
304
+ done();
305
+ };
306
+ res.pipe(writable);
307
+ const S = new DataSender();
308
+ S.send(res, data);
309
+ });
310
+
311
+ it('should send the string value as a plain text', function (done) {
312
+ const data = 'text';
313
+ const res = createResponseMock();
314
+ const writable = new Writable();
315
+ const chunks = [];
316
+ writable._write = function (chunk, encoding, done) {
317
+ chunks.push(chunk);
318
+ done();
319
+ };
320
+ writable._final = function (callback) {
321
+ const sentData = Buffer.concat(chunks).toString('utf-8');
322
+ expect(sentData).to.be.eq(data);
323
+ const ct = res.getHeader('content-type');
324
+ expect(ct).to.be.eq('text/plain');
325
+ callback();
326
+ done();
327
+ };
328
+ res.pipe(writable);
329
+ const S = new DataSender();
330
+ S.send(res, data);
331
+ });
332
+
333
+ it('should allow override the "content-type" header for a string value', function (done) {
334
+ const data = 'text';
335
+ const res = createResponseMock();
336
+ const contentType = 'custom/type';
337
+ res.setHeader('content-type', contentType);
338
+ const writable = new Writable();
339
+ const chunks = [];
340
+ writable._write = function (chunk, encoding, done) {
341
+ chunks.push(chunk);
342
+ done();
343
+ };
344
+ writable._final = function (callback) {
345
+ const sentData = Buffer.concat(chunks).toString('utf-8');
346
+ expect(sentData).to.be.eq(data);
347
+ const ct = res.getHeader('content-type');
348
+ expect(ct).to.be.eq(contentType);
349
+ callback();
350
+ done();
351
+ };
352
+ res.pipe(writable);
353
+ const S = new DataSender();
354
+ S.send(res, data);
355
+ });
192
356
  });
193
357
  });
@@ -25,7 +25,7 @@ export class TrieRouter extends DebuggableService {
25
25
  * ```
26
26
  * const router = new TrieRouter();
27
27
  * router.defineRoute({
28
- * method: HttpMethod.GET, // Request method.
28
+ * method: HttpMethod.GET, // Request method.
29
29
  * path: '/', // Path template.
30
30
  * handler: ctx => 'Hello world!', // Request handler.
31
31
  * });
@@ -32,7 +32,7 @@ describe('TrieRouter', function () {
32
32
 
33
33
  it('should pass the "path" option to a router branch', function () {
34
34
  const router = new TrieRouter();
35
- const branchDef = {path: 'foo'};
35
+ const branchDef = {path: '/foo'};
36
36
  const res = router.createBranch(branchDef);
37
37
  expect(res.getDefinition().path).to.be.eq(branchDef.path);
38
38
  });
@@ -9,7 +9,7 @@ type RequestPatch = {
9
9
  method?: string;
10
10
  secure?: boolean;
11
11
  path?: string;
12
- query?: object;
12
+ query?: string | object;
13
13
  cookies?: object;
14
14
  headers?: object;
15
15
  body?: string;
@@ -13,7 +13,7 @@ import {CHARACTER_ENCODING_LIST} from './fetch-request-body.js';
13
13
  * method?: string;
14
14
  * secure?: boolean;
15
15
  * path?: string;
16
- * query?: object;
16
+ * query?: string | object;
17
17
  * cookies?: object;
18
18
  * headers?: object;
19
19
  * body?: string;
@@ -17,8 +17,8 @@ describe('createRouteMock', function () {
17
17
  });
18
18
 
19
19
  it('sets the "path" option', function () {
20
- const res = createRouteMock({path: 'test'});
21
- expect(res.path).to.be.eq('test');
20
+ const res = createRouteMock({path: '/test'});
21
+ expect(res.path).to.be.eq('/test');
22
22
  });
23
23
 
24
24
  it('sets the "handler" option', function () {
@@ -4,7 +4,6 @@ export * from './is-promise.js';
4
4
  export * from './create-error.js';
5
5
  export * from './to-camel-case.js';
6
6
  export * from './to-pascal-case.js';
7
- export * from './normalize-path.js';
8
7
  export * from './is-response-sent.js';
9
8
  export * from './create-route-mock.js';
10
9
  export * from './is-readable-stream.js';
@@ -4,7 +4,6 @@ export * from './is-promise.js';
4
4
  export * from './create-error.js';
5
5
  export * from './to-camel-case.js';
6
6
  export * from './to-pascal-case.js';
7
- export * from './normalize-path.js';
8
7
  export * from './is-response-sent.js';
9
8
  export * from './create-route-mock.js';
10
9
  export * from './is-readable-stream.js';
@@ -1,12 +0,0 @@
1
- /**
2
- * Normalize path.
3
- *
4
- * Заменяет любые повторяющиеся слеши на один.
5
- * Удаляет пробельные символы в начале и конце.
6
- * Удаляет слеш в конце строки.
7
- * Гарантирует слеш в начале строки (по умолчанию).
8
- *
9
- * @param value
10
- * @param noStartingSlash
11
- */
12
- export function normalizePath(value: string, noStartingSlash?: boolean): string;
@@ -1,22 +0,0 @@
1
- /**
2
- * Normalize path.
3
- *
4
- * Заменяет любые повторяющиеся слеши на один.
5
- * Удаляет пробельные символы в начале и конце.
6
- * Удаляет слеш в конце строки.
7
- * Гарантирует слеш в начале строки (по умолчанию).
8
- *
9
- * @param {string} value
10
- * @param {boolean} [noStartingSlash]
11
- * @returns {string}
12
- */
13
- export function normalizePath(value, noStartingSlash = false) {
14
- if (typeof value !== 'string') {
15
- return '/';
16
- }
17
- const res = value
18
- .trim()
19
- .replace(/\/+/g, '/')
20
- .replace(/(^\/|\/$)/g, '');
21
- return noStartingSlash ? res : '/' + res;
22
- }
@@ -1,56 +0,0 @@
1
- import {expect} from 'chai';
2
- import {normalizePath} from './normalize-path.js';
3
-
4
- describe('normalizePath', function () {
5
- describe('input validation', function () {
6
- it('should return a root path "/" if value is null', function () {
7
- expect(normalizePath(null)).to.equal('/');
8
- });
9
-
10
- it('should return a root path "/" if value is undefined', function () {
11
- expect(normalizePath(undefined)).to.equal('/');
12
- });
13
-
14
- it('should return a root path "/" if value is a number', function () {
15
- expect(normalizePath(123)).to.equal('/');
16
- });
17
-
18
- it('should return a root path "/" if value is an object', function () {
19
- expect(normalizePath({})).to.equal('/');
20
- });
21
- });
22
-
23
- describe('path normalization', function () {
24
- it('should replace multiple slashes with a single slash', function () {
25
- expect(normalizePath('//api///users//')).to.equal('/api/users');
26
- });
27
-
28
- it('should trim a given string but preserve whitespace characters', function () {
29
- expect(normalizePath(' /my folder/ ')).to.equal('/my folder');
30
- expect(normalizePath('path\twith\ntabs')).to.equal('/path\twith\ntabs');
31
- });
32
-
33
- it('should remove leading and trailing slashes before applying the final format', function () {
34
- expect(normalizePath('/foo/bar/')).to.equal('/foo/bar');
35
- });
36
-
37
- it('should handle an empty string by returning "/" by default', function () {
38
- expect(normalizePath('')).to.equal('/');
39
- });
40
- });
41
-
42
- describe('the "noStartingSlash" option', function () {
43
- it('should always prepend a leading slash when the option is false', function () {
44
- expect(normalizePath('foo/bar', false)).to.equal('/foo/bar');
45
- });
46
-
47
- it('should not prepend a leading slash when the option is true', function () {
48
- expect(normalizePath('/foo/bar/', true)).to.equal('foo/bar');
49
- });
50
-
51
- it('should return an empty string if the input results in an empty path', function () {
52
- expect(normalizePath('', true)).to.equal('');
53
- expect(normalizePath('///', true)).to.equal('');
54
- });
55
- });
56
- });