Skip to content

Commit

Permalink
feat(router): add daffRouterComposeGuards (#2809)
Browse files Browse the repository at this point in the history
  • Loading branch information
griest024 committed May 23, 2024
1 parent 5839572 commit 075859b
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 1 deletion.
2 changes: 1 addition & 1 deletion libs/router/src/data/helpers/public_api.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { daffRouterNamedViewsCollect } from './collect-data';
export { daffRouterDataCollect } from './collect-data';
180 changes: 180 additions & 0 deletions libs/router/src/guards/compose.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import {
ActivatedRouteSnapshot,
CanActivateFn,
RouterStateSnapshot,
} from '@angular/router';
import { of } from 'rxjs';

import { observe } from '@daffodil/core';

import { daffRouterComposeGuards } from './compose';

describe('@daffodil/router | daffRouterComposeGuards', () => {
let blockingGuard0: jasmine.Spy<CanActivateFn>;
let blockingGuard1: jasmine.Spy<CanActivateFn>;
let blockingGuard2: jasmine.Spy<CanActivateFn>;
let nonBlockingGuard0: jasmine.Spy<CanActivateFn>;
let nonBlockingGuard1: jasmine.Spy<CanActivateFn>;
let nonBlockingGuard2: jasmine.Spy<CanActivateFn>;
let result: CanActivateFn;
const args = <const>[new ActivatedRouteSnapshot(), <RouterStateSnapshot>{}];

beforeEach(() => {
blockingGuard0 = jasmine.createSpy().and.returnValue(of(true));
blockingGuard1 = jasmine.createSpy().and.returnValue(of(true));
blockingGuard2 = jasmine.createSpy().and.returnValue(of(true));
nonBlockingGuard0 = jasmine.createSpy().and.returnValue(of(true));
nonBlockingGuard1 = jasmine.createSpy().and.returnValue(of(true));
nonBlockingGuard2 = jasmine.createSpy().and.returnValue(of(true));

result = daffRouterComposeGuards([
blockingGuard0,
blockingGuard1,
blockingGuard2,
], [
nonBlockingGuard0,
nonBlockingGuard1,
nonBlockingGuard2,
]);
});

describe('when all guards return true', () => {
it('should return true', (done) => {
observe(result(...args)).subscribe((res) => {
expect(res).toBeTrue();
done();
});
});

it('should call the all the guards', (done) => {
observe(result(...args)).subscribe((res) => {
expect(blockingGuard0).toHaveBeenCalledWith(...args);
expect(blockingGuard1).toHaveBeenCalledWith(...args);
expect(blockingGuard2).toHaveBeenCalledWith(...args);
expect(nonBlockingGuard0).toHaveBeenCalledWith(...args);
expect(nonBlockingGuard1).toHaveBeenCalledWith(...args);
expect(nonBlockingGuard2).toHaveBeenCalledWith(...args);
done();
});
});

it('should call the first blocking guard before all the other guards', (done) => {
blockingGuard0.and.callFake(() => {
expect(blockingGuard1).not.toHaveBeenCalled();
expect(blockingGuard2).not.toHaveBeenCalled();
expect(nonBlockingGuard0).not.toHaveBeenCalled();
expect(nonBlockingGuard1).not.toHaveBeenCalled();
expect(nonBlockingGuard2).not.toHaveBeenCalled();
return true;
});
observe(result(...args)).subscribe((res) => {
done();
});
});

it('should call the second blocking guard after the first and before all the other guards', (done) => {
blockingGuard1.and.callFake(() => {
expect(blockingGuard0).toHaveBeenCalledWith(...args);

expect(blockingGuard2).not.toHaveBeenCalled();
expect(nonBlockingGuard0).not.toHaveBeenCalled();
expect(nonBlockingGuard1).not.toHaveBeenCalled();
expect(nonBlockingGuard2).not.toHaveBeenCalled();
return true;
});
observe(result(...args)).subscribe((res) => {
done();
});
});

it('should call the third blocking guard after the first and second and before all the other guards', (done) => {
blockingGuard2.and.callFake(() => {
expect(blockingGuard0).toHaveBeenCalledWith(...args);
expect(blockingGuard1).toHaveBeenCalledWith(...args);

expect(nonBlockingGuard0).not.toHaveBeenCalled();
expect(nonBlockingGuard1).not.toHaveBeenCalled();
expect(nonBlockingGuard2).not.toHaveBeenCalled();
return true;
});
observe(result(...args)).subscribe((res) => {
done();
});
});
});

describe('when the first blocking guard returns false', () => {
beforeEach(() => {
blockingGuard0.and.returnValue(of(false));
});

it('should return false', (done) => {
observe(result(...args)).subscribe((res) => {
expect(res).toBeFalse();
done();
});
});

it('should not call any other guards', (done) => {
observe(result(...args)).subscribe((res) => {
expect(blockingGuard1).not.toHaveBeenCalled();
expect(blockingGuard2).not.toHaveBeenCalled();
expect(nonBlockingGuard0).not.toHaveBeenCalled();
expect(nonBlockingGuard1).not.toHaveBeenCalled();
expect(nonBlockingGuard2).not.toHaveBeenCalled();
done();
});
});
});

describe('when the second blocking guard returns false', () => {
beforeEach(() => {
blockingGuard1.and.returnValue(of(false));
});

it('should return false', (done) => {
observe(result(...args)).subscribe((res) => {
expect(res).toBeFalse();
done();
});
});
});

describe('when the third blocking guard returns false', () => {
beforeEach(() => {
blockingGuard2.and.returnValue(of(false));
});

it('should return false', (done) => {
observe(result(...args)).subscribe((res) => {
expect(res).toBeFalse();
done();
});
});
});

describe('when a single non-blocking guard returns false', () => {
beforeEach(() => {
nonBlockingGuard1.and.returnValue(of(false));
});

it('should return false', (done) => {
observe(result(...args)).subscribe((res) => {
expect(res).toBeFalse();
done();
});
});

it('should call the all the guards', (done) => {
observe(result(...args)).subscribe((res) => {
expect(blockingGuard0).toHaveBeenCalledWith(...args);
expect(blockingGuard1).toHaveBeenCalledWith(...args);
expect(blockingGuard2).toHaveBeenCalledWith(...args);
expect(nonBlockingGuard0).toHaveBeenCalledWith(...args);
expect(nonBlockingGuard1).toHaveBeenCalledWith(...args);
expect(nonBlockingGuard2).toHaveBeenCalledWith(...args);
done();
});
});
});
});
46 changes: 46 additions & 0 deletions libs/router/src/guards/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
GuardResult,
UrlTree,
CanActivateChildFn,
CanActivateFn,
} from '@angular/router';
import {
of,
switchMap,
combineLatest,
OperatorFunction,
map,
} from 'rxjs';

import { observe } from '@daffodil/core';

function guardFailure(val: GuardResult): boolean {
return !val || val instanceof UrlTree;
}

/**
* Composes functional guards together into a single functional guard.
* Both blocking and non-blocking guards may be specified.
* Blocking guards run in serial, waiting for a response before calling the next blocking guard.
* If a blocking guard returns a failure condition (falsy or a `UrlTree`), all future guards will be skipped and not called. The failure condition return will be returned from the composed guard.
* Non-blocking guards are run in parallel after all of the blocking guards have finished.
*/
export function daffRouterComposeGuards(blockingGuards: Array<CanActivateFn | CanActivateChildFn>, nonBlockingGuards: Array<CanActivateFn | CanActivateChildFn> = []): CanActivateFn | CanActivateChildFn {
return (...args) => of(true).pipe(
// @ts-expect-error rxjs has not written a function overload for only rest param...so this errors https://github.com/ReactiveX/rxjs/issues/4177#issuecomment-2125328922
...blockingGuards.map<OperatorFunction<GuardResult, GuardResult>>((guard) =>
switchMap((prevGuardResult) =>
guardFailure(prevGuardResult)
? of(prevGuardResult)
: observe(guard(...args)),
),
),
switchMap((prevGuardResult) =>
guardFailure(prevGuardResult)
? of(prevGuardResult)
: combineLatest(nonBlockingGuards.map((guard) => observe(guard(...args)))).pipe(
map((results) => results.reduce((acc, res) => res ? acc && res : res)),
),
),
);
}
1 change: 1 addition & 0 deletions libs/router/src/guards/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './compose';
2 changes: 2 additions & 0 deletions libs/router/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './named-view/public_api';
export * from './data/public_api';
export * from './guards/public_api';

0 comments on commit 075859b

Please sign in to comment.