diff --git a/libs/router/src/data/helpers/public_api.ts b/libs/router/src/data/helpers/public_api.ts index bdd503e8cb..325190b09d 100644 --- a/libs/router/src/data/helpers/public_api.ts +++ b/libs/router/src/data/helpers/public_api.ts @@ -1 +1 @@ -export { daffRouterNamedViewsCollect } from './collect-data'; +export { daffRouterDataCollect } from './collect-data'; diff --git a/libs/router/src/guards/compose.spec.ts b/libs/router/src/guards/compose.spec.ts new file mode 100644 index 0000000000..61054d56f9 --- /dev/null +++ b/libs/router/src/guards/compose.spec.ts @@ -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; + let blockingGuard1: jasmine.Spy; + let blockingGuard2: jasmine.Spy; + let nonBlockingGuard0: jasmine.Spy; + let nonBlockingGuard1: jasmine.Spy; + let nonBlockingGuard2: jasmine.Spy; + let result: CanActivateFn; + const args = [new ActivatedRouteSnapshot(), {}]; + + 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(); + }); + }); + }); +}); diff --git a/libs/router/src/guards/compose.ts b/libs/router/src/guards/compose.ts new file mode 100644 index 0000000000..c34d3378f8 --- /dev/null +++ b/libs/router/src/guards/compose.ts @@ -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, nonBlockingGuards: Array = []): 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>((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)), + ), + ), + ); +} diff --git a/libs/router/src/guards/public_api.ts b/libs/router/src/guards/public_api.ts new file mode 100644 index 0000000000..ce4e4b8a58 --- /dev/null +++ b/libs/router/src/guards/public_api.ts @@ -0,0 +1 @@ +export * from './compose'; diff --git a/libs/router/src/public_api.ts b/libs/router/src/public_api.ts index e796193338..fd4099862f 100644 --- a/libs/router/src/public_api.ts +++ b/libs/router/src/public_api.ts @@ -1 +1,3 @@ export * from './named-view/public_api'; +export * from './data/public_api'; +export * from './guards/public_api';