Source: composer-common/lib/introspect/classdeclaration.js

/*
 * 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 Field = require('./field');
const EnumValueDeclaration = require('./enumvaluedeclaration');
const RelationshipDeclaration = require('./relationshipdeclaration');
const IllegalModelException = require('./illegalmodelexception');
const Globalize = require('../globalize');

/**
 * ClassDeclaration defines the structure (model/schema) of composite data.
 * It is composed of a set of Properties, may have an identifying field, and may
 * have a super-type.
 * A ClassDeclaration is conceptually owned with a ModelFile which
 * defines all the classes that are part of a namespace.
 *
 * @private
 * @abstract
 * @class
 * @memberof module:composer-common
 */
class ClassDeclaration {

    /**
     * Create a ClassDeclaration from an Abstract Syntax Tree. The AST is the
     * result of parsing.
     *
     * @param {ModelFile} modelFile - the ModelFile for this class
     * @param {string} ast - the AST created by the parser
     * @throws {IllegalModelException}
     */
    constructor(modelFile, ast) {
        if(!modelFile || !ast) {
            throw new IllegalModelException(Globalize.formatMessage('classdeclaration-constructor-modelastreq'));
        }

        this.ast = ast;
        this.modelFile = modelFile;
        this.process();
    }

    /**
     * Visitor design pattern
     * @param {Object} visitor - the visitor
     * @param {Object} parameters  - the parameter
     * @return {Object} the result of visiting or null
     * @private
     */
    accept(visitor,parameters) {
        return visitor.visit(this, parameters);
    }

    /**
     * Returns the ModelFile that defines this class.
     *
     * @return {ModelFile} the owning ModelFile
     */
    getModelFile() {
        return this.modelFile;
    }

    /**
     * Process the AST and build the model
     *
     * @throws {InvalidModelException}
     * @private
     */
    process() {
        this.name = this.ast.id.name;
        this.properties = [];
        this.superType = null;
        this.idField = null;
        this.abstract = false;

        if(this.ast.abstract) {
            this.abstract = true;
        }

        if(this.ast.classExtension) {
            this.superType = this.ast.classExtension.class.name;
        }

        if(this.ast.idField) {
            this.idField = this.ast.idField.name;
        }

        for(let n=0; n < this.ast.body.declarations.length; n++ ) {
            let thing = this.ast.body.declarations[n];

            //console.log('Found: ' + thing.type + ' ' + thing.id.name);

            if(thing.type === 'FieldDeclaration') {
                this.properties.push( new Field(this, thing) );
            }
            else if(thing.type === 'RelationshipDeclaration') {
                this.properties.push( new RelationshipDeclaration(this, thing) );
            }
            else if(thing.type === 'EnumPropertyDeclaration') {
                this.properties.push( new EnumValueDeclaration(this, thing) );
            }
            else {
                let formatter = Globalize.messageFormatter('classdeclaration-process-unrecmodelelem');
                throw new IllegalModelException(formatter({
                    'type': thing.type
                }), this.modelFile, this.ast.location);
            }
        }
    }

    /**
     * Semantic validation of the structure of this class. Subclasses should
     * override this method to impose additional semantic constraints on the
     * contents/relations of fields.
     *
     * @throws {InvalidModelException}
     * @private
     */
    validate() {
        // TODO (LG) check that all imported classes exist, rather than just
        // used imports

        // if we have a super type make sure it exists
        if(this.superType!==null) {
            let classDecl = null;
            if(this.getModelFile().isImportedType(this.superType)) {
                let fqnSuper = this.getModelFile().resolveImport(this.superType);
                classDecl = this.modelFile.getModelManager().getType(fqnSuper);
            }
            else {
                classDecl = this.getModelFile().getType(this.superType);
            }

            if(classDecl===null) {
                throw new IllegalModelException('Could not find super type ' + this.superType, this.modelFile, this.ast.location);
            }
            // TODO (DCS)
            // else {
            //     // check that assets only inherit from assets etc.
            //     if( Object.getPrototypeOf(classDecl) !== Object.getPrototypeOf(this)) {
            //         throw new Error('Invalid super type for ' + this.name + ' is must be of type ' + Object.getPrototypeOf(this) );
            //     }
            // }
        }

        if(this.idField) {
            const field = this.getProperty(this.idField);
            if(!field) {
                let formatter = Globalize('en').messageFormatter('classdeclaration-validate-identifiernotproperty');
                throw new IllegalModelException(formatter({
                    'class': this.name,
                    'idField': this.idField
                }), this.modelFile, this.ast.location);
            }
            else {
                // check that identifiers are strings
                if(field.getType() !== 'String') {
                    let formatter = Globalize('en').messageFormatter('classdeclaration-validate-identifiernotstring');
                    throw new IllegalModelException( formatter({
                        'class': this.name,
                        'idField': this.idField
                    }),this.modelFile, this.ast.location);
                }

                if(field.isOptional()) {
                    throw new IllegalModelException('Identifying fields cannot be optional.',this.modelFile, this.ast.location);
                }
            }
        }
        else {
            if( this.isAbstract() === false && this.isEnum() === false && this.isConcept() === false) {
                if( this.getIdentifierFieldName() === null) {
                    let formatter = Globalize('en').messageFormatter('classdeclaration-validate-missingidentifier');
                    throw new IllegalModelException( formatter({
                        'class': this.name
                    }),this.modelFile, this.ast.location);
                }
            }
        }

        // we also have to check fields defined in super classes
        const properties = this.getProperties();
        for(let n=0; n < properties.length; n++) {
            let field = properties[n];

            // check we don't have a field with the same name
            for(let i=n+1; i < properties.length; i++) {
                let otherField = properties[i];
                if(field.getName() === otherField.getName()) {
                    let formatter = Globalize('en').messageFormatter('classdeclaration-validate-duplicatefieldname');
                    throw new IllegalModelException( formatter({
                        'class': this.name,
                        'fieldName': field.getName()
                    }),this.modelFile, this.ast.location);
                }
            }

            field.validate(this);
        }
    }

    /**
     * Returns true if this class is declared as abstract in the model file
     *
     * @return {boolean} true if the class is abstract
     */
    isAbstract() {
        return this.abstract;
    }

    /**
     * Returns true if this class is an enumeration.
     *
     * @return {boolean} true if the class is an enumerated type
     */
    isEnum() {
        return false;
    }

    /**
     * Returns true if this class is the definition of a concept.
     *
     * @return {boolean} true if the class is a concept
     */
    isConcept() {
        return false;
    }

    /**
     * Returns true if this class can be pointed to by a relationship
     *
     * @return {boolean} true if the class may be pointed to by a relationship
     */
    isRelationshipTarget() {
        return false;
    }

    /**
     * Returns the short name of a class. This name does not include the
     * namespace from the owning ModelFile.
     *
     * @return {string} the short name of this class
     */
    getName() {
        return this.name;
    }

    /**
     * Returns the fully qualified name of this class.
     * The name will include the namespace if present.
     *
     * @return {string} the fully-qualified name of this class
     */
    getFullyQualifiedName() {
        return this.modelFile.getNamespace() + '.' + this.name;
    }

    /**
     * Returns the name of the identifying field for this class. Note
     * that the identifying field may come from a super type.
     *
     * @return {string} the name of the id field for this class
     */
    getIdentifierFieldName() {

        if(this.idField) {
            return this.idField;
        }
        else {
            if(this.getSuperType()) {
                // we first check our own modelfile, as we may be called from validate
                // in which case our model file has not yet been added to the model modelManager
                let classDecl = this.getModelFile().getLocalType(this.getSuperType());

                // not a local type, so we try the model manager
                if(!classDecl) {
                    classDecl = this.modelFile.getModelManager().getType(this.getSuperType());
                }
                return classDecl.getIdentifierFieldName();
            }
            else {
                return null;
            }
        }
    }

    /**
     * Returns the field with a given name or null if it does not exist.
     * The field must be directly owned by this class -- the super-type is
     * not introspected.
     *
     * @param {string} name the name of the field
     * @return {Property} the field definition or null if it does not exist.
     */
    getOwnProperty(name) {
        for(let n=0; n < this.properties.length; n++) {
            const field = this.properties[n];
            if(field.getName() === name) {
                return field;
            }
        }

        return null;
    }

    /**
     * Returns the fields directly defined by this class.
     *
     * @return {Property[]} the array of fields
     */
    getOwnProperties() {
        return this.properties;
    }

    /**
     * Returns the FQN of the super type for this class or null if this
     * class does not have a super type.
     *
     * @return {string} the FQN name of the super type or null
     */
    getSuperType() {
        if(this.superType) {
            const type = this.getModelFile().getType(this.superType);
            if(type === null) {
                throw new Error('Could not find super type:' + this.superType );
            }
            else {
                return type.getFullyQualifiedName();
            }
        }

        return null;
    }

    /**
     * Returns the property with a given name or null if it does not exist.
     * Fields defined in super-types are also introspected.
     *
     * @param {string} name the name of the field
     * @return {Property} the field, or null if it does not exist
     */
    getProperty(name) {
        let result = this.getOwnProperty(name);
        let classDecl = null;

        if(result === null && this.superType!==null) {
            if(this.getModelFile().isImportedType(this.superType)) {
                let fqnSuper = this.getModelFile().resolveImport(this.superType);
                classDecl = this.modelFile.getModelManager().getType(fqnSuper);
            }
            else {
                classDecl = this.getModelFile().getType(this.superType);
            }
            result = classDecl.getProperty(name);
        }

        return result;
    }

    /**
     * Returns the properties defined in this class and all super classes.
     *
     * @return {Property[]} the array of fields
     */
    getProperties() {
        let result = this.getOwnProperties();
        let classDecl = null;
        if(this.superType!==null) {
            if(this.getModelFile().isImportedType(this.superType)) {
                let fqnSuper = this.getModelFile().resolveImport(this.superType);
                classDecl = this.modelFile.getModelManager().getType(fqnSuper);
            }
            else {
                classDecl = this.getModelFile().getType(this.superType);
            }

            if(classDecl===null) {
                throw new IllegalModelException('Could not find super type ' + this.superType,this.modelFile, this.ast.location);
            }

            // go get the fields from the super type
            result = result.concat(classDecl.getProperties());
        }
        else {
            // console.log('No super type for ' + this.getName() );
        }

        return result;
    }

    /**
     * Stop serialization of this object.
     * @return {Object} An empty object.
     */
    toJSON() {
        return {};
    }

    /**
     * Returns the string representation of this class
     * @return {String} the string representation of the class
     */
    toString() {
        let superType = '';
        if(this.superType) {
            superType = ' super=' + this.superType;
        }
        return 'ClassDeclaration {id=' + this.getFullyQualifiedName() + superType + ' enum=' + this.isEnum() + ' abstract=' + this.isAbstract() + '}';
    }
}

module.exports = ClassDeclaration;