/
templateinstance.js
263 lines (231 loc) · 9.75 KB
/
templateinstance.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/*
* 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
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const Logger = require('@accordproject/concerto-core').Logger;
const ParseException = require('@accordproject/concerto-core').ParseException;
const crypto = require('crypto');
const ErrorUtil = require('./errorutil');
const Util = require('@accordproject/ergo-compiler').Util;
const moment = require('moment-mini');
// Make sure Moment serialization preserves utcOffset. See https://momentjs.com/docs/#/displaying/as-json/
moment.fn.toJSON = Util.momentToJson;
const TemplateLoader = require('./templateloader');
const ErgoEngine = require('@accordproject/ergo-engine/index.browser.js').EvalEngine;
/**
* A TemplateInstance is an instance of a Clause or Contract template. It is executable business logic, linked to
* a natural language (legally enforceable) template.
* A TemplateInstance must be constructed with a template and then prior to execution the data for the clause must be set.
* Set the data for the TemplateInstance by either calling the setData method or by
* calling the parse method and passing in natural language text that conforms to the template grammar.
* @public
* @abstract
* @class
*/
class TemplateInstance {
/**
* Create the Clause and link it to a Template.
* @param {Template} template - the template for the clause
*/
constructor(template) {
if (this.constructor === TemplateInstance) {
throw new TypeError('Abstract class "TemplateInstance" cannot be instantiated directly.');
}
this.template = template;
this.data = null;
this.concertoData = null;
this.ergoEngine = new ErgoEngine();
}
/**
* Set the data for the clause
* @param {object} data - the data for the clause, must be an instance of the
* template model for the clause's template. This should be a plain JS object
* and will be deserialized and validated into the Concerto object before assignment.
*/
setData(data) {
// verify that data is an instance of the template model
const templateModel = this.getTemplate().getTemplateModel();
if (data.$class !== templateModel.getFullyQualifiedName()) {
throw new Error(`Invalid data, must be a valid instance of the template model ${templateModel.getFullyQualifiedName()} but got: ${JSON.stringify(data)} `);
}
// downloadExternalDependencies the data using the template model
Logger.debug('Setting clause data: ' + JSON.stringify(data));
const resource = this.getTemplate().getSerializer().fromJSON(data);
resource.validate();
// save the data
this.data = data;
// save the concerto data
this.concertoData = resource;
}
/**
* Get the data for the clause. This is a plain JS object. To retrieve the Concerto
* object call getConcertoData().
* @return {object} - the data for the clause, or null if it has not been set
*/
getData() {
return this.data;
}
/**
* Get the current Ergo engine
* @return {object} - the data for the clause, or null if it has not been set
*/
getEngine() {
return this.ergoEngine;
}
/**
* Get the data for the clause. This is a Concerto object. To retrieve the
* plain JS object suitable for serialization call toJSON() and retrieve the `data` property.
* @return {object} - the data for the clause, or null if it has not been set
*/
getDataAsConcertoObject() {
return this.concertoData;
}
/**
* Set the data for the clause by parsing natural language text.
* @param {string} input - the text for the clause
* @param {string} [currentTime] - the definition of 'now' (optional)
* @param {string} [fileName] - the fileName for the text (optional)
*/
parse(input, currentTime, fileName) {
let text = TemplateLoader.normalizeText(input);
// Roundtrip the sample through the Commonmark parser
text = this.getTemplate().getParserManager().roundtripMarkdown(text);
// Set the current time and UTC Offset
const now = Util.setCurrentTime(currentTime);
const utcOffset = now.utcOffset();
let parser = this.getTemplate().getParserManager().getParser();
try {
parser.feed(text);
} catch(err) {
const fileLocation = ErrorUtil.locationOfError(err);
throw new ParseException(err.message, fileLocation, fileName, err.message, 'cicero-core');
}
if (parser.results.length !== 1) {
const head = JSON.stringify(parser.results[0]);
parser.results.forEach(function (element) {
if (head !== JSON.stringify(element)) {
const err = `Ambiguous text. Got ${parser.results.length} ASTs for text: ${text}`;
throw new ParseException(err, null, fileName, err, 'cicero-core' );
}
}, this);
}
let ast = parser.results[0];
Logger.debug('Result of parsing: ' + JSON.stringify(ast));
if(!ast) {
const err = 'Parsing clause text returned a null AST. This may mean the text is valid, but not complete.';
throw new ParseException(err, null, fileName, err, 'cicero-core' );
}
ast = TemplateInstance.convertDateTimes(ast, utcOffset);
this.setData(ast);
}
/**
* Recursive function that converts all instances of ParsedDateTime
* to a Moment.
* @param {*} obj the input object
* @param {number} utcOffset - the default utcOffset
* @returns {*} the converted object
*/
static convertDateTimes(obj, utcOffset) {
if(obj.$class === 'ParsedDateTime') {
let instance = null;
if(obj.timezone) {
instance = moment(obj).utcOffset(obj.timezone, true);
}
else {
instance = moment(obj)
.utcOffset(utcOffset, true);
}
if(!instance) {
throw new Error(`Failed to handle datetime ${JSON.stringify(obj, null, 4)}`);
}
const result = instance.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
if(result === 'Invalid date') {
throw new Error(`Failed to handle datetime ${JSON.stringify(obj, null, 4)}`);
}
return result;
}
else if( typeof obj === 'object' && obj !== null) {
Object.entries(obj).forEach(
([key, value]) => {obj[key] = TemplateInstance.convertDateTimes(value, utcOffset);}
);
}
return obj;
}
/**
* Generates the natural language text for a contract or clause clause; combining the text from the template
* and the instance data.
* @param {*} [options] text generation options. options.wrapVariables encloses variables
* and editable sections in '<variable ...' and '/>'
* @param {string} currentTime - the definition of 'now' (optional)
* @returns {string} the natural language text for the contract or clause; created by combining the structure of
* the template with the JSON data for the clause.
*/
async draft(options, currentTime) {
if(!this.concertoData) {
throw new Error('Data has not been set. Call setData or parse before calling this method.');
}
const markdownOptions = {
'$class': 'org.accordproject.markdown.MarkdownOptions',
'wrapVariables': options && options.wrapVariables ? options.wrapVariables : false,
'template': true
};
const logicManager = this.getLogicManager();
const clauseId = this.getIdentifier();
const contract = this.getData();
return logicManager.compileLogic(false).then(async () => {
const result = await this.getEngine().draft(logicManager,clauseId,contract,{},currentTime,markdownOptions);
// Roundtrip the response through the Commonmark parser
return this.getTemplate().getParserManager().roundtripMarkdown(result.response);
});
}
/**
* Returns the identifier for this clause. The identifier is the identifier of
* the template plus '-' plus a hash of the data for the clause (if set).
* @return {String} the identifier of this clause
*/
getIdentifier() {
let hash = '';
if (this.data) {
const textToHash = JSON.stringify(this.getData());
const hasher = crypto.createHash('sha256');
hasher.update(textToHash);
hash = '-' + hasher.digest('hex');
}
return this.getTemplate().getIdentifier() + hash;
}
/**
* Returns the template for this clause
* @return {Template} the template for this clause
*/
getTemplate() {
return this.template;
}
/**
* Returns the template logic for this clause
* @return {LogicManager} the template for this clause
*/
getLogicManager() {
return this.template.getLogicManager();
}
/**
* Returns a JSON representation of the clause
* @return {object} the JS object for serialization
*/
toJSON() {
return {
template: this.getTemplate().getIdentifier(),
data: this.getData()
};
}
}
module.exports = TemplateInstance;