From 400b26dcc5bb02e1d3ac83b2bd3f547e524a0754 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 21 Apr 2023 16:30:48 +0200 Subject: [PATCH] matcher as injectable --- .env.dist | 3 +- .../factory/algorithm-factory.abstract.ts | 2 + .../domain/entities/engine/factory/classic.ts | 3 + .../matcher/domain/entities/engine/matcher.ts | 21 ++--- .../matcher/domain/usecases/match.usecase.ts | 88 +++++++++---------- src/modules/matcher/matcher.module.ts | 2 + .../tests/unit/domain/engine/matcher.spec.ts | 60 +++++++++++++ .../tests/unit/domain/match.usecase.spec.ts | 62 +++++++++---- 8 files changed, 164 insertions(+), 77 deletions(-) create mode 100644 src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts diff --git a/.env.dist b/.env.dist index 412486b..dc8af99 100644 --- a/.env.dist +++ b/.env.dist @@ -13,7 +13,7 @@ DEFAULT_TIMEZONE=Europe/Paris # default number of seats proposed as driver DEFAULT_SEATS=3 # algorithm type -ALGORITHM=classic +ALGORITHM=CLASSIC # strict algorithm (if relevant with the algorithm type) # if set to true, matches are made so that # punctual ads match only with punctual ads and @@ -38,7 +38,6 @@ VALIDITY_DURATION=365 MAX_DETOUR_DISTANCE_RATIO=0.3 MAX_DETOUR_DURATION_RATIO=0.3 - # PRISMA DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher" diff --git a/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts index e934f1d..6c9c1eb 100644 --- a/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts @@ -1,6 +1,7 @@ import { MatchQuery } from 'src/modules/matcher/queries/match.query'; import { Processor } from '../processor/processor.abstract'; import { Candidate } from '../candidate'; +import { Selector } from '../selector/selector.abstract'; export abstract class AlgorithmFactory { _matchQuery: MatchQuery; @@ -11,5 +12,6 @@ export abstract class AlgorithmFactory { this._candidates = []; } + abstract createSelector(): Selector; abstract createProcessors(): Array; } diff --git a/src/modules/matcher/domain/entities/engine/factory/classic.ts b/src/modules/matcher/domain/entities/engine/factory/classic.ts index 688f8f1..d5fdcbe 100644 --- a/src/modules/matcher/domain/entities/engine/factory/classic.ts +++ b/src/modules/matcher/domain/entities/engine/factory/classic.ts @@ -5,8 +5,11 @@ import { ClassicGeoFilter } from '../processor/filter/geofilter/classic.filter.p import { JourneyCompleter } from '../processor/completer/journey.completer.processor'; import { ClassicTimeFilter } from '../processor/filter/timefilter/classic.filter.processor'; import { Processor } from '../processor/processor.abstract'; +import { Selector } from '../selector/selector.abstract'; +import { ClassicSelector } from '../selector/classic.selector'; export class ClassicAlgorithmFactory extends AlgorithmFactory { + createSelector = (): Selector => new ClassicSelector(this._matchQuery); createProcessors = (): Array => [ new ClassicWaypointsCompleter(this._matchQuery), new RouteCompleter(this._matchQuery, true, true, true), diff --git a/src/modules/matcher/domain/entities/engine/matcher.ts b/src/modules/matcher/domain/entities/engine/matcher.ts index 68b59cb..9bce7f8 100644 --- a/src/modules/matcher/domain/entities/engine/matcher.ts +++ b/src/modules/matcher/domain/entities/engine/matcher.ts @@ -1,7 +1,4 @@ -import { - MatcherException, - MatcherExceptionCode, -} from 'src/modules/matcher/exceptions/matcher.exception'; +import { Injectable } from '@nestjs/common'; import { MatchQuery } from '../../../queries/match.query'; import { Algorithm } from '../../types/algorithm.enum'; import { Match } from '../ecosystem/match'; @@ -9,23 +6,23 @@ import { Candidate } from './candidate'; import { AlgorithmFactory } from './factory/algorithm-factory.abstract'; import { ClassicAlgorithmFactory } from './factory/classic'; +@Injectable() export class Matcher { - match = (matchQuery: MatchQuery): Array => { + match = async (matchQuery: MatchQuery): Promise> => { let algorithm: AlgorithmFactory; switch (matchQuery.algorithmSettings.algorithm) { case Algorithm.CLASSIC: algorithm = new ClassicAlgorithmFactory(matchQuery); break; - default: - throw new MatcherException( - MatcherExceptionCode.INVALID_ARGUMENT, - 'Unknown algorithm', - ); } - let candidates: Array = []; + let candidates: Array = await algorithm + .createSelector() + .select(); for (const processor of algorithm.createProcessors()) { candidates = processor.execute(candidates); } - return []; + const match = new Match(); + match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; + return [match]; }; } diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index 44ce17b..fbb6952 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -3,64 +3,25 @@ import { InjectMapper } from '@automapper/nestjs'; import { QueryHandler } from '@nestjs/cqrs'; import { Messager } from '../../adapters/secondaries/messager'; import { MatchQuery } from '../../queries/match.query'; -import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { Match } from '../entities/ecosystem/match'; import { ICollection } from '../../../database/src/interfaces/collection.interface'; +import { Matcher } from '../entities/engine/matcher'; @QueryHandler(MatchQuery) export class MatchUseCase { constructor( - private readonly _repository: AdRepository, + private readonly _matcher: Matcher, private readonly _messager: Messager, @InjectMapper() private readonly _mapper: Mapper, ) {} execute = async (matchQuery: MatchQuery): Promise> => { try { - // const paths = []; - // for (let i = 0; i < 1; i++) { - // paths.push({ - // key: 'route' + i, - // points: [ - // { - // lat: 48.110899, - // lon: -1.68365, - // }, - // { - // lat: 48.131105, - // lon: -1.690067, - // }, - // { - // lat: 48.534769, - // lon: -1.894032, - // }, - // { - // lat: 48.56516, - // lon: -1.923553, - // }, - // { - // lat: 48.622813, - // lon: -1.997177, - // }, - // { - // lat: 48.67846, - // lon: -1.8554, - // }, - // ], - // }); - // } - // const routes = await matchQuery.algorithmSettings.georouter.route(paths, { - // withDistance: false, - // withPoints: true, - // withTime: true, - // }); - // routes.map((route) => console.log(route.route.spacetimePoints)); - const match = new Match(); - match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; + const data: Array = await this._matcher.match(matchQuery); this._messager.publish('matcher.match', 'match !'); return { - data: [match], - total: 1, + data, + total: data.length, }; } catch (error) { const err: Error = error; @@ -75,3 +36,42 @@ export class MatchUseCase { } }; } + +// const paths = []; +// for (let i = 0; i < 1; i++) { +// paths.push({ +// key: 'route' + i, +// points: [ +// { +// lat: 48.110899, +// lon: -1.68365, +// }, +// { +// lat: 48.131105, +// lon: -1.690067, +// }, +// { +// lat: 48.534769, +// lon: -1.894032, +// }, +// { +// lat: 48.56516, +// lon: -1.923553, +// }, +// { +// lat: 48.622813, +// lon: -1.997177, +// }, +// { +// lat: 48.67846, +// lon: -1.8554, +// }, +// ], +// }); +// } +// const routes = await matchQuery.algorithmSettings.georouter.route(paths, { +// withDistance: false, +// withPoints: true, +// withTime: true, +// }); +// routes.map((route) => console.log(route.route.spacetimePoints)); diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index 7173746..4ceca6e 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -15,6 +15,7 @@ import { DefaultParamsProvider } from './adapters/secondaries/default-params.pro import { GeorouterCreator } from './adapters/secondaries/georouter-creator'; import { HttpModule } from '@nestjs/axios'; import { MatcherGeodesic } from './adapters/secondaries/geodesic'; +import { Matcher } from './domain/entities/engine/matcher'; @Module({ imports: [ @@ -57,6 +58,7 @@ import { MatcherGeodesic } from './adapters/secondaries/geodesic'; MatchUseCase, GeorouterCreator, MatcherGeodesic, + Matcher, ], exports: [], }) diff --git a/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts new file mode 100644 index 0000000..3a38ee8 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts @@ -0,0 +1,60 @@ +import { Algorithm } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Matcher } from '../../../../domain/entities/engine/matcher'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: Algorithm.CLASSIC, + strict: false, + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + georouterType: 'graphhopper', + georouterUrl: 'http://localhost', + }, +}; + +const matchRequest: MatchRequest = new MatchRequest(); +matchRequest.departure = '2023-04-01 12:00'; +matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('Matcher', () => { + it('should be defined', () => { + expect(new Matcher()).toBeDefined(); + }); + + it('should return matches', async () => { + const matcher = new Matcher(); + const matches = await matcher.match(matchQuery); + expect(matches.length).toBe(1); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts index 6de7ad9..d3d2b8c 100644 --- a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -3,13 +3,28 @@ import { Messager } from '../../../adapters/secondaries/messager'; import { MatchUseCase } from '../../../domain/usecases/match.usecase'; import { MatchRequest } from '../../../domain/dtos/match.request'; import { MatchQuery } from '../../../queries/match.query'; -import { AdRepository } from '../../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; import { IDefaultParams } from '../../../domain/types/default-params.type'; import { Algorithm } from '../../../domain/types/algorithm.enum'; +import { Matcher } from '../../../domain/entities/engine/matcher'; +import { Match } from '../../../domain/entities/ecosystem/match'; +import { + MatcherException, + MatcherExceptionCode, +} from '../../../exceptions/matcher.exception'; -const mockAdRepository = {}; +const mockMatcher = { + match: jest + .fn() + .mockImplementationOnce(() => [new Match(), new Match(), new Match()]) + .mockImplementationOnce(() => { + throw new MatcherException( + MatcherExceptionCode.INTERNAL, + 'Something terrible happened !', + ); + }), +}; const mockMessager = { publish: jest.fn().mockImplementation(), @@ -40,6 +55,19 @@ const defaultParams: IDefaultParams = { }, }; +const matchRequest: MatchRequest = new MatchRequest(); +matchRequest.waypoints = [ + { + lon: 1.093912, + lat: 49.440041, + }, + { + lat: 50.630992, + lon: 3.045432, + }, +]; +matchRequest.departure = '2023-04-01 12:23:00'; + describe('MatchUseCase', () => { let matchUseCase: MatchUseCase; @@ -47,14 +75,14 @@ describe('MatchUseCase', () => { const module: TestingModule = await Test.createTestingModule({ imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], providers: [ - { - provide: AdRepository, - useValue: mockAdRepository, - }, { provide: Messager, useValue: mockMessager, }, + { + provide: Matcher, + useValue: mockMatcher, + }, MatchUseCase, ], }).compile(); @@ -68,22 +96,18 @@ describe('MatchUseCase', () => { describe('execute', () => { it('should return matches', async () => { - const matchRequest: MatchRequest = new MatchRequest(); - matchRequest.waypoints = [ - { - lon: 1.093912, - lat: 49.440041, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ]; - matchRequest.departure = '2023-04-01 12:23:00'; const matches = await matchUseCase.execute( new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), ); - expect(matches.total).toBe(1); + expect(matches.total).toBe(3); + }); + + it('should throw an exception when error occurs', async () => { + await expect( + matchUseCase.execute( + new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), + ), + ).rejects.toBeInstanceOf(MatcherException); }); }); });