matcher as injectable
This commit is contained in:
parent
45b33f1ce1
commit
400b26dcc5
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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: [],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue