Source: composer-common/lib/serializer.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 Resource = require('./model/resource');
const Globalize = require('./globalize');
const JSONGenerator = require('./serializer/jsongenerator');
const JSONPopulator = require('./serializer/jsonpopulator');
const ResourceValidator = require('./serializer/resourcevalidator');
const TypedStack = require('./serializer/typedstack');
const JSONWriter = require('./codegen/jsonwriter');

/**
 * Serialize Resources instances to/from various formats for long-term storage
 * (e.g. on the blockchain).
 * <p><a href="./diagrams/serializer.svg"><img src="./diagrams/serializer.svg" style="width:100%;"/></a></p>
 * @class
 * @memberof module:composer-common
 */
class Serializer {
    /**
     * Create a Serializer.
     * <strong>Note: Only to be called by framework code. Applications should
     * retrieve instances from {@link Composer}</strong>
     * </p>
     * @param {Factory} factory - The Factory to use to create instances
     * @param {ModelManager} modelManager - The ModelManager to use for validation etc.
     */
    constructor(factory,modelManager) {

        if(!factory) {
            throw new Error(Globalize.formatMessage('serializer-constructor-factorynull'));
        } else if(!modelManager) {
            throw new Error(Globalize.formatMessage('serializer-constructor-modelmanagernull'));
        }

        this.factory = factory;
        this.modelManager = modelManager;
    }

    /**
     * <p>
     * Convert a {@link Resource} to a JavaScript object suitable for long-term
     * peristent storage.
     * </p>
     * @param {Resource} resource - The instance to convert to JSON
     * @param {Object} options - the optional serialization options.
     * @param {boolean} options.validate - validate the structure of the Resource
     * with its model prior to serialization (default to true)
     * @param {boolean} options.convertResourcesToRelationships - Convert resources that
     * are specified for relationship fields into relationships, false by default.
     * @param {boolean} options.permitResourcesForRelationships - Permit resources in the
     * place of relationships (serializing them as resources), false by default.
     * @return {Object} - The Javascript Object that represents the resource
     * @throws {Error} - throws an exception if resource is not an instance of
     * Resource or fails validation.
     */
    toJSON(resource, options) {
        // correct instance type
        if(!(resource instanceof Resource)) {
            throw new Error(Globalize.formatMessage('serializer-tojson-notcobject'));
        }

        const parameters = {};
        parameters.stack = new TypedStack(resource);
        parameters.modelManager = this.modelManager;
        parameters.seenResources = new Set();
        const classDeclaration = this.modelManager.getType( resource.getFullyQualifiedType() );
        if(!classDeclaration) {
            throw new Error( 'Failed to find type ' + resource.getFullyQualifiedType() + ' in ModelManager.' );
        }

        // validate the resource against the model
        options = options || { validate: true };
        if(options.validate === true) {
            const validator = new ResourceValidator();
            classDeclaration.accept(validator, parameters);
        }

        const generator = new JSONGenerator(
            options.convertResourcesToRelationships === true,
            options.permitResourcesForRelationships === true
        );
        const writer = new JSONWriter();
        parameters.writer = writer;
        parameters.stack.clear();
        parameters.stack.push(resource);

        // this writes the JSON into the parameters.writer
        classDeclaration.accept(generator, parameters);
        const jsonText = parameters.writer.getBuffer();

        try {
            return JSON.parse(jsonText);
        }
        catch(err) {
            throw new Error( 'Generated invalid JSON: ' + jsonText );
        }
    }

    /**
     * Create a {@link Resource} from a JavaScript Object representation.
     * The JavaScript Object should have been created by calling the
     * {@link Serializer#toJSON toJSON} API.
     *
     * The Resource is populated based on the JavaScript object.
     *
     * @param {Object} jsonObject The JavaScript Object for a Resource
     * @param {Object} options - the optional serialization options
     * @param {boolean} options.acceptResourcesForRelationships - handle JSON objects
     * in the place of strings for relationships, defaults to false.
     * @return {Resource} The new populated resource
     */
    fromJSON(jsonObject, options) {

        if(!jsonObject.$class) {
            throw new Error('Invalid JSON data. Does not contain a $class type identifier.');
        }

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

        // default the options.
        options = options || {};

        // create a new instance, using the identifier field name as the ID.
        let resource = this.factory.newInstance( classDeclaration.getModelFile().getNamespace(),
          classDeclaration.getName(), jsonObject[classDeclaration.getIdentifierFieldName()] );

        // populate the resource based on the jsonObject
        // by walking the classDeclaration
        const parameters = {};
        parameters.jsonStack = new TypedStack(jsonObject);
        parameters.resourceStack = new TypedStack(resource);
        parameters.modelManager = this.modelManager;
        parameters.factory = this.factory;
        const populator = new JSONPopulator(options.acceptResourcesForRelationships === true);
        classDeclaration.accept(populator, parameters);
        return resource;
    }
}

module.exports = Serializer;