1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | 'use strict';
|
16 |
|
17 | const ClassDeclaration = require('../introspect/classdeclaration');
|
18 | const Field = require('../introspect/field');
|
19 | const RelationshipDeclaration = require('../introspect/relationshipdeclaration');
|
20 | const EnumDeclaration = require('../introspect/enumdeclaration');
|
21 | const Relationship = require('../model/relationship');
|
22 | const Resource = require('../model/resource');
|
23 | const Concept = require('../model/concept');
|
24 | const Identifiable = require('../model/identifiable');
|
25 | const Util = require('../util');
|
26 | const ModelUtil = require('../modelutil');
|
27 | const ValidationException = require('./validationexception');
|
28 | const Globalize = require('../globalize');
|
29 | const Moment = require('moment-mini');
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | class ResourceValidator {
|
50 |
|
51 | |
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | constructor(options) {
|
61 | this.options = options || {};
|
62 | }
|
63 | |
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
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 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | visitEnumDeclaration(enumDeclaration, parameters) {
|
92 | const obj = parameters.stack.pop();
|
93 |
|
94 |
|
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 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | visitClassDeclaration(classDeclaration, parameters) {
|
119 |
|
120 | const obj = parameters.stack.pop();
|
121 |
|
122 |
|
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 |
|
135 |
|
136 |
|
137 | if(toBeAssignedClassDeclaration.isAbstract()) {
|
138 | ResourceValidator.reportAbstractClass(toBeAssignedClassDeclaration);
|
139 | }
|
140 |
|
141 |
|
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 |
|
162 | if(!id || id.trim().length === 0) {
|
163 | ResourceValidator.reportEmptyIdentifier(parameters.rootResourceIdentifier);
|
164 | }
|
165 |
|
166 | parameters.currentIdentifier = obj.getFullyQualifiedIdentifier();
|
167 | }
|
168 |
|
169 |
|
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 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 | isSystemProperty(propertyName) {
|
195 | return propertyName.charAt(0) === '$';
|
196 | }
|
197 |
|
198 | |
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
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 |
|
232 |
|
233 |
|
234 |
|
235 |
|
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 |
|
261 |
|
262 |
|
263 |
|
264 |
|
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 |
|
280 |
|
281 |
|
282 |
|
283 |
|
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 |
|
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 |
|
340 | if(!ModelUtil.isAssignableTo(classDeclaration.getModelFile(), classDeclaration.getFullyQualifiedName(), field)) {
|
341 | ResourceValidator.reportInvalidFieldAssignment(parameters.rootResourceIdentifier, propName, obj, field);
|
342 | }
|
343 | }
|
344 |
|
345 |
|
346 | parameters.stack.push(obj);
|
347 | classDeclaration.accept(this, parameters);
|
348 | }
|
349 | }
|
350 |
|
351 | |
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
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 |
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 | checkRelationship(parameters, relationshipDeclaration, obj) {
|
385 | if(obj instanceof Relationship) {
|
386 |
|
387 | } else if (obj instanceof Resource && (this.options.convertResourcesToRelationships || this.options.permitResourcesForRelationships)) {
|
388 |
|
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 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
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 |
|
444 |
|
445 |
|
446 |
|
447 |
|
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 |
|
460 |
|
461 |
|
462 |
|
463 |
|
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 |
|
476 |
|
477 |
|
478 |
|
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 |
|
490 |
|
491 |
|
492 |
|
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 |
|
503 |
|
504 |
|
505 |
|
506 |
|
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 |
|
519 |
|
520 |
|
521 |
|
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 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
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 |
|
549 |
|
550 |
|
551 |
|
552 |
|
553 |
|
554 |
|
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 |
|
573 | module.exports = ResourceValidator;
|