Skip to content

Commit

Permalink
feat(server) Add new /invoke/:template endpoint implementation (#746)
Browse files Browse the repository at this point in the history
Signed-off-by: Mehmet Tokgöz <mehmet.tokgoz@hazelcast.com>
  • Loading branch information
mehmettokgoz authored Sep 19, 2022
1 parent b6f6598 commit 62a2f44
Show file tree
Hide file tree
Showing 9 changed files with 4,282 additions and 6,669 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
Expand Down Expand Up @@ -61,6 +61,10 @@ typings/
# Optional npm cache directory
.npm

# VSCode and JetBrains folders
.vscode
.idea

# Optional eslint cache
.eslintcache

Expand Down
10,715 changes: 4,053 additions & 6,662 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/cicero-server/.eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ env:
mocha: true
extends: 'eslint:recommended'
parserOptions:
ecmaVersion: 8
ecmaVersion: 2020
sourceType: 'script'
rules:
indent:
Expand Down
3 changes: 3 additions & 0 deletions packages/cicero-server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jspm_packages/
# Typescript v1 declaration files
typings/

# Optional VS Code debug folder
.vscode

# Optional npm cache directory
.npm

Expand Down
126 changes: 123 additions & 3 deletions packages/cicero-server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

'use strict';

const fs = require('fs');

const app = require('express')();
const bodyParser = require('body-parser');
const Template = require('@accordproject/cicero-core').Template;
Expand All @@ -34,6 +36,15 @@ app.use(bodyParser.json());
// set the port for Express
app.set('port', PORT);

/** @template [T=object] */
class MissingArgumentError extends Error {
/** @param {T} message Error message */
constructor(message) {
super(message);
this.name = 'MissingArgumentError';
}
}

/**
* Handle POST requests to /trigger/:template
* The clause is created using the template and the data.
Expand Down Expand Up @@ -162,13 +173,122 @@ app.post('/draft/:template', async function (req, httpResponse, next) {
}
});

/**
* Handle POST requests to /invoke/:template
* The body of the POST should contain the params, data and state.
* The clause is created using the template and the data.
* The call returns the output of requested clause.
*
* Template
* ----------
* The template parameter is the name of a directory under CICERO_DIR that contains
* the template to use.
*
* Request
* ----------
* The POST body contains six properties:
* - sample or data
* - parameters
* - clause name
* - state
* - currentTime
* - utcOffset
*
* Response
* ----------
* Output of the given clause from contract
*
*/
app.post('/invoke/:template', async function(req, httpResponse, next) {

try {
const options = req.body.options ?? {};
const currentTime = req.body.currentTime ?? new Date().toISOString();
const utcOffset = req.body.utcOffset ?? new Date().getTimezoneOffset();

const engine = new Engine();
const clause = await initTemplateInstance(req, options);
let clauseName;
let params;
let state;

if (req.body.clauseName) {
clauseName = req.body.clauseName.toString();
} else {
throw new MissingArgumentError('Missing `clauseName` in /invoke body');
}

if (req.body.params) {
params = req.body.params;
} else {
throw new MissingArgumentError('Missing `params` in /invoke body');
}

if (req.body.sample) {
clause.parse(req.body.sample.toString(), currentTime, utcOffset);
} else if (req.body.data) {
clause.setData(req.body.data);
} else {
throw new MissingArgumentError('Missing `sample` or `data` in /invoke body');
}

if(req.body.state) {
state = req.body.state;
} else {
const initResult = await engine.init(clause, currentTime, utcOffset);
state = initResult.state;
}

const result = await engine.invoke(clause, clauseName, params, state, currentTime, utcOffset);
httpResponse.status(200).send(result);
} catch(err) {
if (err.name === 'MissingArgumentError') {
httpResponse.status(422).send({error: err.message});
} else {
httpResponse.status(500).send({error: err.message});
}
}
});

/**
* Helper function to determine whether the template is archived or not
* @param {string} templateName Name of the template
* @returns {boolean} True if the given template is a .cta file
*/
function isTemplateArchive(templateName) {
try {
fs.lstatSync(`${process.env.CICERO_DIR}/${templateName}.cta`).isFile();
return true;
} catch(err) {
return false;
}
}

/**
* Helper function to load a template from disk
* @param {string} templateName Name of the template
* @param {object} options an optional set of options
* @returns {object} The template instance object.
*/
async function loadTemplate(templateName, options) {
if (process.env.CICERO_URL) {
return await Template.fromUrl(`${process.env.CICERO_URL}/${templateName}.cta`, options);
} else if (isTemplateArchive(templateName)) {
const buffer = fs.readFileSync(`${process.env.CICERO_DIR}/${templateName}.cta`);
return await Template.fromArchive(buffer, options);
} else {
return await Template.fromDirectory(`${process.env.CICERO_DIR}/${templateName}`, options);
}
}

/**
* Helper function to initialise the template.
* @param {req} req The request passed in from endpoint.
* @returns {object} The template instance object.
* @param {object} options an optional set of options
* @returns {object} The clause instance object.
*/
async function initTemplateInstance(req) {
const template = await Template.fromDirectory(`${process.env.CICERO_DIR}/${req.params.template}`);
async function initTemplateInstance(req, options) {
const template = await loadTemplate(req.params.template, options);
return new Clause(template);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/cicero-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@
"chai-as-promised": "7.1.1",
"chai-things": "0.2.0",
"decache": "4.4.0",
"eslint": "8.2.0",
"eslint": "^8.23.1",
"jsdoc": "^3.6.10",
"license-check": "1.1.5",
"mocha": "8.3.2",
"mocha": "^8.4.0",
"mockery": "2.0.0",
"nyc": "15.1.0",
"supertest": "3.0.0"
Expand Down
78 changes: 78 additions & 0 deletions packages/cicero-server/test/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ let server;
chai.should();

const body = require('./data/latedeliveryandpenalty/request.json');
const params = require('./data/latedeliveryandpenalty/params.json');
const params_err = require('./data/latedeliveryandpenalty/params_err.json');
const state = require('./data/latedeliveryandpenalty/state.json');
const triggerData = require('./data/latedeliveryandpenalty/data.json');
const responseBody = {
Expand Down Expand Up @@ -255,6 +257,82 @@ describe('cicero-server', () => {
});
});

it('/should invoke a clause from contract with data', async () => {
return request.post('/invoke/latedeliveryandpenalty')
.send({
data: parseBody,
state: state,
params: params,
clauseName: 'latedeliveryandpenalty'
})
.expect(200);
});

it('/should invoke a clause from contract with sample', async () => {
return request.post('/invoke/latedeliveryandpenalty')
.send({
sample: draftLateText,
state: state,
params: params,
clauseName: 'latedeliveryandpenalty'
})
.expect(200);
});

it('/should fail to invoke a with errornous params', async () => {
return request.post('/invoke/latedeliveryandpenalty')
.send({
sample: draftLateText,
state: state,
params: params_err,
clauseName: 'latedeliveryandpenalty'
})
.expect(500);
});

it('/should fail to invoke without clause name', async () => {
return request.post('/invoke/latedeliveryandpenalty')
.send({
data: parseBody,
state: state,
params: params,
})
.expect(422)
.expect('Content-Type',/json/)
.then(response => {
response.body.error.should.equal('Missing `clauseName` in /invoke body');
});
});

it('/should fail to invoke without sample or data', async () => {
return request.post('/invoke/latedeliveryandpenalty')
.send({
state: state,
params: params,
clauseName: 'latedeliveryandpenalty'
})
.expect(422)
.expect('Content-Type',/json/)
.then(response => {
response.body.error.should.equal('Missing `sample` or `data` in /invoke body');
});
});

it('/should fail to invoke without params', async () => {
return request.post('/invoke/latedeliveryandpenalty')
.send({
data: parseBody,
state: state,
clauseName: 'latedeliveryandpenalty'
})
.expect(422)
.expect('Content-Type',/json/)
.then(response => {
response.body.error.should.equal('Missing `params` in /invoke body');
});
});


after(() => {
server.close();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"request" : {
"$class": "org.accordproject.latedeliveryandpenalty.LateDeliveryAndPenaltyRequest",
"forceMajeure": false,
"agreedDelivery": "2017-12-17T03:24:00Z",
"deliveredAt": null,
"goodsValue": 200.00
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"request" : {
"forceMajeure": false,
"agreedDelivery": "December 17, 2017 03:24:00",
"deliveredAt": null,
"goodsValue": 200.00
}
}

0 comments on commit 62a2f44

Please sign in to comment.