matcher as injectable

This commit is contained in:
sbriat 2023-04-21 16:30:48 +02:00
parent 45b33f1ce1
commit 400b26dcc5
8 changed files with 164 additions and 77 deletions

View File

@ -13,7 +13,7 @@ DEFAULT_TIMEZONE=Europe/Paris
# default number of seats proposed as driver # default number of seats proposed as driver
DEFAULT_SEATS=3 DEFAULT_SEATS=3
# algorithm type # algorithm type
ALGORITHM=classic ALGORITHM=CLASSIC
# strict algorithm (if relevant with the algorithm type) # strict algorithm (if relevant with the algorithm type)
# if set to true, matches are made so that # if set to true, matches are made so that
# punctual ads match only with punctual ads and # punctual ads match only with punctual ads and
@ -38,7 +38,6 @@ VALIDITY_DURATION=365
MAX_DETOUR_DISTANCE_RATIO=0.3 MAX_DETOUR_DISTANCE_RATIO=0.3
MAX_DETOUR_DURATION_RATIO=0.3 MAX_DETOUR_DURATION_RATIO=0.3
# PRISMA # PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher" DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"

View File

@ -1,6 +1,7 @@
import { MatchQuery } from 'src/modules/matcher/queries/match.query'; import { MatchQuery } from 'src/modules/matcher/queries/match.query';
import { Processor } from '../processor/processor.abstract'; import { Processor } from '../processor/processor.abstract';
import { Candidate } from '../candidate'; import { Candidate } from '../candidate';
import { Selector } from '../selector/selector.abstract';
export abstract class AlgorithmFactory { export abstract class AlgorithmFactory {
_matchQuery: MatchQuery; _matchQuery: MatchQuery;
@ -11,5 +12,6 @@ export abstract class AlgorithmFactory {
this._candidates = []; this._candidates = [];
} }
abstract createSelector(): Selector;
abstract createProcessors(): Array<Processor>; abstract createProcessors(): Array<Processor>;
} }

View File

@ -5,8 +5,11 @@ import { ClassicGeoFilter } from '../processor/filter/geofilter/classic.filter.p
import { JourneyCompleter } from '../processor/completer/journey.completer.processor'; import { JourneyCompleter } from '../processor/completer/journey.completer.processor';
import { ClassicTimeFilter } from '../processor/filter/timefilter/classic.filter.processor'; import { ClassicTimeFilter } from '../processor/filter/timefilter/classic.filter.processor';
import { Processor } from '../processor/processor.abstract'; import { Processor } from '../processor/processor.abstract';
import { Selector } from '../selector/selector.abstract';
import { ClassicSelector } from '../selector/classic.selector';
export class ClassicAlgorithmFactory extends AlgorithmFactory { export class ClassicAlgorithmFactory extends AlgorithmFactory {
createSelector = (): Selector => new ClassicSelector(this._matchQuery);
createProcessors = (): Array<Processor> => [ createProcessors = (): Array<Processor> => [
new ClassicWaypointsCompleter(this._matchQuery), new ClassicWaypointsCompleter(this._matchQuery),
new RouteCompleter(this._matchQuery, true, true, true), new RouteCompleter(this._matchQuery, true, true, true),

View File

@ -1,7 +1,4 @@
import { import { Injectable } from '@nestjs/common';
MatcherException,
MatcherExceptionCode,
} from 'src/modules/matcher/exceptions/matcher.exception';
import { MatchQuery } from '../../../queries/match.query'; import { MatchQuery } from '../../../queries/match.query';
import { Algorithm } from '../../types/algorithm.enum'; import { Algorithm } from '../../types/algorithm.enum';
import { Match } from '../ecosystem/match'; import { Match } from '../ecosystem/match';
@ -9,23 +6,23 @@ import { Candidate } from './candidate';
import { AlgorithmFactory } from './factory/algorithm-factory.abstract'; import { AlgorithmFactory } from './factory/algorithm-factory.abstract';
import { ClassicAlgorithmFactory } from './factory/classic'; import { ClassicAlgorithmFactory } from './factory/classic';
@Injectable()
export class Matcher { export class Matcher {
match = (matchQuery: MatchQuery): Array<Match> => { match = async (matchQuery: MatchQuery): Promise<Array<Match>> => {
let algorithm: AlgorithmFactory; let algorithm: AlgorithmFactory;
switch (matchQuery.algorithmSettings.algorithm) { switch (matchQuery.algorithmSettings.algorithm) {
case Algorithm.CLASSIC: case Algorithm.CLASSIC:
algorithm = new ClassicAlgorithmFactory(matchQuery); algorithm = new ClassicAlgorithmFactory(matchQuery);
break; break;
default:
throw new MatcherException(
MatcherExceptionCode.INVALID_ARGUMENT,
'Unknown algorithm',
);
} }
let candidates: Array<Candidate> = []; let candidates: Array<Candidate> = await algorithm
.createSelector()
.select();
for (const processor of algorithm.createProcessors()) { for (const processor of algorithm.createProcessors()) {
candidates = processor.execute(candidates); candidates = processor.execute(candidates);
} }
return []; const match = new Match();
match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85';
return [match];
}; };
} }

View File

@ -3,64 +3,25 @@ import { InjectMapper } from '@automapper/nestjs';
import { QueryHandler } from '@nestjs/cqrs'; import { QueryHandler } from '@nestjs/cqrs';
import { Messager } from '../../adapters/secondaries/messager'; import { Messager } from '../../adapters/secondaries/messager';
import { MatchQuery } from '../../queries/match.query'; import { MatchQuery } from '../../queries/match.query';
import { AdRepository } from '../../adapters/secondaries/ad.repository';
import { Match } from '../entities/ecosystem/match'; import { Match } from '../entities/ecosystem/match';
import { ICollection } from '../../../database/src/interfaces/collection.interface'; import { ICollection } from '../../../database/src/interfaces/collection.interface';
import { Matcher } from '../entities/engine/matcher';
@QueryHandler(MatchQuery) @QueryHandler(MatchQuery)
export class MatchUseCase { export class MatchUseCase {
constructor( constructor(
private readonly _repository: AdRepository, private readonly _matcher: Matcher,
private readonly _messager: Messager, private readonly _messager: Messager,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly _mapper: Mapper,
) {} ) {}
execute = async (matchQuery: MatchQuery): Promise<ICollection<Match>> => { execute = async (matchQuery: MatchQuery): Promise<ICollection<Match>> => {
try { try {
// const paths = []; const data: Array<Match> = await this._matcher.match(matchQuery);
// 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';
this._messager.publish('matcher.match', 'match !'); this._messager.publish('matcher.match', 'match !');
return { return {
data: [match], data,
total: 1, total: data.length,
}; };
} catch (error) { } catch (error) {
const err: Error = 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));

View File

@ -15,6 +15,7 @@ import { DefaultParamsProvider } from './adapters/secondaries/default-params.pro
import { GeorouterCreator } from './adapters/secondaries/georouter-creator'; import { GeorouterCreator } from './adapters/secondaries/georouter-creator';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { MatcherGeodesic } from './adapters/secondaries/geodesic'; import { MatcherGeodesic } from './adapters/secondaries/geodesic';
import { Matcher } from './domain/entities/engine/matcher';
@Module({ @Module({
imports: [ imports: [
@ -57,6 +58,7 @@ import { MatcherGeodesic } from './adapters/secondaries/geodesic';
MatchUseCase, MatchUseCase,
GeorouterCreator, GeorouterCreator,
MatcherGeodesic, MatcherGeodesic,
Matcher,
], ],
exports: [], exports: [],
}) })

View File

@ -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);
});
});

View File

@ -3,13 +3,28 @@ import { Messager } from '../../../adapters/secondaries/messager';
import { MatchUseCase } from '../../../domain/usecases/match.usecase'; import { MatchUseCase } from '../../../domain/usecases/match.usecase';
import { MatchRequest } from '../../../domain/dtos/match.request'; import { MatchRequest } from '../../../domain/dtos/match.request';
import { MatchQuery } from '../../../queries/match.query'; import { MatchQuery } from '../../../queries/match.query';
import { AdRepository } from '../../../adapters/secondaries/ad.repository';
import { AutomapperModule } from '@automapper/nestjs'; import { AutomapperModule } from '@automapper/nestjs';
import { classes } from '@automapper/classes'; import { classes } from '@automapper/classes';
import { IDefaultParams } from '../../../domain/types/default-params.type'; import { IDefaultParams } from '../../../domain/types/default-params.type';
import { Algorithm } from '../../../domain/types/algorithm.enum'; 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 = { const mockMessager = {
publish: jest.fn().mockImplementation(), 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', () => { describe('MatchUseCase', () => {
let matchUseCase: MatchUseCase; let matchUseCase: MatchUseCase;
@ -47,14 +75,14 @@ describe('MatchUseCase', () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
providers: [ providers: [
{
provide: AdRepository,
useValue: mockAdRepository,
},
{ {
provide: Messager, provide: Messager,
useValue: mockMessager, useValue: mockMessager,
}, },
{
provide: Matcher,
useValue: mockMatcher,
},
MatchUseCase, MatchUseCase,
], ],
}).compile(); }).compile();
@ -68,22 +96,18 @@ describe('MatchUseCase', () => {
describe('execute', () => { describe('execute', () => {
it('should return matches', async () => { 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( const matches = await matchUseCase.execute(
new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), 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);
}); });
}); });
}); });