UNPKG

22.7 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 Decorated = require('./decorated');
18const EnumValueDeclaration = require('./enumvaluedeclaration');
19const Field = require('./field');
20const Globalize = require('../globalize');
21const IllegalModelException = require('./illegalmodelexception');
22const Introspector = require('./introspector');
23const ModelUtil = require('../modelutil');
24const RelationshipDeclaration = require('./relationshipdeclaration');
25
26/**
27 * ClassDeclaration defines the structure (model/schema) of composite data.
28 * It is composed of a set of Properties, may have an identifying field, and may
29 * have a super-type.
30 * A ClassDeclaration is conceptually owned by a ModelFile which
31 * defines all the classes that are part of a namespace.
32 *
33 *
34 * @abstract
35 * @class
36 * @memberof module:concerto-core
37 */
38class ClassDeclaration extends Decorated {
39
40 /**
41 * Create a ClassDeclaration from an Abstract Syntax Tree. The AST is the
42 * result of parsing.
43 *
44 * @param {ModelFile} modelFile - the ModelFile for this class
45 * @param {string} ast - the AST created by the parser
46 * @throws {IllegalModelException}
47 */
48 constructor(modelFile, ast) {
49 super(modelFile, ast);
50 this.process();
51 this.fqn = ModelUtil.getFullyQualifiedName(this.modelFile.getNamespace(), this.name);
52 this._isClassDeclaration = true;
53 }
54
55 /**
56 * Process the AST and build the model
57 *
58 * @throws {IllegalModelException}
59 * @private
60 */
61 process() {
62
63 super.process();
64
65 this.name = this.ast.id.name;
66 this.properties = [];
67 this.superType = null;
68 this.superTypeDeclaration = null;
69 this.idField = null;
70 this.abstract = false;
71
72 if (this.ast.abstract) {
73 this.abstract = true;
74 }
75
76 if (this.ast.classExtension) {
77 this.superType = this.ast.classExtension.class.name;
78 } else {
79 // if we are not a system type, then we should set the
80 // super type to the system type for this class declaration
81 if (!this.isSystemCoreType()) {
82 this.superType = this.getSystemType();
83 }
84 }
85
86 if (this.ast.idField) {
87 this.idField = this.ast.idField.name;
88 }
89
90 for (let n = 0; n < this.ast.body.declarations.length; n++) {
91 let thing = this.ast.body.declarations[n];
92
93 if (thing.type === 'FieldDeclaration') {
94 this.properties.push(new Field(this, thing));
95 } else if (thing.type === 'RelationshipDeclaration') {
96 this.properties.push(new RelationshipDeclaration(this, thing));
97 } else if (thing.type === 'EnumPropertyDeclaration') {
98 this.properties.push(new EnumValueDeclaration(this, thing));
99 } else {
100 let formatter = Globalize.messageFormatter('classdeclaration-process-unrecmodelelem');
101 throw new IllegalModelException(formatter({
102 'type': thing.type
103 }), this.modelFile, this.ast.location);
104 }
105 }
106 }
107
108 /**
109 * Adds a required field named 'timestamp' of type 'DateTime' if this class declaration does not have a super type.
110 * This method should only be called by system code.
111 * @private
112 */
113 addTimestampField() {
114 // add a timestamp field
115 if(this.superType === null) {
116 const definition = {};
117 definition.id = {};
118 definition.id.name = 'timestamp';
119 definition.propertyType = {};
120 definition.propertyType.name = 'DateTime';
121 this.properties.push(new Field(this, definition));
122 }
123 }
124
125 /**
126 * Resolve the super type on this class and store it as an internal property.
127 * @return {ClassDeclaration} The super type, or null if non specified.
128 */
129 _resolveSuperType() {
130 if (!this.superType) {
131 return null;
132 }
133 // Clear out any old resolved super types.
134 this.superTypeDeclaration = null;
135 let classDecl = null;
136 if (this.getModelFile().isImportedType(this.superType)) {
137 let fqnSuper = this.getModelFile().resolveImport(this.superType);
138 classDecl = this.modelFile.getModelManager().getType(fqnSuper);
139 } else {
140 classDecl = this.getModelFile().getType(this.superType);
141 }
142
143 if (!classDecl) {
144 throw new IllegalModelException('Could not find super type ' + this.superType, this.modelFile, this.ast.location);
145 }
146
147 // Prevent extending declaration with different type of declaration
148 if (this.constructor.name !== classDecl.constructor.name) {
149 let typeName = this.getSystemType();
150 let superTypeName = classDecl.getSystemType();
151 throw new IllegalModelException(`${typeName} (${this.getName()}) cannot extend ${superTypeName} (${classDecl.getName()})`, this.modelFile, this.ast.location);
152 }
153 this.superTypeDeclaration = classDecl;
154 return classDecl;
155 }
156
157 /**
158 * Semantic validation of the structure of this class. Subclasses should
159 * override this method to impose additional semantic constraints on the
160 * contents/relations of fields.
161 *
162 * @throws {IllegalModelException}
163 * @private
164 */
165 validate() {
166
167 super.validate();
168
169 const declarations = this.getModelFile().getAllDeclarations();
170 for (let n = 0; n < declarations.length; n++) {
171 let declaration = declarations[n];
172
173 // check we don't have an asset with the same name
174 for (let i = n + 1; i < declarations.length; i++) {
175 let otherDeclaration = declarations[i];
176 if (declaration.getFullyQualifiedName() === otherDeclaration.getFullyQualifiedName()) {
177 throw new IllegalModelException(`Duplicate class name ${declaration.getName()}`);
178 }
179 }
180 }
181
182 // TODO (LG) check that all imported classes exist, rather than just
183 // used imports
184
185 // if we have a super type make sure it exists
186 if (this.superType !== null) {
187 this._resolveSuperType();
188 }
189
190 if (this.idField) {
191 const idField = this.getProperty(this.idField);
192 if (!idField) {
193 let formatter = Globalize('en').messageFormatter('classdeclaration-validate-identifiernotproperty');
194 throw new IllegalModelException(formatter({
195 'class': this.name,
196 'idField': this.idField
197 }), this.modelFile, this.ast.location);
198 } else {
199 // check that identifiers are strings
200 if (idField.getType() !== 'String') {
201 let formatter = Globalize('en').messageFormatter('classdeclaration-validate-identifiernotstring');
202 throw new IllegalModelException(formatter({
203 'class': this.name,
204 'idField': this.idField
205 }), this.modelFile, this.ast.location);
206 }
207
208 if (idField.isOptional()) {
209 throw new IllegalModelException('Identifying fields cannot be optional.', this.modelFile, this.ast.location);
210 }
211 if (this.getSuperType()) {
212 // check this class doesn't declare the identifying field as a property.
213 if (idField.getName() === this.getModelFile().getType(this.superType).getIdentifierFieldName()) {
214 throw new IllegalModelException('Identifier from super class cannot be redeclared.', this.modelFile, this.ast.location);
215 }
216
217 // TODO: This has been disabled pending major version bump and/or confirmation that this is illegal
218 // As this class has an idField declared, check the superclass doesn't
219 //if (this.getModelFile().getType(this.superType).getIdentifierFieldName()) {
220 // throw new IllegalModelException('Identifier defined in super class, identifiers cannot be overridden', this.modelFile, this.ast.location);
221 //}
222 }
223 }
224 } else {
225 if (this.isAbstract() === false && this.isEnum() === false && this.isConcept() === false) {
226 if (this.getIdentifierFieldName() === null) {
227 let formatter = Globalize('en').messageFormatter('classdeclaration-validate-missingidentifier');
228 throw new IllegalModelException(formatter({
229 'class': this.name
230 }), this.modelFile, this.ast.location);
231 }
232 }
233 }
234
235 // we also have to check fields defined in super classes
236 const properties = this.getProperties();
237 for (let n = 0; n < properties.length; n++) {
238 let field = properties[n];
239
240 // check we don't have a field with the same name
241 for (let i = n + 1; i < properties.length; i++) {
242 let otherField = properties[i];
243 if (field.getName() === otherField.getName()) {
244 let formatter = Globalize('en').messageFormatter('classdeclaration-validate-duplicatefieldname');
245 throw new IllegalModelException(formatter({
246 'class': this.name,
247 'fieldName': field.getName()
248 }), this.modelFile, this.ast.location);
249 }
250 }
251
252 // we now validate the field, however to ensure that
253 // imports are resolved correctly we validate in the context
254 // of the declared type of the field for non-primitives in a different namespace
255 if (field.isPrimitive() || this.isEnum() || field.getNamespace() === this.getNamespace()) {
256 field.validate(this);
257 } else {
258 const typeFqn = field.getFullyQualifiedTypeName();
259 const classDecl = this.modelFile.getModelManager().getType(typeFqn);
260 field.validate(classDecl);
261 }
262 }
263 }
264
265 /**
266 * Returns the base system type for this type of class declaration. Override
267 * this method in derived classes to specify a base system type.
268 *
269 * @return {string} the short name of the base system type or null
270 */
271 getSystemType() {
272 return null;
273 }
274
275 /**
276 * Returns true if this class is declared as abstract in the model file
277 *
278 * @return {boolean} true if the class is abstract
279 */
280 isAbstract() {
281 return this.abstract;
282 }
283
284 /**
285 * Returns true if this class is an enumeration.
286 *
287 * @return {boolean} true if the class is an enumerated type
288 */
289 isEnum() {
290 return false;
291 }
292
293 /**
294 * Returns true if this class is the definition of a concept.
295 *
296 * @return {boolean} true if the class is a concept
297 */
298 isConcept() {
299 return false;
300 }
301
302 /**
303 * Returns true if this class is the definition of an event.
304 *
305 * @return {boolean} true if the class is an event
306 */
307 isEvent() {
308 return false;
309 }
310
311 /**
312 * Returns true if this class can be pointed to by a relationship
313 *
314 * @return {boolean} true if the class may be pointed to by a relationship
315 */
316 isRelationshipTarget() {
317 return false;
318 }
319
320 /**
321 * Returns true if this class can be pointed to by a relationship in a
322 * system model
323 *
324 * @return {boolean} true if the class may be pointed to by a relationship
325 */
326 isSystemRelationshipTarget() {
327 return this.isRelationshipTarget();
328 }
329
330 /**
331 * Returns true is this type is in the system namespace
332 *
333 * @return {boolean} true if the class may be pointed to by a relationship
334 */
335 isSystemType() {
336 return this.modelFile.isSystemModelFile();
337 }
338
339 /**
340 * Returns true if this class is a system core type - both in the system
341 * namespace, and also one of the system core types (Asset, Participant, etc).
342 *
343 * @return {boolean} true if the class may be pointed to by a relationship
344 */
345 isSystemCoreType() {
346 return this.isSystemType() &&
347 this.getSystemType() === this.getName();
348 }
349
350 /**
351 * Returns the short name of a class. This name does not include the
352 * namespace from the owning ModelFile.
353 *
354 * @return {string} the short name of this class
355 */
356 getName() {
357 return this.name;
358 }
359
360 /**
361 * Return the namespace of this class.
362 * @return {String} namespace - a namespace.
363 */
364 getNamespace() {
365 return this.modelFile.getNamespace();
366 }
367
368 /**
369 * Returns the fully qualified name of this class.
370 * The name will include the namespace if present.
371 *
372 * @return {string} the fully-qualified name of this class
373 */
374 getFullyQualifiedName() {
375 return this.fqn;
376 }
377
378 /**
379 * Returns the name of the identifying field for this class. Note
380 * that the identifying field may come from a super type.
381 *
382 * @return {string} the name of the id field for this class
383 */
384 getIdentifierFieldName() {
385
386 if (this.idField) {
387 return this.idField;
388 } else {
389 if (this.getSuperType()) {
390 // we first check our own modelfile, as we may be called from validate
391 // in which case our model file has not yet been added to the model modelManager
392 let classDecl = this.getModelFile().getLocalType(this.getSuperType());
393
394 // not a local type, so we try the model manager
395 if (!classDecl) {
396 classDecl = this.modelFile.getModelManager().getType(this.getSuperType());
397 }
398 return classDecl.getIdentifierFieldName();
399 } else {
400 return null;
401 }
402 }
403 }
404
405 /**
406 * Returns the field with a given name or null if it does not exist.
407 * The field must be directly owned by this class -- the super-type is
408 * not introspected.
409 *
410 * @param {string} name the name of the field
411 * @return {Property} the field definition or null if it does not exist.
412 */
413 getOwnProperty(name) {
414 for (let n = 0; n < this.properties.length; n++) {
415 const field = this.properties[n];
416 if (field.getName() === name) {
417 return field;
418 }
419 }
420
421 return null;
422 }
423
424 /**
425 * Returns the fields directly defined by this class.
426 *
427 * @return {Property[]} the array of fields
428 */
429 getOwnProperties() {
430 return this.properties;
431 }
432
433 /**
434 * Returns the FQN of the super type for this class or null if this
435 * class does not have a super type.
436 *
437 * @return {string} the FQN name of the super type or null
438 */
439 getSuperType() {
440 const superTypeDeclaration = this.getSuperTypeDeclaration();
441 if (superTypeDeclaration) {
442 return superTypeDeclaration.getFullyQualifiedName();
443 } else {
444 return null;
445 }
446 }
447
448 /**
449 * Get the super type class declaration for this class.
450 * @return {ClassDeclaration} the super type declaration, or null if there is no super type.
451 */
452 getSuperTypeDeclaration() {
453 if (!this.superType) {
454 // No super type.
455 return null;
456 } else if (!this.superTypeDeclaration) {
457 // Super type that hasn't been resolved yet.
458 return this._resolveSuperType();
459 } else {
460 // Resolved super type.
461 return this.superTypeDeclaration;
462 }
463 }
464
465 /**
466 * Get the class declarations for all subclasses of this class, including this class.
467 * @return {ClassDeclaration[]} subclass declarations.
468 */
469 getAssignableClassDeclarations() {
470 const results = new Set();
471 const modelManager = this.getModelFile().getModelManager();
472 const introspector = new Introspector(modelManager);
473 const allClassDeclarations = introspector.getClassDeclarations();
474 const subclassMap = new Map();
475
476 // Build map of all direct subclasses relationships
477 allClassDeclarations.forEach((declaration) => {
478 const superType = declaration.getSuperType();
479 if (superType) {
480 const subclasses = subclassMap.get(superType) || new Set();
481 subclasses.add(declaration);
482 subclassMap.set(superType, subclasses);
483 }
484 });
485
486 // Recursive function to collect all direct and indirect subclasses of a given (set) of base classes.
487 const collectSubclasses = (superclasses) => {
488 superclasses.forEach((declaration) => {
489 results.add(declaration);
490 const superType = declaration.getFullyQualifiedName();
491 const subclasses = subclassMap.get(superType);
492 if (subclasses) {
493 collectSubclasses(subclasses);
494 }
495 });
496 };
497
498 collectSubclasses([this]);
499
500 return Array.from(results);
501 }
502
503 /**
504 * Get all the super-type declarations for this type.
505 * @return {ClassDeclaration[]} super-type declarations.
506 */
507 getAllSuperTypeDeclarations() {
508 const results = [];
509 for (let type = this;
510 (type = type.getSuperTypeDeclaration());) {
511 results.push(type);
512 }
513
514 return results;
515 }
516
517 /**
518 * Returns the property with a given name or null if it does not exist.
519 * Fields defined in super-types are also introspected.
520 *
521 * @param {string} name the name of the field
522 * @return {Property} the field, or null if it does not exist
523 */
524 getProperty(name) {
525 let result = this.getOwnProperty(name);
526 let classDecl = null;
527
528 if (result === null && this.superType !== null) {
529 if (this.getModelFile().isImportedType(this.superType)) {
530 let fqnSuper = this.getModelFile().resolveImport(this.superType);
531 classDecl = this.modelFile.getModelManager().getType(fqnSuper);
532 } else {
533 classDecl = this.getModelFile().getType(this.superType);
534 }
535 result = classDecl.getProperty(name);
536 }
537
538 return result;
539 }
540
541 /**
542 * Returns the properties defined in this class and all super classes.
543 *
544 * @return {Property[]} the array of fields
545 */
546 getProperties() {
547 let result = this.getOwnProperties();
548 let classDecl = null;
549 if (this.superType !== null) {
550 if (this.getModelFile().isImportedType(this.superType)) {
551 let fqnSuper = this.getModelFile().resolveImport(this.superType);
552 classDecl = this.modelFile.getModelManager().getType(fqnSuper);
553 } else {
554 classDecl = this.getModelFile().getType(this.superType);
555 }
556
557 if (classDecl === null) {
558 throw new IllegalModelException('Could not find super type ' + this.superType, this.modelFile, this.ast.location);
559 }
560
561 // go get the fields from the super type
562 result = result.concat(classDecl.getProperties());
563 } else {
564 // console.log('No super type for ' + this.getName() );
565 }
566
567 return result;
568 }
569
570 /**
571 * Get a nested property using a dotted property path
572 * @param {string} propertyPath The property name or name with nested structure e.g a.b.c
573 * @returns {Property} the property
574 * @throws {IllegalModelException} if the property path is invalid or the property does not exist
575 */
576 getNestedProperty(propertyPath) {
577
578 const propertyNames = propertyPath.split('.');
579 let classDeclaration = this;
580 let result = null;
581
582 for (let n = 0; n < propertyNames.length; n++) {
583
584 // get the nth property
585 result = classDeclaration.getProperty(propertyNames[n]);
586
587 if (result === null) {
588 throw new IllegalModelException('Property ' + propertyNames[n] + ' does not exist on ' + classDeclaration.getFullyQualifiedName(), this.modelFile, this.ast.location);
589 }
590 // not the last element, get the class of the element
591 else if (n < propertyNames.length - 1) {
592 if (result.isPrimitive() || result.isTypeEnum()) {
593 throw new Error('Property ' + propertyNames[n] + ' is a primitive or enum. Invalid property path: ' + propertyPath);
594 } else {
595 // get the nested type, this throws if the type is missing or if the type is an enum
596 classDeclaration = classDeclaration.getModelFile().getModelManager().getType(result.getFullyQualifiedTypeName());
597 }
598 }
599 }
600
601 return result;
602 }
603
604 /**
605 * Returns the string representation of this class
606 * @return {String} the string representation of the class
607 */
608 toString() {
609 let superType = '';
610 if (this.superType) {
611 superType = ' super=' + this.superType;
612 }
613 return 'ClassDeclaration {id=' + this.getFullyQualifiedName() + superType + ' enum=' + this.isEnum() + ' abstract=' + this.isAbstract() + '}';
614 }
615
616 /**
617 * Alternative instanceof that is reliable across different module instances
618 * @see https://github.com/hyperledger/composer-concerto/issues/47
619 *
620 * @param {object} object - The object to test against
621 * @returns {boolean} - True, if the object is an instance of a Class Declaration
622 */
623 static [Symbol.hasInstance](object){
624 return typeof object !== 'undefined' && object !== null && Boolean(object._isClassDeclaration);
625 }
626
627}
628
629module.exports = ClassDeclaration;