UNPKG

12.9 kBJavaScriptView Raw
1/*
2 * Licensed under the Apache License, Version 2.0 (the "License");
3 * you may not use this file except in compliance with the License.
4 * You may obtain a copy of the License at
5 *
6 * http://www.apache.org/licenses/LICENSE-2.0
7 *
8 * Unless required by applicable law or agreed to in writing, software
9 * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limitations under the License.
13 */
14'use strict';
15
16function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
17
18function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
19
20function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
21
22function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
23
24const Logger = require('@accordproject/ergo-compiler').Logger;
25
26const crypto = require('crypto');
27
28const Util = require('@accordproject/ergo-compiler').Util;
29
30const moment = require('moment-mini'); // Make sure Moment serialization preserves utcOffset. See https://momentjs.com/docs/#/displaying/as-json/
31
32
33moment.fn.toJSON = Util.momentToJson;
34
35const RelationshipDeclaration = require('composer-concerto').RelationshipDeclaration;
36/**
37 * A TemplateInstance is an instance of a Clause or Contract template. It is executable business logic, linked to
38 * a natural language (legally enforceable) template.
39 * A TemplateInstance must be constructed with a template and then prior to execution the data for the clause must be set.
40 * Set the data for the TemplateInstance by either calling the setData method or by
41 * calling the parse method and passing in natural language text that conforms to the template grammar.
42 * @public
43 * @abstract
44 * @class
45 * @memberof module:cicero-core
46 */
47
48
49class TemplateInstance {
50 /**
51 * Create the Clause and link it to a Template.
52 * @param {Template} template - the template for the clause
53 */
54 constructor(template) {
55 if (this.constructor === TemplateInstance) {
56 throw new TypeError('Abstract class "TemplateInstance" cannot be instantiated directly.');
57 }
58
59 this.template = template;
60 this.data = null;
61 this.composerData = null;
62 }
63 /**
64 * Set the data for the clause
65 * @param {object} data - the data for the clause, must be an instance of the
66 * template model for the clause's template. This should be a plain JS object
67 * and will be deserialized and validated into the Composer object before assignment.
68 */
69
70
71 setData(data) {
72 // verify that data is an instance of the template model
73 const templateModel = this.getTemplate().getTemplateModel();
74
75 if (data.$class !== templateModel.getFullyQualifiedName()) {
76 throw new Error("Invalid data, must be a valid instance of the template model ".concat(templateModel.getFullyQualifiedName(), " but got: ").concat(JSON.stringify(data), " "));
77 } // downloadExternalDependencies the data using the template model
78
79
80 Logger.debug('Setting clause data: ' + JSON.stringify(data));
81 const resource = this.getTemplate().getSerializer().fromJSON(data);
82 resource.validate(); // save the data
83
84 this.data = data; // save the composer data
85
86 this.composerData = resource;
87 }
88 /**
89 * Get the data for the clause. This is a plain JS object. To retrieve the Composer
90 * object call getComposerData().
91 * @return {object} - the data for the clause, or null if it has not been set
92 */
93
94
95 getData() {
96 return this.data;
97 }
98 /**
99 * Get the data for the clause. This is a Composer object. To retrieve the
100 * plain JS object suitable for serialization call toJSON() and retrieve the `data` property.
101 * @return {object} - the data for the clause, or null if it has not been set
102 */
103
104
105 getDataAsComposerObject() {
106 return this.composerData;
107 }
108 /**
109 * Set the data for the clause by parsing natural language text.
110 * @param {string} text - the data for the clause
111 * @param {string} currentTime - the definition of 'now' (optional)
112 */
113
114
115 parse(text, currentTime) {
116 // Set the current time and UTC Offset
117 const now = Util.setCurrentTime(currentTime);
118 const utcOffset = now.utcOffset();
119 let parser = this.getTemplate().getParserManager().getParser();
120 parser.feed(text);
121
122 if (parser.results.length !== 1) {
123 const head = JSON.stringify(parser.results[0]);
124 parser.results.forEach(function (element) {
125 if (head !== JSON.stringify(element)) {
126 throw new Error('Ambiguous text. Got ' + parser.results.length + ' ASTs for text: ' + text);
127 }
128 }, this);
129 }
130
131 let ast = parser.results[0];
132 Logger.debug('Result of parsing: ' + JSON.stringify(ast));
133
134 if (!ast) {
135 throw new Error('Parsing clause text returned a null AST. This may mean the text is valid, but not complete.');
136 }
137
138 ast = TemplateInstance.convertDateTimes(ast, utcOffset);
139 this.setData(ast);
140 }
141 /**
142 * Left pads a number
143 * @param {*} n - the number
144 * @param {*} width - the number of chars to pad to
145 * @param {string} z - the pad character
146 * @return {string} the left padded string
147 */
148
149
150 static pad(n, width) {
151 let z = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '0';
152 n = n + '';
153 return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
154 }
155 /**
156 * Recursive function that converts all instances of ParsedDateTime
157 * to a Moment.
158 * @param {*} obj the input object
159 * @param {number} utcOffset - the default utcOffset
160 * @returns {*} the converted object
161 */
162
163
164 static convertDateTimes(obj, utcOffset) {
165 if (obj.$class === 'ParsedDateTime') {
166 let instance = null;
167
168 if (obj.timezone) {
169 instance = moment([obj.years, obj.months, obj.days, obj.hours, obj.minutes, obj.seconds, obj.milliseconds]).utcOffset(obj.timezone, true);
170 } else {
171 instance = moment(obj).utcOffset(utcOffset, true);
172 }
173
174 if (!instance) {
175 throw new Error("Failed to handle datetime ".concat(JSON.stringify(obj, null, 4)));
176 }
177
178 const result = instance.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
179
180 if (result === 'Invalid date') {
181 throw new Error("Failed to handle datetime ".concat(JSON.stringify(obj, null, 4)));
182 }
183
184 return result;
185 } else if (typeof obj === 'object' && obj !== null) {
186 Object.entries(obj).forEach((_ref) => {
187 let _ref2 = _slicedToArray(_ref, 2),
188 key = _ref2[0],
189 value = _ref2[1];
190
191 obj[key] = TemplateInstance.convertDateTimes(value, utcOffset);
192 });
193 }
194
195 return obj;
196 }
197 /**
198 * Generates the natural language text for a clause; combining the text from the template
199 * and the clause data.
200 * @returns {string} the natural language text for the clause; created by combining the structure of
201 * the template with the JSON data for the clause.
202 */
203
204
205 generateText() {
206 if (!this.composerData) {
207 throw new Error('Data has not been set. Call setData or parse before calling this method.');
208 }
209
210 const ast = this.getTemplate().getParserManager().getTemplateAst(); // console.log('AST: ' + JSON.stringify(ast, null, 4));
211
212 let result = '';
213
214 for (let n = 0; n < ast.data.length; n++) {
215 const thing = ast.data[n];
216
217 switch (thing.type) {
218 case 'LastChunk':
219 case 'Chunk':
220 result += thing.value;
221 break;
222
223 case 'BooleanBinding':
224 {
225 const property = this.getTemplate().getTemplateModel().getProperty(thing.fieldName.value);
226
227 if (this.composerData[property.getName()]) {
228 result += thing.string.value.substring(1, thing.string.value.length - 1);
229 }
230 }
231 break;
232
233 case 'FormattedBinding':
234 case 'Binding':
235 {
236 const property = this.getTemplate().getTemplateModel().getProperty(thing.fieldName.value);
237 const value = this.composerData[property.getName()];
238 result += this.convertPropertyToString(property, value, thing.format ? thing.format.value : null);
239 }
240 break;
241
242 default:
243 throw new Error('Unrecognized item: ' + thing.type);
244 }
245 }
246
247 return result;
248 }
249 /**
250 * Converts a composer object to a string
251 * @param {ClassDeclaration} clazz - the composer classdeclaration
252 * @param {object} obj - the instance to convert
253 * @returns {string} the parseable string representation of the object
254 * @private
255 */
256
257
258 convertClassToString(clazz, obj) {
259 const properties = clazz.getProperties();
260 let result = '';
261
262 for (let n = 0; n < properties.length; n++) {
263 const child = properties[n];
264 result += this.convertPropertyToString(child, obj[child.getName()]);
265
266 if (n < properties.length - 1) {
267 result += ' ';
268 }
269 }
270
271 return result;
272 }
273 /**
274 * Returns moment Date object parsed using a format string
275 * @param {Date} date - date/time string to parse
276 * @param {string} format - the optional format string. If not specified
277 * this defaults to 'MM/DD/YYYY'
278 * @returns {string} ISO-8601 formatted date as a string
279 * @private
280 */
281
282
283 static formatDateTime(date, format) {
284 if (!format) {
285 format = 'MM/DD/YYYY';
286 }
287
288 const instance = moment.parseZone(date);
289 return instance.format(format);
290 }
291 /**
292 * Converts a composer object to a string
293 * @param {Property} property - the composer property
294 * @param {object} obj - the instance to convert
295 * @param {string} format - the optional format string to use
296 * @returns {string} the parseable string representation of the object
297 * @private
298 */
299
300
301 convertPropertyToString(property, obj, format) {
302 if (property.isOptional() === false && !obj) {
303 throw new Error("Required property ".concat(property.getFullyQualifiedName(), " is null."));
304 }
305
306 if (property instanceof RelationshipDeclaration) {
307 return "\"".concat(obj.getIdentifier(), "\"");
308 }
309
310 if (property.isTypeEnum()) {
311 return obj;
312 }
313
314 if (format) {
315 // strip quotes
316 format = format.substr(1, format.length - 2);
317 } // uncomment this code when the templates support arrays
318 // if(property.isArray()) {
319 // let result = '';
320 // for(let n=0; n < obj.length; n++) {
321 // result += this.convertPropertyToString(this.getTemplate().getTemplateModel().
322 // getModelFile().getModelManager().getType(property.getFullyQualifiedTypeName()), obj[n] );
323 // if(n < obj.length-1) {
324 // result += ' ';
325 // }
326 // }
327 // return result;
328 // }
329
330
331 switch (property.getFullyQualifiedTypeName()) {
332 case 'String':
333 return "\"".concat(obj, "\"");
334
335 case 'Integer':
336 case 'Long':
337 case 'Double':
338 return obj.toString();
339
340 case 'DateTime':
341 return TemplateInstance.formatDateTime(obj, format);
342
343 case 'Boolean':
344 if (obj) {
345 return 'true';
346 } else {
347 return 'false';
348 }
349
350 default:
351 return this.convertClassToString(this.getTemplate().getTemplateModel().getModelFile().getModelManager().getType(property.getFullyQualifiedTypeName()), obj);
352 }
353 }
354 /**
355 * Returns the identifier for this clause. The identifier is the identifier of
356 * the template plus '-' plus a hash of the data for the clause (if set).
357 * @return {String} the identifier of this clause
358 */
359
360
361 getIdentifier() {
362 let hash = '';
363
364 if (this.data) {
365 const textToHash = JSON.stringify(this.getData());
366 const hasher = crypto.createHash('sha256');
367 hasher.update(textToHash);
368 hash = '-' + hasher.digest('hex');
369 }
370
371 return this.getTemplate().getIdentifier() + hash;
372 }
373 /**
374 * Returns the template for this clause
375 * @return {Template} the template for this clause
376 */
377
378
379 getTemplate() {
380 return this.template;
381 }
382 /**
383 * Returns the template logic for this clause
384 * @return {TemplateLogic} the template for this clause
385 */
386
387
388 getTemplateLogic() {
389 return this.template.getTemplateLogic();
390 }
391 /**
392 * Returns a JSON representation of the clause
393 * @return {object} the JS object for serialization
394 */
395
396
397 toJSON() {
398 return {
399 template: this.getTemplate().getIdentifier(),
400 data: this.getData()
401 };
402 }
403
404}
405
406module.exports = TemplateInstance;
\No newline at end of file