-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added ZenEngine implementation, Added decisions API endpoints, Added …
…JSON documents API endpoints, Added first dto for decisions API
- Loading branch information
1 parent
c30d1b8
commit d3c5899
Showing
13 changed files
with
488 additions
and
4 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Controller, Post, Param, Body, HttpException, HttpStatus } from '@nestjs/common'; | ||
import { DecisionsService } from './decisions.service'; | ||
import { EvaluateDecisionDto, EvaluateDecisionWithContentDto } from './dto/evaluate-decision.dto'; | ||
|
||
@Controller('api/decisions') | ||
export class DecisionsController { | ||
constructor(private readonly decisionsService: DecisionsService) {} | ||
|
||
@Post('/evaluate') | ||
async evaluateDecisionByContent(@Body() { content, context, trace }: EvaluateDecisionWithContentDto) { | ||
try { | ||
return await this.decisionsService.runDecision(content, context, { trace }); | ||
} catch (error) { | ||
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); | ||
} | ||
} | ||
|
||
@Post('/evaluate/:ruleFileName') | ||
async evaluateDecisionByFile( | ||
@Param('ruleFileName') ruleFileName: string, | ||
@Body() { context, trace }: EvaluateDecisionDto, | ||
) { | ||
try { | ||
return await this.decisionsService.runDecisionByFile(ruleFileName, context, { trace }); | ||
} catch (error) { | ||
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { DecisionsService } from './decisions.service'; | ||
import { ZenEngine, ZenDecision, ZenEvaluateOptions } from '@gorules/zen-engine'; | ||
import { readFile } from 'fs/promises'; | ||
|
||
jest.mock('fs/promises'); | ||
|
||
describe('DecisionsService', () => { | ||
let service: DecisionsService; | ||
let mockEngine: Partial<ZenEngine>; | ||
let mockDecision: Partial<ZenDecision>; | ||
|
||
beforeEach(async () => { | ||
mockDecision = { | ||
evaluate: jest.fn(), | ||
}; | ||
mockEngine = { | ||
createDecision: jest.fn().mockReturnValue(mockDecision), | ||
}; | ||
|
||
const module: TestingModule = await Test.createTestingModule({ | ||
providers: [DecisionsService, { provide: ZenEngine, useValue: mockEngine }], | ||
}).compile(); | ||
|
||
service = module.get<DecisionsService>(DecisionsService); | ||
service.engine = mockEngine as ZenEngine; | ||
}); | ||
|
||
describe('runDecision', () => { | ||
it('should run a decision', async () => { | ||
const content = {}; | ||
const context = {}; | ||
const options: ZenEvaluateOptions = { trace: false }; | ||
await service.runDecision(content, context, options); | ||
expect(mockEngine.createDecision).toHaveBeenCalledWith(content); | ||
expect(mockDecision.evaluate).toHaveBeenCalledWith(context, options); | ||
}); | ||
|
||
it('should throw an error if the decision fails', async () => { | ||
const content = {}; | ||
const context = {}; | ||
const options: ZenEvaluateOptions = { trace: false }; | ||
(mockDecision.evaluate as jest.Mock).mockRejectedValue(new Error('Error')); | ||
await expect(service.runDecision(content, context, options)).rejects.toThrow('Failed to run decision: Error'); | ||
}); | ||
}); | ||
|
||
describe('runDecisionByFile', () => { | ||
it('should read a file and run a decision', async () => { | ||
const ruleFileName = 'rule'; | ||
const context = {}; | ||
const options: ZenEvaluateOptions = { trace: false }; | ||
const content = JSON.stringify({ rule: 'rule' }); | ||
(readFile as jest.Mock).mockResolvedValue(content); | ||
await service.runDecisionByFile(ruleFileName, context, options); | ||
expect(readFile).toHaveBeenCalledWith(`src/rules/${ruleFileName}`); | ||
expect(mockEngine.createDecision).toHaveBeenCalledWith(content); | ||
expect(mockDecision.evaluate).toHaveBeenCalledWith(context, options); | ||
}); | ||
|
||
it('should throw an error if reading the file fails', async () => { | ||
const ruleFileName = 'rule'; | ||
const context = {}; | ||
const options: ZenEvaluateOptions = { trace: false }; | ||
(readFile as jest.Mock).mockRejectedValue(new Error('Error')); | ||
await expect(service.runDecisionByFile(ruleFileName, context, options)).rejects.toThrow( | ||
'Failed to run decision: Error', | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { readFile } from 'fs/promises'; | ||
import { Injectable } from '@nestjs/common'; | ||
import { ZenEngine, ZenEvaluateOptions } from '@gorules/zen-engine'; | ||
|
||
const RULES_DIRECTORY = 'src/rules'; | ||
|
||
@Injectable() | ||
export class DecisionsService { | ||
engine: ZenEngine; | ||
|
||
constructor() { | ||
this.engine = new ZenEngine(); | ||
} | ||
|
||
async runDecision(content: object, context: object, options: ZenEvaluateOptions) { | ||
try { | ||
const decision = this.engine.createDecision(content); | ||
return await decision.evaluate(context, options); | ||
} catch (error) { | ||
throw new Error(`Failed to run decision: ${error.message}`); | ||
} | ||
} | ||
|
||
async runDecisionByFile(ruleFileName: string, context: object, options: ZenEvaluateOptions) { | ||
try { | ||
const content = await readFile(`${RULES_DIRECTORY}/${ruleFileName}`); | ||
return this.runDecision(content, context, options); | ||
} catch (error) { | ||
throw new Error(`Failed to run decision: ${error.message}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { HttpException } from '@nestjs/common'; | ||
import { DecisionsController } from './decisions.controller'; | ||
import { DecisionsService } from './decisions.service'; | ||
import { EvaluateDecisionDto, EvaluateDecisionWithContentDto } from './dto/evaluate-decision.dto'; | ||
|
||
describe('DecisionsController', () => { | ||
let controller: DecisionsController; | ||
let service: DecisionsService; | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
controllers: [DecisionsController], | ||
providers: [ | ||
{ | ||
provide: DecisionsService, | ||
useValue: { | ||
runDecision: jest.fn(), | ||
runDecisionByFile: jest.fn(), | ||
}, | ||
}, | ||
], | ||
}).compile(); | ||
|
||
controller = module.get<DecisionsController>(DecisionsController); | ||
service = module.get<DecisionsService>(DecisionsService); | ||
}); | ||
|
||
it('should call runDecision with correct parameters', async () => { | ||
const dto: EvaluateDecisionWithContentDto = { | ||
content: { value: 'content' }, | ||
context: { value: 'context' }, | ||
trace: false, | ||
}; | ||
await controller.evaluateDecisionByContent(dto); | ||
expect(service.runDecision).toHaveBeenCalledWith(dto.content, dto.context, { trace: dto.trace }); | ||
}); | ||
|
||
it('should throw an error when runDecision fails', async () => { | ||
const dto: EvaluateDecisionWithContentDto = { | ||
content: { value: 'content' }, | ||
context: { value: 'context' }, | ||
trace: false, | ||
}; | ||
(service.runDecision as jest.Mock).mockRejectedValue(new Error('Error')); | ||
await expect(controller.evaluateDecisionByContent(dto)).rejects.toThrow(HttpException); | ||
}); | ||
|
||
it('should call runDecisionByFile with correct parameters', async () => { | ||
const dto: EvaluateDecisionDto = { context: { value: 'context' }, trace: false }; | ||
const ruleFileName = 'rule'; | ||
await controller.evaluateDecisionByFile(ruleFileName, dto); | ||
expect(service.runDecisionByFile).toHaveBeenCalledWith(ruleFileName, dto.context, { trace: dto.trace }); | ||
}); | ||
|
||
it('should throw an error when runDecisionByFile fails', async () => { | ||
const dto: EvaluateDecisionDto = { context: { value: 'context' }, trace: false }; | ||
const ruleFileName = 'rule'; | ||
(service.runDecisionByFile as jest.Mock).mockRejectedValue(new Error('Error')); | ||
await expect(controller.evaluateDecisionByFile(ruleFileName, dto)).rejects.toThrow(HttpException); | ||
}); | ||
}); |
Oops, something went wrong.