Source: composer-common/lib/serializer/jsonpopulator.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 ClassDeclaration = require('../introspect/classdeclaration');
const Field = require('../introspect/field');
const RelationshipDeclaration = require('../introspect/relationshipdeclaration');
const Util = require('../util');
const ModelUtil = require('../modelutil');

/**
 * Populates a Resource with data from a JSON object graph. The JSON objects
 * should be the result of calling Serializer.toJSON and then JSON.parse.
 * The parameters object should contain the keys
 * 'stack' - the TypedStack of objects being processed. It should
 * start with the root object from JSON.parse.
 * 'factory' - the Factory instance to use for creating objects.
 * 'modelManager' - the ModelManager instance to use to resolve classes
 * @private
 * @class
 * @memberof module:composer-common
 */
class JSONPopulator {

    /**
     * Constructor.
     * @param {boolean} [acceptResourcesForRelationships] Permit resources in the
     * place of relationships, false by default.
     */
    constructor(acceptResourcesForRelationships) {
        this.acceptResourcesForRelationships = acceptResourcesForRelationships;
    }

    /**
     * Visitor design pattern
     * @param {Object} thing - the object being visited
     * @param {Object} parameters  - the parameter
     * @return {Object} the result of visiting or null
     * @private
     */
    visit(thing, parameters) {
        if (thing instanceof ClassDeclaration) {
            return this.visitClassDeclaration(thing, parameters);
        } else if (thing instanceof RelationshipDeclaration) {
            return this.visitRelationshipDeclaration(thing, parameters);
        } else if (thing instanceof Field) {
            return this.visitField(thing, parameters);
        } else {
            throw new Error('Unrecognised ' + JSON.stringify(thing) );
        }
    }

    /**
     * Visitor design pattern
     * @param {ClassDeclaration} classDeclaration - the object being visited
     * @param {Object} parameters  - the parameter
     * @return {Object} the result of visiting or null
     * @private
     */
    visitClassDeclaration(classDeclaration, parameters) {
        const jsonObj = parameters.jsonStack.pop();
        const resourceObj = parameters.resourceStack.pop();

        const properties = classDeclaration.getProperties();
        for(let n=0; n < properties.length; n++) {
            const property = properties[n];
            const value = jsonObj[property.getName()];
            if(!Util.isNull(value)) {
                parameters.jsonStack.push(value);
                resourceObj[property.getName()] = property.accept(this,parameters);
            }
        }
        return resourceObj;
    }

    /**
     * Visitor design pattern
     * @param {Field} field - the object being visited
     * @param {Object} parameters  - the parameter
     * @return {Object} the result of visiting or null
     * @private
     */
    visitField(field, parameters) {
        const jsonObj = parameters.jsonStack.pop();
        let result = null;

        if(field.isArray()) {
            result = [];
            for(let n=0; n < jsonObj.length; n++) {
                const jsonItem = jsonObj[n];
                result.push(this.convertItem(field,jsonItem, parameters));
            }
        }
        else {
            result = this.convertItem(field,jsonObj, parameters);
        }

        return result;
    }


    /**
    *
    * @param {Field} field - the field of the item being converted
    * @param {Object} jsonItem - the JSON object of the item being converted
    * @param {Object} parameters - the parameters
    * @return {Object} - the populated object.
    */
    convertItem(field, jsonItem, parameters) {
        let result = null;

        if(!field.isPrimitive() && !field.isTypeEnum()) {
            let typeName = jsonItem.$class;
            if(!typeName) {
                // If the type name is not specified in the data, then use the
                // type name from the model. This will only happen in the case of
                // a sub resource inside another resource.
                typeName = field.getFullyQualifiedTypeName();
            }

            // This throws if the type does not exist.
            const classDeclaration = parameters.modelManager.getType(typeName);

            // create a new instance, using the identifier field name as the ID.
            let subResource = null;

            // if this is identifiable, then we create a resource
            if(!classDeclaration.isConcept()) {
                subResource = parameters.factory.newResource(classDeclaration.getModelFile().getNamespace(),
              classDeclaration.getName(), jsonItem[classDeclaration.getIdentifierFieldName()] );
            }
            else {
              // otherwise we create a concept
                subResource = parameters.factory.newConcept(classDeclaration.getModelFile().getNamespace(),
                            classDeclaration.getName() );
            }

            result = subResource;
            parameters.resourceStack.push(subResource);
            parameters.jsonStack.push(jsonItem);
            classDeclaration.accept(this, parameters);
        }
        else {
            result = this.convertToObject(field,jsonItem);
        }

        return result;
    }

    /**
     * Converts a primtive object to JSON text.
     *
     * @param {Field} field - the field declaration of the object
     * @param {Object} json - the JSON object to convert to a Composer Object
     * @return {string} the text representation
     */
    convertToObject(field, json) {
        let result = null;

        switch(field.getType()) {
        case 'DateTime':
            result = new Date(json);
            break;
        case 'Integer':
        case 'Long':
            result = parseInt(json);
            break;
        case 'Double':
            result = parseFloat(json);
            break;
        case 'Boolean':
            result = (json === true || json === 'true');
            break;
        case 'String':
            result = json.toString();
            break;
        default:
            // everything else should be an enumerated value...
            result = json;
        }

        return result;
    }

    /**
     * Visitor design pattern
     * @param {RelationshipDeclaration} relationshipDeclaration - the object being visited
     * @param {Object} parameters  - the parameter
     * @return {Object} the result of visiting or null
     * @private
     */
    visitRelationshipDeclaration(relationshipDeclaration, parameters) {
        const jsonObj = parameters.jsonStack.pop();
        let result = null;

        let typeFQN = relationshipDeclaration.getFullyQualifiedTypeName();
        let namespace = ModelUtil.getNamespace(typeFQN);
        if(!namespace) {
            namespace = relationshipDeclaration.getNamespace();
        }
        let type = ModelUtil.getShortName(typeFQN);

        if(relationshipDeclaration.isArray()) {
            result = [];
            for(let n=0; n < jsonObj.length; n++) {
                let jsonItem = jsonObj[n];
                if (typeof jsonItem === 'string') {
                    result.push(parameters.factory.newRelationship(namespace, type, jsonItem));
                } else {
                    if (!this.acceptResourcesForRelationships) {
                        throw new Error('Invalid JSON data. Found a value that is not a string: ' + jsonObj + ' for relationship ' + relationshipDeclaration);
                    }

                    // this isn't a relationship, but it might be an object!
                    if(!jsonItem.$class) {
                        throw new Error('Invalid JSON data. Does not contain a $class type identifier: ' + jsonItem + ' for relationship ' + relationshipDeclaration );
                    }

                    const classDeclaration = parameters.modelManager.getType(jsonItem.$class);
                    if(!classDeclaration) {
                        throw new Error( 'Failed to find type ' + jsonItem.$class + ' in ModelManager.' );
                    }

                    // create a new instance, using the identifier field name as the ID.
                    let subResource = parameters.factory.newResource(classDeclaration.getModelFile().getNamespace(),
                        classDeclaration.getName(), jsonItem[classDeclaration.getIdentifierFieldName()] );
                    parameters.jsonStack.push(jsonItem);
                    parameters.resourceStack.push(subResource);
                    classDeclaration.accept(this, parameters);
                    result.push(subResource);
                }
            }
        }
        else {
            if (typeof jsonObj === 'string') {
                result = parameters.factory.newRelationship(namespace, type, jsonObj);
            } else {
                if (!this.acceptResourcesForRelationships) {
                    throw new Error('Invalid JSON data. Found a value that is not a string: ' + jsonObj + ' for relationship ' + relationshipDeclaration);
                }

                // this isn't a relationship, but it might be an object!
                if(!jsonObj.$class) {
                    throw new Error('Invalid JSON data. Does not contain a $class type identifier: ' + jsonObj + ' for relationship ' + relationshipDeclaration );
                }

                const classDeclaration = parameters.modelManager.getType(jsonObj.$class);
                if(!classDeclaration) {
                    throw new Error( 'Failed to find type ' + jsonObj.$class + ' in ModelManager.' );
                }

                // create a new instance, using the identifier field name as the ID.
                let subResource = parameters.factory.newResource(classDeclaration.getModelFile().getNamespace(),
                    classDeclaration.getName(), jsonObj[classDeclaration.getIdentifierFieldName()] );
                parameters.jsonStack.push(jsonObj);
                parameters.resourceStack.push(subResource);
                classDeclaration.accept(this, parameters);
                result = subResource;
            }
        }
        return result;
    }
}

module.exports = JSONPopulator;