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_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"

View File

@ -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<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 { 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<Processor> => [
new ClassicWaypointsCompleter(this._matchQuery),
new RouteCompleter(this._matchQuery, true, true, true),

View File

@ -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> => {
match = async (matchQuery: MatchQuery): Promise<Array<Match>> => {
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<Candidate> = [];
let candidates: Array<Candidate> = 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];
};
}

View File

@ -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<ICollection<Match>> => {
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<Match> = 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));

View File

@ -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: [],
})

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