imp(core): IxRouteBuilder
MODIFY
packages/core/src/index.ts
packages/core/src/index.ts
@@ -3,7 +3,7 @@ export { defaultError$ } from './error/error.effect';
export { coreErrorFactory, CoreErrorOptions } from './error/error.factory';
export { HttpError, CoreError, EventError } from './error/error.model';
export { combineRoutes } from './router/router.combiner';
-export { r } from './router/router.builder';
+export { r } from './router/router.ixbuilder';
export { combineEffects, combineMiddlewares } from './effects/effects.combiner';
export { createEffectMetadata } from './effects/effectsMetadata.factory';
export * from './effects/effects.interface';

DELETE
packages/core/src/router/router.builder.ts
-import { Either, left, right } from 'fp-ts/lib/Either';
-import { isNonNullable } from '../+internal/utils';
-import { HttpMiddlewareEffect, HttpEffect } from '../effects/http-effects.interface';
-import { combineMiddlewares } from '../effects/effects.combiner';
-import { coreErrorFactory } from '../error/error.factory';
-import { HttpMethod } from '../http.interface';
-import { RouteEffect } from './router.interface';
-import { CoreError } from '../error/error.model';
-
-type Pipeable = (route: Either<CoreError, Partial<RouteEffect>>) => Either<CoreError, Partial<RouteEffect>>;
-
-const pipe = (
- fn1: Pipeable,
- fn2: Pipeable,
- fn3: Pipeable,
- ...fns: Array<Pipeable>
-) => {
- const initial = right<CoreError, Partial<RouteEffect>>({});
- const pipeable = [fn1, fn2, fn3, ...fns];
- const piped = pipeable.reduce(
- (prevFn, nextFn) => value => nextFn(prevFn(value)),
- value => value,
- );
-
- return piped(initial)
- .getOrElseL(error => { throw error; }) as RouteEffect;
-};
-
-const matchPath = (path: string): Pipeable => route =>
- isNonNullable(path)
- ? route.map(r => ({ ...r, path }))
- : left(coreErrorFactory('Route path has to be defined', { contextMethod: 'matchPath'}));
-
-const matchType = (method: HttpMethod): Pipeable => route =>
- isNonNullable(method)
- ? route.map(r => ({ ...r, method }))
- : left(coreErrorFactory('HttpMethod has to be defined', { contextMethod: 'matchType'}));
-
-const use = (middleware: HttpMiddlewareEffect): Pipeable => route =>
- isNonNullable(middleware)
- ? route.map(r => ({
- ...r,
- middleware: r.middleware
- ? combineMiddlewares(r.middleware, middleware)
- : combineMiddlewares(middleware)
- }))
- : left(coreErrorFactory('Middleware has to be defined', { contextMethod: 'use'}));
-
-const useEffect = (effect: HttpEffect): Pipeable => route =>
- isNonNullable(effect)
- ? route.map(r => ({ ...r, effect }))
- : left(coreErrorFactory('Effect has to be defined', { contextMethod: 'useEffect'}));
-
-export const r = { pipe, matchPath, matchType, use, useEffect };

ADD
packages/core/src/router/router.ixbuilder.ts
+import { RouteEffect } from './router.interface';
+import { HttpMethod } from '../http.interface';
+import { HttpMiddlewareEffect, HttpEffect } from '../effects/http-effects.interface';
+import { combineMiddlewares } from '../effects/effects.combiner';
+
+type Ready = 'Ready';
+type Empty = 'Empty';
+type PathApplied = 'PathApplied';
+type TypeApplied = 'TypeApplied';
+type MiddlewareApplied = 'MiddlewareApplied';
+type EffectApplied = 'EffectApplied';
+
+type RouteEffectSpec = Partial<RouteEffect>;
+
+export class IxRouteBuilder<I, O, A> {
+ readonly _A: A | undefined;
+ readonly _L: O | undefined;
+ readonly _U: I | undefined;
+
+ constructor(readonly spec: A) {}
+
+ run(): A {
+ return this.spec;
+ }
+
+ ichain<Z, B>(f: (a: A) => IxRouteBuilder<O, Z, B>): IxRouteBuilder<I, Z, B> {
+ return new IxRouteBuilder(f(this.spec).run());
+ }
+}
+
+export const ichain = <I, O, Z, A, B>
+ (f: (a: A) => IxRouteBuilder<O, Z, B>) =>
+ (fa: IxRouteBuilder<I, O, A>): IxRouteBuilder<I, Z, B> => {
+ return fa.ichain(f);
+};
+
+type Arity<A, B> = (a: A) => B;
+
+function pipe<A, B>(
+ f1: Arity<A, B>
+): RouteEffect;
+function pipe<A, B, C> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+): RouteEffect;
+function pipe<A, B, C, D> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+ f3: Arity<C, D>,
+): RouteEffect;
+function pipe<A, B, C, D, E> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+ f3: Arity<C, D>,
+ f4: Arity<D, E>,
+): RouteEffect;
+function pipe<A, B, C, D, E, F> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+ f3: Arity<C, D>,
+ f4: Arity<D, E>,
+ f5: Arity<E, F>,
+): RouteEffect;
+function pipe<A, B, C, D, E, F, G> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+ f3: Arity<C, D>,
+ f4: Arity<D, E>,
+ f5: Arity<E, F>,
+ f6: Arity<F, G>,
+): RouteEffect;
+function pipe<A, B, C, D, E, F, G, H> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+ f3: Arity<C, D>,
+ f4: Arity<D, E>,
+ f5: Arity<E, F>,
+ f6: Arity<F, G>,
+ f7: Arity<G, H>,
+): RouteEffect;
+function pipe<A, B, C, D, E, F, G, H, I> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+ f3: Arity<C, D>,
+ f4: Arity<D, E>,
+ f5: Arity<E, F>,
+ f6: Arity<F, G>,
+ f7: Arity<G, H>,
+ f8: Arity<H, I>,
+): RouteEffect;
+function pipe<A, B, C, D, E, F, G, H, I, J> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+ f3: Arity<C, D>,
+ f4: Arity<D, E>,
+ f5: Arity<E, F>,
+ f6: Arity<F, G>,
+ f7: Arity<G, H>,
+ f8: Arity<H, I>,
+ f9: Arity<I, J>,
+): RouteEffect;
+function pipe<A, B, C, D, E, F, G, H, I, J, K> (
+ f1: Arity<A, B>,
+ f2: Arity<B, C>,
+ f3: Arity<C, D>,
+ f4: Arity<D, E>,
+ f5: Arity<E, F>,
+ f6: Arity<F, G>,
+ f7: Arity<G, H>,
+ f8: Arity<H, I>,
+ f9: Arity<I, J>,
+ f10: Arity<J, K>,
+): RouteEffect;
+function pipe(
+ ...fns: Array<Arity<
+ IxRouteBuilder<any, any, any>,
+ IxRouteBuilder<any, any, any>
+ >>
+): RouteEffect {
+ return fns.reduce(
+ (prevFn, nextFn) => value => nextFn(prevFn(value)),
+ value => value,
+ )(route).run();
+}
+
+const route =
+ new IxRouteBuilder<Ready, Empty, RouteEffectSpec>({});
+
+const matchPath = (path: string) => ichain((spec: RouteEffectSpec) =>
+ new IxRouteBuilder<Empty, PathApplied, RouteEffectSpec>({ ...spec, path }));
+
+const matchType = (method: HttpMethod) => ichain((spec: RouteEffectSpec) =>
+ new IxRouteBuilder<PathApplied, TypeApplied, RouteEffectSpec>({ ...spec, method }));
+
+const use = (middleware: HttpMiddlewareEffect) => ichain((spec: RouteEffectSpec) =>
+ new IxRouteBuilder<TypeApplied | MiddlewareApplied, MiddlewareApplied, RouteEffectSpec>({
+ ...spec,
+ middleware: spec.middleware
+ ? combineMiddlewares(spec.middleware, middleware)
+ : middleware
+ }));
+
+const useEffect = (effect: HttpEffect) => ichain((spec: RouteEffectSpec) =>
+ new IxRouteBuilder<TypeApplied | MiddlewareApplied, EffectApplied, RouteEffectSpec>({ ...spec, effect }));
+
+export const r = { matchPath, matchType, use, useEffect, pipe };

ADD
packages/core/src/router/specs/route.ixbuilder.spec.ts
+import { r } from '../router.ixbuilder';
+import { HttpMiddlewareEffect, HttpEffect } from '../../effects/http-effects.interface';
+
+describe('#IxRouteBuilder', () => {
+ test('builds EffectRoute with all ingridients', () => {
+ const m$: HttpMiddlewareEffect = req$ => req$;
+ const e$: HttpEffect = req$ => req$;
+
+ const route = r.pipe(
+ r.matchPath('/'),
+ r.matchType('GET'),
+ r.use(m$),
+ r.use(m$),
+ r.use(m$),
+ r.useEffect(e$),
+ );
+
+ expect(route.path).toEqual('/');
+ expect(route.method).toEqual('GET');
+ expect(route.effect).toEqual(e$);
+ expect(route.middleware).toBeInstanceOf(Function);
+ expect(route.effect).toBeInstanceOf(Function);
+ });
+
+ test('builds EffectRoute without middleware', () => {
+ const e$: HttpEffect = req$ => req$;
+
+ const route = r.pipe(
+ r.matchPath('/'),
+ r.matchType('GET'),
+ r.useEffect(e$),
+ );
+
+ expect(route.path).toEqual('/');
+ expect(route.method).toEqual('GET');
+ expect(route.effect).toEqual(e$);
+ expect(route.middleware).toBeUndefined();
+ expect(route.effect).toBeInstanceOf(Function);
+ });
+});

imp(core): IxRouteBuilder