@accordproject/concerto-core
Version:
Core Implementation for the Concerto Modeling Language
207 lines (185 loc) • 9.09 kB
JavaScript
/*
* 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.
*/
;
const { TypedStack } = require('@accordproject/concerto-util');
const DateTimeUtil = require('./datetimeutil');
const Globalize = require('./globalize');
const JSONGenerator = require('./serializer/jsongenerator');
const JSONPopulator = require('./serializer/jsonpopulator');
const Typed = require('./model/typed');
const ResourceValidator = require('./serializer/resourcevalidator');
const { utcOffset: defaultUtcOffset } = DateTimeUtil.setCurrentTime();
const baseDefaultOptions = {
validate: true,
utcOffset: defaultUtcOffset,
};
// Types needed for TypeScript generation.
/* eslint-disable no-unused-vars */
/* istanbul ignore next */
if (global === undefined) {
const Factory = require('./factory');
const ModelManager = require('./modelmanager');
const Resource = require('./model/resource');
}
/* eslint-enable no-unused-vars */
/**
* Serialize Resources instances to/from various formats for long-term storage
* (e.g. on the blockchain).
*
* @class
* @memberof module:concerto-core
*/
class Serializer {
/**
* Create a Serializer.
* @param {Factory} factory - The Factory to use to create instances
* @param {ModelManager} modelManager - The ModelManager to use for validation etc.
* @param {object} [options] - Serializer options
*/
constructor(factory, modelManager, options) {
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;
this.defaultOptions = Object.assign({}, baseDefaultOptions, options || {});
}
/**
* Set the default options for the serializer.
* @param {Object} newDefaultOptions The new default options for the serializer.
*/
setDefaultOptions(newDefaultOptions) {
// Combine the specified default options with the base default
this.defaultOptions = Object.assign({}, baseDefaultOptions, newDefaultOptions);
}
/**
* <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.
* @param {boolean} [options.deduplicateResources] - Generate $id for resources and
* if a resources appears multiple times in the object graph only the first instance is
* serialized in full, subsequent instances are replaced with a reference to the $id
* @param {boolean} [options.convertResourcesToId] - Convert resources that
* are specified for relationship fields into their id, false by default.
* @param {number} [options.utcOffset] - UTC Offset for DateTime values.
* @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 Typed)) {
throw new Error(Globalize.formatMessage('serializer-tojson-notcobject'));
}
const parameters = {};
parameters.stack = new TypedStack(resource);
parameters.modelManager = this.modelManager;
parameters.seenResources = new Set();
parameters.dedupeResources = new Set();
const classDeclaration = this.modelManager.getType( resource.getFullyQualifiedType() );
// validate the resource against the model
options = options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions;
if(options.validate) {
const validator = new ResourceValidator(options);
classDeclaration.accept(validator, parameters);
}
const generator = new JSONGenerator(
options.convertResourcesToRelationships === true,
options.permitResourcesForRelationships === true,
options.deduplicateResources === true,
options.convertResourcesToId === true,
false,
options.utcOffset,
);
parameters.stack.clear();
parameters.stack.push(resource);
// this performs the conversion of the resouce into a standard JSON object
let result = classDeclaration.accept(generator, parameters);
return result;
}
/**
* 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.
* @param {boolean} options.validate - validate the structure of the Resource
* with its model prior to serialization (default to true)
* @param {number} [options.utcOffset] - UTC Offset for DateTime values.
* @param {boolean} [options.strictQualifiedDateTimes] - Only allow fully-qualified date-times with offsets.
* @return {Resource} The new populated resource
*/
fromJSON(jsonObject, options) {
// set default options
options = options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions;
if(!jsonObject.$class) {
throw new Error('Invalid JSON data. Does not contain a $class type identifier.');
}
const classDeclaration = this.modelManager.getType(jsonObject.$class);
// create a new instance, using the identifier field name as the ID.
let resource;
if (classDeclaration.isTransaction?.()) {
resource = this.factory.newTransaction(classDeclaration.getNamespace(),
classDeclaration.getName(),
jsonObject[classDeclaration.getIdentifierFieldName()] );
} else if (classDeclaration.isEvent?.()) {
resource = this.factory.newEvent(classDeclaration.getNamespace(),
classDeclaration.getName(),
jsonObject[classDeclaration.getIdentifierFieldName()] );
} else if (classDeclaration.isConcept?.()) {
resource = this.factory.newConcept(classDeclaration.getNamespace(),
classDeclaration.getName(),
jsonObject[classDeclaration.getIdentifierFieldName()] );
} else if (classDeclaration.isMapDeclaration?.()) {
throw new Error('Attempting to create a Map declaration is not supported.');
} else if (classDeclaration.isEnum()) {
throw new Error('Attempting to create an ENUM declaration is not supported.');
} else {
resource = this.factory.newResource( classDeclaration.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, false, options.utcOffset, options.strictQualifiedDateTimes === true);
classDeclaration.accept(populator, parameters);
// validate the resource against the model
if(options.validate) {
resource.validate();
}
return resource;
}
}
module.exports = Serializer;