fix(core): matchers should be resetted after each matching failure
RENAME
packages/core/src/operators/matchPath/matcher.factory.spec.ts
packages/core/src/operators/matchPath/matchPath.factory.spec.ts
-import { matcherFactory, removeQueryParams, removeTrailingSlash } from './matcher.factory';
+import { matchPathFactory, removeQueryParams, removeTrailingSlash } from './matchPath.factory';
-describe('matcher factory', () => {
+describe('matchPath factory', () => {
it('#removeQueryParams removes query parameters from url', () => {
// given
@@ -32,7 +32,7 @@ describe('matcher factory', () => {
const pathToMatch = '/foo/';
// when
- const matcher = matcherFactory(matchingHistory, pathToMatch);
+ const matcher = matchPathFactory(matchingHistory, pathToMatch);
// then
expect(matcher).toEqual('/api/v1/user/foo');
@@ -44,7 +44,7 @@ describe('matcher factory', () => {
const pathToMatch = '/foo/';
// when
- const matcher = matcherFactory(matchingHistory, pathToMatch);
+ const matcher = matchPathFactory(matchingHistory, pathToMatch);
// then
expect(matcher).toEqual('/api/v1/user/foo');
@@ -57,7 +57,7 @@ describe('matcher factory', () => {
const suffix = '/:foo*';
// when
- const matcher = matcherFactory(matchingHistory, pathToMatch, suffix);
+ const matcher = matchPathFactory(matchingHistory, pathToMatch, suffix);
// then
expect(matcher).toEqual('/api/:version/user/foo/:foo*');

RENAME
packages/core/src/operators/matchPath/matcher.factory.ts
packages/core/src/operators/matchPath/matchPath.factory.ts
@@ -3,7 +3,7 @@ export const removeTrailingSlash = (path: string): string =>
export const removeQueryParams = (path: string): string => path.split('?')[0];
-export const matcherFactory = (
+export const matchPathFactory = (
matchingHistory: string[],
pathToMatch: string,
suffix?: string

MODIFY
packages/core/src/operators/matchPath/matchPath.operator.ts
packages/core/src/operators/matchPath/matchPath.operator.ts
@@ -2,7 +2,8 @@ import * as pathToRegexp from 'path-to-regexp';
import { Observable } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { HttpRequest } from '../../http.interface';
-import { matcherFactory, removeQueryParams } from './matcher.factory';
+import { isRequestNotMatched, matchGuard } from '../../util/matcher.guard';
+import { matchPathFactory, removeQueryParams } from './matchPath.factory';
import { queryParamsFactory } from './queryParams.factory';
import { urlParamsFactory } from './urlParams.factory';
@@ -11,22 +12,20 @@ type MatcherOpts = {
combiner?: boolean;
};
-const isRequestMatched = (req: HttpRequest) =>
- !(req.matchPath && req.matchType);
-
export const matchPath = (path: string, opts: MatcherOpts = {}) => (source$: Observable<HttpRequest>) =>
source$.pipe(
tap(req => (req.query = req.query || {})),
tap(req => (req.params = req.params || {})),
tap(req => (req.matchers = req.matchers || [])),
- filter(isRequestMatched),
+ filter(isRequestNotMatched),
filter(req => {
if (path === '*') { return true; }
- const matcher = matcherFactory(req.matchers!, path, opts.suffix);
+ const matcher = matchPathFactory(req.matchers!, path, opts.suffix);
const url = removeQueryParams(req.url);
+ const isMatched = pathToRegexp(matcher).test(url);
- return pathToRegexp(matcher).test(url);
+ return matchGuard(isMatched)(req);
}),
tap(req => (req.params = urlParamsFactory(req, path))),
tap(req => (req.query = queryParamsFactory(req.url))),

MODIFY
packages/core/src/operators/matchPath/urlParams.factory.ts
packages/core/src/operators/matchPath/urlParams.factory.ts
import * as pathToRegexp from 'path-to-regexp';
import { HttpRequest, RouteParameters } from '../../http.interface';
-import { matcherFactory, removeQueryParams } from './matcher.factory';
+import { matchPathFactory, removeQueryParams } from './matchPath.factory';
export const urlParamsFactory = (req: HttpRequest, path: string): RouteParameters => {
- const match = matcherFactory(req.matchers!, path);
+ const match = matchPathFactory(req.matchers!, path);
const url = removeQueryParams(req.url);
const params = pathToRegexp(match).exec(url);
const routes = pathToRegexp.parse(match);

MODIFY
packages/core/src/operators/matchType/matchType.operator.ts
packages/core/src/operators/matchType/matchType.operator.ts
import { Observable } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { HttpMethod, HttpRequest } from '../../http.interface';
+import { isRequestNotMatched, matchGuard } from '../../util/matcher.guard';
export const matchType = (method: HttpMethod | '*') => (source$: Observable<HttpRequest>) =>
source$.pipe(
- filter(req => req.method === method || method === '*'),
- tap(req => req.matchType = true),
+ filter(isRequestNotMatched),
+ filter(req => matchGuard(req.method === method || method === '*')(req)),
+ tap(req => (req.matchType = true)),
);

ADD
packages/core/src/util/matcher.guard.spec.ts
+import { HttpRequest } from '../http.interface';
+import { isRequestNotMatched, matchGuard } from './matcher.guard';
+
+describe('Matcher guard', () => {
+
+ test('#isRequestNotMatched checks if request was matched', () => {
+ // given
+ const notMatchedReq = { matchType: true, matchPath: false } as HttpRequest;
+
+ // when
+ const isNotMatched = isRequestNotMatched(notMatchedReq);
+
+ // then
+ expect(isNotMatched).toBe(true);
+ });
+
+ test(`#matchGuard resets matchers if matching predicate is false and request wasn't matched`, () => {
+ // given
+ const req = { matchType: true, matchPath: false } as HttpRequest;
+ const expectedReq = { matchType: false, matchPath: false } as HttpRequest;
+
+ // when
+ const matchingResult = matchGuard(false)(req);
+
+ // then
+ expect(matchingResult).toBe(false);
+ expect(req).toEqual(expectedReq);
+ });
+
+});

ADD
packages/core/src/util/matcher.guard.ts
+import { HttpRequest } from '../http.interface';
+
+export const isRequestNotMatched = (req: HttpRequest) =>
+ !(req.matchPath && req.matchType);
+
+export const matchGuard = (isMatched: boolean) => (req: HttpRequest) => {
+ if (!isMatched && isRequestNotMatched(req)) {
+ req.matchType = false;
+ req.matchPath = false;
+ }
+
+ return isMatched;
+};

MODIFY
packages/spec/routing.integration.spec.ts
packages/spec/routing.integration.spec.ts
import { delay, map, tap } from 'rxjs/operators';
import * as request from 'supertest';
-import { Effect, combineRoutes, httpListener, matchPath, matchType } from '../core/src';
+import { combineRoutes, Effect, httpListener, matchPath, matchType } from '../core/src';
describe('Routing integration', async () => {
@@ -69,7 +69,10 @@ describe('Routing integration', async () => {
const app = httpListener({ effects: [p1$, p2$, p3$ ] });
// when
- return request(app).get('/p1').expect(200, [1]);
+ const response = request(app).get('/p1');
+
+ // then
+ return response.expect(200, [1]);
});
test('when paths with params are matched - resolves Effects in array order', () => {
@@ -86,7 +89,6 @@ describe('Routing integration', async () => {
matchPath('/:name'), matchType('GET'), map(req => ({ body: req.test }))
);
- // then
const app1 = httpListener({ effects: [p1$, p2$ ] });
const app2 = httpListener({ effects: [p2$, p1$ ] });
@@ -118,7 +120,56 @@ describe('Routing integration', async () => {
const app = httpListener({ effects: [p1$, p2$, notFound$ ] });
// when
- return request(app).get('/test').expect(200, '"not-found"');
+ const response = request(app).get('/test');
+
+ // then
+ return response.expect(200, '"not-found"');
+ });
+
+ test(`reordered matchers doesn't produces 404 error`, () => {
+ // given
+ const p1$: Effect = req$ => req$.pipe(
+ matchPath('/p1'),
+ matchType('GET'),
+ map(req => ({ body: 1 }))
+ );
+
+ const p2$: Effect = req$ => req$.pipe(
+ matchType('POST'),
+ matchPath('/p1'),
+ map(req => ({ body: 2 }))
+ );
+
+ const app = httpListener({ effects: [p1$, p2$] });
+
+ // when
+ const response = request(app).post('/p1');
+
+ // then
+ return response.expect(200, '2');
+ });
+
+ test(`single matcher doesn't pass request when routing is resolved`, () => {
+ // given
+ const p1$: Effect = req$ => req$.pipe(
+ matchType('POST'),
+ matchPath('/p1'),
+ map(req => ({ body: 1 }))
+ );
+
+ const p2$: Effect = req$ => req$.pipe(
+ matchType('POST'),
+ map(req => ({ body: 2 }))
+ );
+
+ const app = httpListener({ effects: [p1$, p2$ ] });
+
+ // when
+ const response = request(app).post('/p1');
+
+ // then
+ return response.expect(200, '1');
});
+
});

fix(core): matchers should be resetted after each matching failure