UNPKG

21.3 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
15'use strict';
16
17const ClassDeclaration = require('../introspect/classdeclaration');
18const Field = require('../introspect/field');
19const RelationshipDeclaration = require('../introspect/relationshipdeclaration');
20const EnumDeclaration = require('../introspect/enumdeclaration');
21const Relationship = require('../model/relationship');
22const Resource = require('../model/resource');
23const Concept = require('../model/concept');
24const Identifiable = require('../model/identifiable');
25const Util = require('../util');
26const ModelUtil = require('../modelutil');
27const ValidationException = require('./validationexception');
28const Globalize = require('../globalize');
29const Moment = require('moment-mini');
30
31/**
32 * <p>
33 * Validates a Resource or Field against the models defined in the ModelManager.
34 * This class is used with the Visitor pattern and visits the class declarations
35 * (etc) for the model, checking that the data in a Resource / Field is consistent
36 * with the model.
37 * </p>
38 * The parameters for the visit method must contain the following properties:
39 * <ul>
40 * <li> 'stack' - the TypedStack of objects being processed. It should
41 * start as [Resource] or [Field]</li>
42 * <li> 'rootResourceIdentifier' - the identifier of the resource being validated </li>
43 * <li> 'modelManager' - the ModelManager instance to use for type checking</li>
44 * </ul>
45 * @private
46 * @class
47 * @memberof module:concerto-core
48 */
49class ResourceValidator {
50
51 /**
52 * ResourceValidator constructor
53 * @param {Object} options - the optional serialization options.
54 * @param {boolean} options.validate - validate the structure of the Resource
55 * with its model prior to serialization (default to true)
56 * @param {boolean} options.convertResourcesToRelationships - Convert resources that
57 * are specified for relationship fields into relationships, false by default.
58 * @param {boolean} options.permitResourcesForRelationships - Permit resources in the
59 */
60 constructor(options) {
61 this.options = options || {};
62 }
63 /**
64 * Visitor design pattern.
65 *
66 * @param {Object} thing - the object being visited
67 * @param {Object} parameters - the parameter
68 * @return {Object} the result of visiting or null
69 * @private
70 */
71 visit(thing, parameters) {
72 if (thing instanceof EnumDeclaration) {
73 return this.visitEnumDeclaration(thing, parameters);
74 } else if (thing instanceof ClassDeclaration) {
75 return this.visitClassDeclaration(thing, parameters);
76 } else if (thing instanceof RelationshipDeclaration) {
77 return this.visitRelationshipDeclaration(thing, parameters);
78 } else if (thing instanceof Field) {
79 return this.visitField(thing, parameters);
80 }
81 }
82
83 /**
84 * Visitor design pattern
85 *
86 * @param {EnumDeclaration} enumDeclaration - the object being visited
87 * @param {Object} parameters - the parameter
88 * @return {Object} the result of visiting or null
89 * @private
90 */
91 visitEnumDeclaration(enumDeclaration, parameters) {
92 const obj = parameters.stack.pop();
93
94 // now check that obj is one of the enum values
95 const properties = enumDeclaration.getProperties();
96 let found = false;
97 for(let n=0; n < properties.length; n++) {
98 const property = properties[n];
99 if(property.getName() === obj) {
100 found = true;
101 }
102 }
103
104 if(!found) {
105 ResourceValidator.reportInvalidEnumValue( parameters.rootResourceIdentifier, enumDeclaration, obj );
106 }
107
108 return null;
109 }
110
111 /**
112 * Visitor design pattern
113 * @param {ClassDeclaration} classDeclaration - the object being visited
114 * @param {Object} parameters - the parameter
115 * @return {Object} the result of visiting or null
116 * @private
117 */
118 visitClassDeclaration(classDeclaration, parameters) {
119
120 const obj = parameters.stack.pop();
121
122 // are we dealing with a Resouce?
123 if(!((obj instanceof Resource) || (obj instanceof Concept))) {
124 ResourceValidator.reportNotResouceViolation(parameters.rootResourceIdentifier, classDeclaration, obj );
125 }
126
127 if(obj instanceof Identifiable) {
128 parameters.rootResourceIdentifier = obj.getFullyQualifiedIdentifier();
129 }
130
131 const toBeAssignedClassDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType());
132 const toBeAssignedClassDecName = toBeAssignedClassDeclaration.getFullyQualifiedName();
133
134 // is the type we are assigning to abstract?
135 // the only way this can happen is if the type is non-abstract
136 // and then gets redeclared as abstract
137 if(toBeAssignedClassDeclaration.isAbstract()) {
138 ResourceValidator.reportAbstractClass(toBeAssignedClassDeclaration);
139 }
140
141 // are there extra fields in the object?
142 let props = Object.getOwnPropertyNames(obj);
143 for (let n = 0; n < props.length; n++) {
144 let propName = props[n];
145 if(!this.isSystemProperty(propName)) {
146 const field = toBeAssignedClassDeclaration.getProperty(propName);
147 if (!field) {
148 if(obj instanceof Identifiable) {
149 ResourceValidator.reportUndeclaredField(obj.getIdentifier(), propName, toBeAssignedClassDecName);
150 }
151 else {
152 ResourceValidator.reportUndeclaredField(parameters.currentIdentifier, propName, toBeAssignedClassDecName);
153 }
154 }
155 }
156 }
157
158 if(obj instanceof Identifiable) {
159 const id = obj.getIdentifier();
160
161 // prevent empty identifiers
162 if(!id || id.trim().length === 0) {
163 ResourceValidator.reportEmptyIdentifier(parameters.rootResourceIdentifier);
164 }
165
166 parameters.currentIdentifier = obj.getFullyQualifiedIdentifier();
167 }
168
169 // now validate each property
170 const properties = toBeAssignedClassDeclaration.getProperties();
171 for(let n=0; n < properties.length; n++) {
172 const property = properties[n];
173 const value = obj[property.getName()];
174 if(!Util.isNull(value)) {
175 parameters.stack.push(value);
176 property.accept(this,parameters);
177 }
178 else {
179 if(!property.isOptional()) {
180 ResourceValidator.reportMissingRequiredProperty( parameters.rootResourceIdentifier, property);
181 }
182 }
183 }
184 return null;
185 }
186
187 /**
188 * Returns true if the property is a system property.
189 * System properties are not declared in the model.
190 * @param {String} propertyName - the name of the property
191 * @return {Boolean} true if the property is a system property
192 * @private
193 */
194 isSystemProperty(propertyName) {
195 return propertyName.charAt(0) === '$';
196 }
197
198 /**
199 * Visitor design pattern
200 * @param {Field} field - the object being visited
201 * @param {Object} parameters - the parameter
202 * @return {Object} the result of visiting or null
203 * @private
204 */
205 visitField(field, parameters) {
206 const obj = parameters.stack.pop();
207
208 let dataType = typeof(obj);
209 let propName = field.getName();
210
211 if (dataType === 'undefined' || dataType === 'symbol') {
212 ResourceValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field);
213 }
214
215 if(field.isTypeEnum()) {
216 this.checkEnum(obj, field,parameters);
217 }
218 else {
219 if(field.isArray()) {
220 this.checkArray(obj, field,parameters);
221 }
222 else {
223 this.checkItem(obj, field,parameters);
224 }
225 }
226
227 return null;
228 }
229
230 /**
231 * Check a Field that is declared as an Array.
232 * @param {Object} obj - the object being validated
233 * @param {Field} field - the object being visited
234 * @param {Object} parameters - the parameter
235 * @private
236 */
237 checkEnum(obj,field,parameters) {
238
239 if(field.isArray() && !(obj instanceof Array)) {
240 ResourceValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, field.getName(), obj, field);
241 }
242
243 const enumDeclaration = field.getParent().getModelFile().getType(field.getType());
244
245 if(field.isArray()) {
246 for(let n=0; n < obj.length; n++) {
247 const item = obj[n];
248 parameters.stack.push(item);
249 enumDeclaration.accept(this, parameters);
250 }
251 }
252 else {
253 const item = obj;
254 parameters.stack.push(item);
255 enumDeclaration.accept(this, parameters);
256 }
257 }
258
259 /**
260 * Check a Field that is declared as an Array.
261 * @param {Object} obj - the object being validated
262 * @param {Field} field - the object being visited
263 * @param {Object} parameters - the parameter
264 * @private
265 */
266 checkArray(obj,field,parameters) {
267
268 if(!(obj instanceof Array)) {
269 ResourceValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, field.getName(), obj, field);
270 }
271
272 for(let n=0; n < obj.length; n++) {
273 const item = obj[n];
274 this.checkItem(item, field, parameters);
275 }
276 }
277
278 /**
279 * Check a single (non-array) field.
280 * @param {Object} obj - the object being validated
281 * @param {Field} field - the object being visited
282 * @param {Object} parameters - the parameter
283 * @private
284 */
285 checkItem(obj,field, parameters) {
286 let dataType = typeof obj;
287 let propName = field.getName();
288
289 if (dataType === 'undefined' || dataType === 'symbol') {
290 ResourceValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field);
291 }
292
293 if(field.isPrimitive()) {
294 let invalid = false;
295
296 switch(field.getType()) {
297 case 'String':
298 if(dataType !== 'string') {
299 invalid = true;
300 }
301 break;
302 case 'Double':
303 case 'Long':
304 case 'Integer':
305 if(dataType !== 'number') {
306 invalid = true;
307 }
308 break;
309 case 'Boolean':
310 if(dataType !== 'boolean') {
311 invalid = true;
312 }
313 break;
314 case 'DateTime':
315 if(!(Moment.isMoment(obj))) {
316 invalid = true;
317 }
318 break;
319 }
320 if (invalid) {
321 ResourceValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field);
322 }
323 else {
324 if(field.getValidator() !== null) {
325 field.getValidator().validate(parameters.currentIdentifier, obj);
326 }
327 }
328 }
329 else {
330 // a field that points to a transaction, asset, participant...
331 let classDeclaration = parameters.modelManager.getType(field.getFullyQualifiedTypeName());
332 if(obj instanceof Identifiable) {
333 try {
334 classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType());
335 } catch (err) {
336 ResourceValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field);
337 }
338
339 // is it compatible?
340 if(!ModelUtil.isAssignableTo(classDeclaration.getModelFile(), classDeclaration.getFullyQualifiedName(), field)) {
341 ResourceValidator.reportInvalidFieldAssignment(parameters.rootResourceIdentifier, propName, obj, field);
342 }
343 }
344
345 // recurse
346 parameters.stack.push(obj);
347 classDeclaration.accept(this, parameters);
348 }
349 }
350
351 /**
352 * Visitor design pattern
353 * @param {RelationshipDeclaration} relationshipDeclaration - the object being visited
354 * @param {Object} parameters - the parameter
355 * @return {Object} the result of visiting or null
356 * @private
357 */
358 visitRelationshipDeclaration(relationshipDeclaration, parameters) {
359 const obj = parameters.stack.pop();
360
361 if(relationshipDeclaration.isArray()) {
362 if(!(obj instanceof Array)) {
363 ResourceValidator.reportInvalidFieldAssignment(parameters.rootResourceIdentifier, relationshipDeclaration.getName(), obj, relationshipDeclaration);
364 }
365
366 for(let n=0; n < obj.length; n++) {
367 const item = obj[n];
368 this.checkRelationship(parameters, relationshipDeclaration, item);
369 }
370 }
371 else {
372 this.checkRelationship(parameters, relationshipDeclaration, obj);
373 }
374 return null;
375 }
376
377 /**
378 * Check a single relationship
379 * @param {Object} parameters - the parameter
380 * @param {relationshipDeclaration} relationshipDeclaration - the object being visited
381 * @param {Object} obj - the object being validated
382 * @private
383 */
384 checkRelationship(parameters, relationshipDeclaration, obj) {
385 if(obj instanceof Relationship) {
386 // All good..
387 } else if (obj instanceof Resource && (this.options.convertResourcesToRelationships || this.options.permitResourcesForRelationships)) {
388 // All good.. Again
389 } else {
390 ResourceValidator.reportNotRelationshipViolation(parameters.rootResourceIdentifier, relationshipDeclaration, obj);
391 }
392
393 const relationshipType = parameters.modelManager.getType(obj.getFullyQualifiedType());
394
395 if(relationshipType.isConcept()) {
396 throw new Error('Cannot have a relationship to a concept. Relationships must be to resources.');
397 }
398
399 if(!ModelUtil.isAssignableTo(relationshipType.getModelFile(), obj.getFullyQualifiedType(), relationshipDeclaration)) {
400 ResourceValidator.reportInvalidFieldAssignment(parameters.rootResourceIdentifier, relationshipDeclaration.getName(), obj, relationshipDeclaration);
401 }
402 }
403
404 /**
405 * Throw a new error for a model violation.
406 * @param {string} id - the identifier of this instance.
407 * @param {string} propName - the name of the field.
408 * @param {*} value - the value of the field.
409 * @param {Field} field - the field
410 * @throws {ValidationException} the exception
411 * @private
412 */
413 static reportFieldTypeViolation(id, propName, value, field) {
414 let isArray = field.isArray() ? '[]' : '';
415 let typeOfValue = typeof value;
416
417 if(value instanceof Identifiable) {
418 typeOfValue = value.getFullyQualifiedType();
419 value = value.getFullyQualifiedIdentifier();
420 }
421 else {
422 if(value) {
423 try {
424 value = JSON.stringify(value);
425 }
426 catch(err) {
427 value = value.toString();
428 }
429 }
430 }
431
432 let formatter = Globalize.messageFormatter('resourcevalidator-fieldtypeviolation');
433 throw new ValidationException(formatter({
434 resourceId: id,
435 propertyName: propName,
436 fieldType: field.getType() + isArray,
437 value: value,
438 typeOfValue: typeOfValue
439 }));
440 }
441
442 /**
443 * Throw a new error for a model violation.
444 * @param {string} id - the identifier of this instance.
445 * @param {classDeclaration} classDeclaration - the declaration of the classs
446 * @param {Object} value - the value of the field.
447 * @private
448 */
449 static reportNotResouceViolation(id, classDeclaration, value) {
450 let formatter = Globalize.messageFormatter('resourcevalidator-notresourceorconcept');
451 throw new ValidationException(formatter({
452 resourceId: id,
453 classFQN: classDeclaration.getFullyQualifiedName(),
454 invalidValue: value.toString()
455 }));
456 }
457
458 /**
459 * Throw a new error for a model violation.
460 * @param {string} id - the identifier of this instance.
461 * @param {RelationshipDeclaration} relationshipDeclaration - the declaration of the classs
462 * @param {Object} value - the value of the field.
463 * @private
464 */
465 static reportNotRelationshipViolation(id, relationshipDeclaration, value) {
466 let formatter = Globalize.messageFormatter('resourcevalidator-notrelationship');
467 throw new ValidationException(formatter({
468 resourceId: id,
469 classFQN: relationshipDeclaration.getFullyQualifiedTypeName(),
470 invalidValue: value.toString()
471 }));
472 }
473
474 /**
475 * Throw a new error for a missing, but required field.
476 * @param {string} id - the identifier of this instance.
477 * @param {Field} field - the field/
478 * @private
479 */
480 static reportMissingRequiredProperty(id, field) {
481 let formatter = Globalize.messageFormatter('resourcevalidator-missingrequiredproperty');
482 throw new ValidationException(formatter({
483 resourceId: id,
484 fieldName: field.getName()
485 }));
486 }
487
488 /**
489 * Throw a new error for a missing, but required field.
490 * @param {string} id - the identifier of this instance.
491 * @param {Field} field - the field/
492 * @private
493 */
494 static reportEmptyIdentifier(id) {
495 let formatter = Globalize.messageFormatter('resourcevalidator-emptyidentifier');
496 throw new ValidationException(formatter({
497 resourceId: id
498 }));
499 }
500
501 /**
502 * Throw a new error for a missing, but required field.
503 * @param {string} id - the identifier of this instance.
504 * @param {Field} field - the field
505 * @param {string} obj - the object value
506 * @private
507 */
508 static reportInvalidEnumValue(id, field, obj) {
509 let formatter = Globalize.messageFormatter('resourcevalidator-invalidenumvalue');
510 throw new ValidationException(formatter({
511 resourceId: id,
512 value: obj,
513 fieldName: field.getName()
514 }));
515 }
516
517 /**
518 * Throw a validation exception for an abstract class
519 * @param {ClassDeclaration} classDeclaration - the class declaration
520 * @throws {ValidationException} the validation exception
521 * @private
522 */
523 static reportAbstractClass(classDeclaration) {
524 let formatter = Globalize.messageFormatter('resourcevalidator-abstractclass');
525 throw new ValidationException(formatter({
526 className: classDeclaration.getFullyQualifiedName(),
527 }));
528 }
529
530 /**
531 * Throw a validation exception for an abstract class
532 * @param {string} resourceId - the id of the resouce being validated
533 * @param {string} propertyName - the name of the property that is not declared
534 * @param {string} fullyQualifiedTypeName - the fully qualified type being validated
535 * @throws {ValidationException} the validation exception
536 * @private
537 */
538 static reportUndeclaredField(resourceId, propertyName, fullyQualifiedTypeName ) {
539 let formatter = Globalize.messageFormatter('resourcevalidator-undeclaredfield');
540 throw new ValidationException(formatter({
541 resourceId: resourceId,
542 propertyName: propertyName,
543 fullyQualifiedTypeName: fullyQualifiedTypeName
544 }));
545 }
546
547 /**
548 * Throw a validation exception for an invalid field assignment
549 * @param {string} resourceId - the id of the resouce being validated
550 * @param {string} propName - the name of the property that is being assigned
551 * @param {*} obj - the Field
552 * @param {Field} field - the Field
553 * @throws {ValidationException} the validation exception
554 * @private
555 */
556 static reportInvalidFieldAssignment(resourceId, propName, obj, field) {
557 let formatter = Globalize.messageFormatter('resourcevalidator-invalidfieldassignment');
558 let typeName = field.getFullyQualifiedTypeName();
559
560 if(field.isArray()) {
561 typeName += '[]';
562 }
563
564 throw new ValidationException(formatter({
565 resourceId: resourceId,
566 propertyName: propName,
567 objectType: obj.getFullyQualifiedType(),
568 fieldType: typeName
569 }));
570 }
571}
572
573module.exports = ResourceValidator;