refactor: routing (part-2)
MODIFY
benchmarks/middleware.js
benchmarks/middleware.js
const http = require('http');
const { mapTo } = require('rxjs/operators');
-const { httpListener, effect } = require('../packages/core/dist');
+const { httpListener, EffectFactory } = require('../packages/core/dist');
const { logger$ } = require('../packages/middleware-logger/dist');
const n = parseInt(process.env.MW || '1', 10);
@@ -9,14 +9,24 @@ const middlewares = [];
const effects = [];
for (let i = 0; i < n-1; i++) {
- effects.push(effect(`/test-${i}`)('GET')(r$ => r$.pipe(
+ effects.push(
+ EffectFactory
+ .matchPath(`/test-${i}`)
+ .matchType('GET')
+ .use(r$ => r$.pipe(
mapTo({ body: 'Hello World' })
- )));
+ ))
+ );
}
-effects.push(effect(`/`)('GET')(r$ => r$.pipe(
+effects.push(
+ EffectFactory
+ .matchPath(`/`)
+ .matchType('GET')
+ .use(r$ => r$.pipe(
mapTo({ body: 'Hello World' })
-)));
+ ))
+);
const app = httpListener({ middlewares, effects });

MODIFY
packages/core/src/effects/effects.factory.spec.ts
packages/core/src/effects/effects.factory.spec.ts
import { mapTo } from 'rxjs/operators';
import { Effect } from './effects.interface';
-import { effect } from './effects.factory';
+import { EffectFactory } from './effects.factory';
describe('Effects factory', () => {
@@ -11,7 +11,10 @@ describe('Effects factory', () => {
const method = 'GET';
// when
- const factorizedEffect = effect(path)(method)(effect$);
+ const factorizedEffect = EffectFactory
+ .matchPath(path)
+ .matchType(method)
+ .use(effect$);
// then
expect(factorizedEffect).toEqual({

MODIFY
packages/core/src/effects/effects.factory.ts
packages/core/src/effects/effects.factory.ts
@@ -2,7 +2,14 @@ import { HttpMethod } from '../http.interface';
import { Effect, EffectResponse } from './effects.interface';
import { RouteConfig } from '../router/router.interface';
-export const effect =
- (path: string) =>
- (method: HttpMethod) =>
- (effect: Effect<EffectResponse>): RouteConfig => ({ path, method, effect });
+export namespace EffectFactory {
+
+ export const matchPath = (path: string) => ({
+ matchType: (method: HttpMethod) => ({
+ use: (effect: Effect<EffectResponse>): RouteConfig => ({
+ path, method, effect
+ }),
+ }),
+ });
+
+}

MODIFY
packages/core/src/http.listener.spec.ts
packages/core/src/http.listener.spec.ts
@@ -3,11 +3,14 @@ import { mapTo, switchMap } from 'rxjs/operators';
import * as request from 'supertest';
import { HttpError } from './error/error.model';
import { httpListener } from './http.listener';
-import { effect } from './effects/effects.factory';
+import { EffectFactory } from './effects/effects.factory';
describe('Http listener', () => {
it('reacts to attached effect', async () => {
- const effect$ = effect('/test')('GET')(req$ => req$
+ const effect$ = EffectFactory
+ .matchPath('/test')
+ .matchType('GET')
+ .use(req$ => req$
.pipe(
mapTo({ status: 200 })
)
@@ -21,7 +24,10 @@ describe('Http listener', () => {
});
it('reacts to throwed exception', async () => {
- const effect$ = effect('/test')('GET')(req$ => req$
+ const effect$ = EffectFactory
+ .matchPath('/test')
+ .matchType('GET')
+ .use(req$ => req$
.pipe(
switchMap(() => throwError(new HttpError('test', 500))),
mapTo({ status: 200 })

MODIFY
packages/core/src/index.spec.ts
packages/core/src/index.spec.ts
@@ -6,8 +6,8 @@ describe('@marblejs/core public API', () => {
expect(API.httpListener).toBeDefined();
expect(API.HttpError).toBeDefined();
expect(API.error$).toBeDefined();
- expect(API.effect).toBeDefined();
- // expect(API.combineRoutes).toBeDefined();
+ expect(API.EffectFactory).toBeDefined();
+ expect(API.combineRoutes).toBeDefined();
expect(API.ContentType).toBeDefined();
expect(API.compose).toBeDefined();
});

MODIFY
packages/core/src/index.ts
packages/core/src/index.ts
-// export { combineRoutes } from './effects/effects.combiner';
-export { effect } from './effects/effects.factory';
+export { EffectFactory } from './effects/effects.factory';
export { error$ } from './error/error.middleware';
export { HttpError } from './error/error.model';
+export { combineRoutes } from './router/router';
export * from './effects/effects.interface';
export * from './router/router.interface';
export * from './http.interface';

MODIFY
packages/core/src/router/router.helpers.ts
packages/core/src/router/router.helpers.ts
-import { RouteGroup, RoutingGroup, Routing } from './router.interface';
+import { RouteGroup, RoutingGroup, Routing, RouteCombinerConfig } from './router.interface';
export { Routing };
export const isRouteGroup = (item): item is RouteGroup =>
Array.isArray(item.effects) &&
Array.isArray(item.middlewares);
+ export const isRouteCombinerConfig = (item): item is RouteCombinerConfig =>
+ Array.isArray(item.effects) &&
+ Array.isArray(item.middlewares);
+
export const isRoutingGroup = (item): item is RoutingGroup =>
item.length === 2;

MODIFY
packages/core/src/router/router.interface.ts
packages/core/src/router/router.interface.ts
@@ -3,6 +3,11 @@ import { Effect } from '../effects/effects.interface';
export type RouteEffects = RouteConfig | RouteGroup;
+export interface RouteCombinerConfig {
+ middlewares?: Effect<HttpRequest>[];
+ effects: RouteEffects[];
+}
+
export interface RouteConfig {
path: string;
method: HttpMethod;
@@ -15,11 +20,6 @@ export interface RouteGroup {
middlewares: Effect<HttpRequest>[];
}
-export interface RouteMatched {
- route: RoutingRoute;
- routeMatcher: string;
-}
-
export type RoutingRoute = [string, HttpMethod, Effect];
export type RoutingGroup = [string, Routing];
export interface Routing extends Array<RoutingRoute | RoutingGroup> {}

MODIFY
packages/core/src/router/router.spec.ts
packages/core/src/router/router.spec.ts
@@ -70,9 +70,9 @@ describe('Router', () => {
const route4 = findRoute(routing, '/group/nested/fo', 'POST', '');
// then
- expect(route1).toEqual({ route: ['/foo', 'POST', e$ ], routeMatcher: '/group/nested/foo' });
- expect(route2).toEqual({ route: ['/foo', 'POST', e$ ], routeMatcher: '/group/nested/foo' });
- expect(route3).toEqual({ route: ['/foo', 'POST', e$ ], routeMatcher: '/group/nested/foo' });
+ expect(route1).toEqual(['/foo', 'POST', e$ ]);
+ expect(route2).toEqual(['/foo', 'POST', e$ ]);
+ expect(route3).toEqual(['/foo', 'POST', e$ ]);
expect(route4).toBeUndefined();
});
@@ -93,7 +93,7 @@ describe('Router', () => {
test('resolves found effect', done => {
// given
const effect$: Effect = req$ => req$.pipe(mapTo({ body: 'test' }));
- const expectedMachingResult = { route: ['/', 'GET', effect$], routeMatcher: '/' };
+ const expectedMachingResult = ['/', 'GET', effect$];
const req = { url: '/', method: 'GET', query: null, params: null } as HttpRequest;
const res = {} as HttpResponse;
const mockedQuery = {};
@@ -110,7 +110,6 @@ describe('Router', () => {
expect(effect).toBeDefined();
expect(effect.body).toEqual('test');
expect(req.query).toEqual(mockedQuery);
- expect(req.params).toEqual(mockedParams);
done();
});
});

MODIFY
packages/core/src/router/router.ts
packages/core/src/router/router.ts
@@ -2,10 +2,26 @@ import { Observable, empty, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { HttpResponse, HttpRequest, HttpMethod } from '../http.interface';
import { EffectResponse } from '../effects/effects.interface';
-import { isRouteGroup, isRoutingGroup } from './router.helpers';
-import { RouteConfig, Routing, RouteGroup, RoutingGroup, RoutingRoute, RouteMatched } from './router.interface';
+import { isRouteGroup, isRoutingGroup, isRouteCombinerConfig } from './router.helpers';
+import {
+ RouteConfig,
+ Routing,
+ RouteGroup,
+ RoutingGroup,
+ RoutingRoute,
+ RouteCombinerConfig,
+ RouteEffects,
+} from './router.interface';
import { queryParamsFactory } from '../router/queryParams.factory';
-import { urlParamsFactory } from './urlParams.factory';
+
+export const combineRoutes = (
+ path: string,
+ configOrEffects: RouteCombinerConfig | RouteEffects[]
+): RouteGroup => ({
+ path,
+ effects: isRouteCombinerConfig(configOrEffects) ? configOrEffects.effects : configOrEffects,
+ middlewares: isRouteCombinerConfig(configOrEffects) ? (configOrEffects.middlewares || []) : [],
+});
export const routingFactory = (routes: (RouteGroup | RouteConfig)[]): Routing =>
routes.map(route => isRouteGroup(route)
@@ -18,7 +34,7 @@ export const findRoute = (
matchUrl: string,
matchMethod: HttpMethod,
matchHistory = ''
-): RouteMatched | undefined => {
+): RoutingRoute | undefined => {
for (let i = 0; i < routing.length; ++i) {
const route = routing[i];
@@ -33,14 +49,11 @@ export const findRoute = (
}
const [routePath, routeMethod] = route;
- const matcher = matchHistory + routePath;
+ const matcher = matchHistory + (routePath === '/' ? '' : routePath);
const isMatched = new RegExp(`^${matcher}\\??[^\/]*$`).test(matchUrl) && routeMethod === matchMethod;
if (isMatched) {
- return {
- route: route as RoutingRoute,
- routeMatcher: matcher,
- };
+ return route as RoutingRoute;
}
}
@@ -62,9 +75,8 @@ export const resolveRouting =
const req$ = of(req).pipe(
tap(req => {
req.query = queryParamsFactory(req.url);
- req.params = urlParamsFactory(routeMatched.routeMatcher, req.url);
})
);
- return routeMatched.route[2](req$, res, null);
+ return routeMatched[2](req$, res, null);
};

MODIFY
packages/middleware-joi/test/helpers/api.spec-util.ts
packages/middleware-joi/test/helpers/api.spec-util.ts
import { validator$, Joi } from '../../src';
-import { Effect, httpListener, use, effect } from '@marblejs/core';
+import { httpListener, use, EffectFactory, combineRoutes } from '@marblejs/core';
import { bodyParser$ } from '@marblejs/middleware-body';
import { map } from 'rxjs/operators';
-const getPost$ = effect('/post')('GET')(req$ => req$
+const getPost$ = EffectFactory
+ .matchPath('/post')
+ .matchType('GET')
+ .use(req$ => req$
.pipe(
use(
validator$({
@@ -16,9 +19,12 @@ const getPost$ = effect('/post')('GET')(req$ => req$
),
map(req => req.query),
map(query => ({ status: 200, body: { ...query } }))
- );
+ ));
-const getUser$ = effect('/user/:id')('GET')(req$ => req$
+const getUser$ = EffectFactory
+ .matchPath('/user/:id')
+ .matchType('GET')
+ .use(req$ => req$
.pipe(
use(
validator$({
@@ -33,12 +39,13 @@ const getUser$ = effect('/user/:id')('GET')(req$ => req$
),
map(req => req.params),
map(params => ({ status: 200, body: { id: params!.id } }))
- );
+ ));
-const storePost$: Effect = request$ =>
- request$.pipe(
- matchPath('/post'),
- matchType('POST'),
+const storePost$ = EffectFactory
+ .matchPath('/post')
+ .matchType('POST')
+ .use(req$ => req$
+ .pipe(
use(
validator$({
query: {
@@ -50,12 +57,13 @@ const storePost$: Effect = request$ =>
})
),
map(resp => ({ body: { ...resp.body, ...resp.query } }))
- );
+ ));
-const postUser$: Effect = request$ =>
- request$.pipe(
- matchPath('/user'),
- matchType('POST'),
+const postUser$ = EffectFactory
+ .matchPath('/user')
+ .matchType('POST')
+ .use(req$ => req$
+ .pipe(
use(
validator$({
body: {
@@ -66,7 +74,7 @@ const postUser$: Effect = request$ =>
),
map(req => req.body),
map(response => ({ body: response }))
- );
+ ));
const api$ = combineRoutes('/api', [getPost$, getUser$, postUser$, storePost$]);

MODIFY
packages/spec/api.integration.spec.ts
packages/spec/api.integration.spec.ts
@@ -4,7 +4,7 @@ import { filter, map, mapTo, switchMap } from 'rxjs/operators';
import * as request from 'supertest';
import { HttpStatus } from '../core/dist/http.interface';
import { ContentType } from '../core/dist/util/contentType.util';
-import { Effect, HttpError, HttpRequest, combineRoutes, httpListener, matchPath, matchType, use } from '../core/src';
+import { EffectFactory, Effect, HttpError, HttpRequest, combineRoutes, httpListener, use } from '../core/src';
import { bodyParser$ } from '../middleware-body/src';
import { readFile } from '../util/fileReader.helper';
@@ -16,56 +16,63 @@ const authorize$: Effect<HttpRequest> = request$ =>
filter(req => req.headers.authorization === 'Bearer test'),
);
-const root$: Effect = req$ =>
- req$.pipe(
- matchPath('/'),
- matchType('GET'),
+const root$ = EffectFactory
+ .matchPath('/')
+ .matchType('GET')
+ .use(req$ => req$
+ .pipe(
mapTo({ status: 200, body: 'root' }),
- );
+ ));
-const getUserList$: Effect = request$ =>
- request$.pipe(
- matchPath('/'),
- matchType('GET'),
+const getUserList$ = EffectFactory
+ .matchPath('/')
+ .matchType('GET')
+ .use(req$ => req$
+ .pipe(
switchMap(() => of(MOCKED_USER_LIST)),
map(users => ({ body: users }))
- );
+ ));
-const getUserSingle$: Effect = request$ =>
- request$.pipe(
- matchPath('/:id'),
- matchType('GET'),
+const getUserSingle$ = EffectFactory
+ .matchPath('/:id')
+ .matchType('GET')
+ .use(req$ => req$
+ .pipe(
map(({ params, query }) => ({ body: { params, query } }))
- );
+ ));
-const postUser$: Effect = request$ =>
- request$.pipe(
- matchPath('/'),
- matchType('POST'),
+const postUser$ = EffectFactory
+ .matchPath('/')
+ .matchType('POST')
+ .use(req$ => req$
+ .pipe(
use(authorize$),
map(req => req.body),
map(response => ({ body: response }))
- );
+ ));
-const error$: Effect = request$ =>
- request$.pipe(
- matchPath('/error'),
- matchType('GET'),
+const error$ = EffectFactory
+ .matchPath('/error')
+ .matchType('GET')
+ .use(req$ => req$
+ .pipe(
switchMap(() => throwError(new HttpError('test', HttpStatus.METHOD_NOT_ALLOWED, { test: 'test' }))),
+ )
);
-const file$: Effect = request$ =>
- request$.pipe(
- matchPath('/static/:dir'),
- matchType('GET'),
+const file$ = EffectFactory
+ .matchPath('/static/:dir')
+ .matchType('GET')
+ .use(req$ => req$
+ .pipe(
map(req => req.params!.dir as string),
switchMap(readFile(STATIC_PATH)),
map(body => ({ body }))
- );
+ ));
const user$ = combineRoutes('/user', [getUserList$, getUserSingle$, postUser$]);
-const api$ = combineRoutes('/api/:version', [root$, file$, error$, user$]);
+const api$ = combineRoutes('/api/v1', [root$, file$, error$, user$]);
const app = httpListener({
middlewares: [bodyParser$],

DELETE
packages/spec/routing.integration.spec.ts
-import { delay, map, tap } from 'rxjs/operators';
-import * as request from 'supertest';
-import { combineRoutes, Effect, httpListener, matchPath, matchType } from '../core/src';
-
-describe('Routing integration', async () => {
-
- test('flow goes through Effects in array order', () => {
- // given
- const p1$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push(1)),
- matchPath('/p1'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const p2$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push(2)),
- delay(100),
- matchPath('/p2'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const p3$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push(3)),
- matchPath('/p3'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const all$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push('all')),
- matchPath('*'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const combined$ = combineRoutes('/combined', [p1$]);
-
- const app = httpListener({ effects: [p1$, p2$, combined$, p3$, all$] });
-
- // when
- return Promise.all([
- request(app).get('/p1').expect(200, [1]),
- request(app).get('/combined/p1').expect(200, [1, 2, 1]),
- request(app).get('/p2').expect(200, [1, 2]),
- request(app).get('/p3').expect(200, [1, 2, 3]),
- request(app).get('/combined/not-found').expect(200, [1, 2, 1, 3, 'all']),
- ]);
- });
-
- test('when all paths are the same - resolves only one Effect', () => {
- // given
- const p1$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push(1)),
- delay(100),
- matchPath('/p1'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const p2$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push(2)),
- matchPath('/p1'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const p3$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push(3)),
- matchPath('/p1'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const app = httpListener({ effects: [p1$, p2$, p3$ ] });
-
- // when
- const response = request(app).get('/p1');
-
- // then
- return response.expect(200, [1]);
- });
-
- test('when paths with params are matched - resolves Effects in array order', () => {
- // given
- const p1$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push(1)),
- matchPath('/p1'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const p2$: Effect = req$ => req$.pipe(
- tap(req => req.test = req.test || []),
- tap(req => req.test.push(2)),
- matchPath('/:name'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const app1 = httpListener({ effects: [p1$, p2$ ] });
- const app2 = httpListener({ effects: [p2$, p1$ ] });
-
- // when
- return Promise.all([
- // 1 - /p1 2 - /:name
- request(app1).get('/p1').expect(200, [1]),
- request(app1).get('/test').expect(200, [1, 2]),
- // 1 - /:name 2 - /p1
- request(app2).get('/test').expect(200, [2]),
- request(app2).get('/p1').expect(200, [2]),
- ]);
- });
-
- test('when no paths are matched - executes last "*" handler', () => {
- // given
- const p1$: Effect = req$ => req$.pipe(
- matchPath('/p1'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const p2$: Effect = req$ => req$.pipe(
- matchPath('/p2'), matchType('GET'), map(req => ({ body: req.test }))
- );
-
- const notFound$: Effect = req$ => req$.pipe(
- matchPath('*'), matchType('GET'), map(req => ({ body: 'not-found' }))
- );
-
- const app = httpListener({ effects: [p1$, p2$, notFound$ ] });
-
- // when
- 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');
- });
-
-
-});

refactor: routing (part-2)