Skip to content

Commit

Permalink
Added ZenEngine implementation, Added decisions API endpoints, Added …
Browse files Browse the repository at this point in the history
…JSON documents API endpoints, Added first dto for decisions API
  • Loading branch information
timwekkenbc committed Jun 3, 2024
1 parent c30d1b8 commit d3c5899
Show file tree
Hide file tree
Showing 13 changed files with 488 additions and 4 deletions.
126 changes: 126 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@gorules/zen-engine": "^0.23.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/mongoose": "^10.0.5",
"@nestjs/platform-express": "^10.3.7",
"axios": "^1.6.8",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"mongoose": "^8.3.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
Expand Down
29 changes: 29 additions & 0 deletions src/api/decisions/decisions.controller.ts
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);
}
}
}
71 changes: 71 additions & 0 deletions src/api/decisions/decisions.service.spec.ts
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',
);
});
});
});
32 changes: 32 additions & 0 deletions src/api/decisions/decisions.service.ts
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}`);
}
}
}
62 changes: 62 additions & 0 deletions src/api/decisions/decsions.controller.spec.ts
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);
});
});
Loading

0 comments on commit d3c5899

Please sign in to comment.