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 | ;
|
16 |
|
17 | const EventDeclaration = require('./introspect/eventdeclaration');
|
18 | const ConceptDeclaration = require('./introspect/conceptdeclaration');
|
19 | const EnumDeclaration = require('./introspect/enumdeclaration');
|
20 | const DateTimeUtil = require('./datetimeutil');
|
21 | const Globalize = require('./globalize');
|
22 | const JSONGenerator = require('./serializer/jsongenerator');
|
23 | const JSONPopulator = require('./serializer/jsonpopulator');
|
24 | const Typed = require('./model/typed');
|
25 | const ResourceValidator = require('./serializer/resourcevalidator');
|
26 | const TransactionDeclaration = require('./introspect/transactiondeclaration');
|
27 | const TypedStack = require('./serializer/typedstack');
|
28 |
|
29 | const { utcOffset: defaultUtcOffset } = DateTimeUtil.setCurrentTime();
|
30 | const baseDefaultOptions = {
|
31 | validate: true,
|
32 | ergo: false,
|
33 | utcOffset: defaultUtcOffset,
|
34 | };
|
35 |
|
36 | /**
|
37 | * Serialize Resources instances to/from various formats for long-term storage
|
38 | * (e.g. on the blockchain).
|
39 | *
|
40 | * @class
|
41 | * @memberof module:concerto-core
|
42 | */
|
43 | class Serializer {
|
44 | /**
|
45 | * Create a Serializer.
|
46 | * @param {Factory} factory - The Factory to use to create instances
|
47 | * @param {ModelManager} modelManager - The ModelManager to use for validation etc.
|
48 | * @param {object} [options] - Serializer options
|
49 | */
|
50 | constructor(factory, modelManager, options) {
|
51 | if(!factory) {
|
52 | throw new Error(Globalize.formatMessage('serializer-constructor-factorynull'));
|
53 | } else if(!modelManager) {
|
54 | throw new Error(Globalize.formatMessage('serializer-constructor-modelmanagernull'));
|
55 | }
|
56 |
|
57 | this.factory = factory;
|
58 | this.modelManager = modelManager;
|
59 | this.defaultOptions = Object.assign({}, baseDefaultOptions, options || {});
|
60 | this._isSerializer = true;
|
61 | }
|
62 |
|
63 | /**
|
64 | * Set the default options for the serializer.
|
65 | * @param {Object} newDefaultOptions The new default options for the serializer.
|
66 | */
|
67 | setDefaultOptions(newDefaultOptions) {
|
68 | // Combine the specified default options with the base default
|
69 | this.defaultOptions = Object.assign({}, baseDefaultOptions, newDefaultOptions);
|
70 | }
|
71 |
|
72 | /**
|
73 | * <p>
|
74 | * Convert a {@link Resource} to a JavaScript object suitable for long-term
|
75 | * peristent storage.
|
76 | * </p>
|
77 | * @param {Resource} resource - The instance to convert to JSON
|
78 | * @param {Object} [options] - the optional serialization options.
|
79 | * @param {boolean} [options.validate] - validate the structure of the Resource
|
80 | * with its model prior to serialization (default to true)
|
81 | * @param {boolean} [options.convertResourcesToRelationships] - Convert resources that
|
82 | * are specified for relationship fields into relationships, false by default.
|
83 | * @param {boolean} [options.permitResourcesForRelationships] - Permit resources in the
|
84 | * place of relationships (serializing them as resources), false by default.
|
85 | * @param {boolean} [options.deduplicateResources] - Generate $id for resources and
|
86 | * if a resources appears multiple times in the object graph only the first instance is
|
87 | * serialized in full, subsequent instances are replaced with a reference to the $id
|
88 | * @param {boolean} [options.convertResourcesToId] - Convert resources that
|
89 | * are specified for relationship fields into their id, false by default.
|
90 | * @param {number} [options.utcOffset] - UTC Offset for DateTime values.
|
91 | * @return {Object} - The Javascript Object that represents the resource
|
92 | * @throws {Error} - throws an exception if resource is not an instance of
|
93 | * Resource or fails validation.
|
94 | */
|
95 | toJSON(resource, options) {
|
96 | // correct instance type
|
97 | if(!(resource instanceof Typed)) {
|
98 | throw new Error(Globalize.formatMessage('serializer-tojson-notcobject'));
|
99 | }
|
100 |
|
101 | const parameters = {};
|
102 | parameters.stack = new TypedStack(resource);
|
103 | parameters.modelManager = this.modelManager;
|
104 | parameters.seenResources = new Set();
|
105 | parameters.dedupeResources = new Set();
|
106 | const classDeclaration = this.modelManager.getType( resource.getFullyQualifiedType() );
|
107 |
|
108 | // validate the resource against the model
|
109 | options = options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions;
|
110 | if(options.validate) {
|
111 | const validator = new ResourceValidator(options);
|
112 | classDeclaration.accept(validator, parameters);
|
113 | }
|
114 |
|
115 | const generator = new JSONGenerator(
|
116 | options.convertResourcesToRelationships === true,
|
117 | options.permitResourcesForRelationships === true,
|
118 | options.deduplicateResources === true,
|
119 | options.convertResourcesToId === true,
|
120 | options.ergo === true,
|
121 | options.utcOffset,
|
122 | );
|
123 |
|
124 | parameters.stack.clear();
|
125 | parameters.stack.push(resource);
|
126 |
|
127 | // this performs the conversion of the resouce into a standard JSON object
|
128 | let result = classDeclaration.accept(generator, parameters);
|
129 | return result;
|
130 | }
|
131 |
|
132 | /**
|
133 | * Create a {@link Resource} from a JavaScript Object representation.
|
134 | * The JavaScript Object should have been created by calling the
|
135 | * {@link Serializer#toJSON toJSON} API.
|
136 | *
|
137 | * The Resource is populated based on the JavaScript object.
|
138 | *
|
139 | * @param {Object} jsonObject The JavaScript Object for a Resource
|
140 | * @param {Object} options - the optional serialization options
|
141 | * @param {boolean} options.acceptResourcesForRelationships - handle JSON objects
|
142 | * in the place of strings for relationships, defaults to false.
|
143 | * @param {boolean} options.validate - validate the structure of the Resource
|
144 | * with its model prior to serialization (default to true)
|
145 | * @param {number} [options.utcOffset] - UTC Offset for DateTime values.
|
146 | * @return {Resource} The new populated resource
|
147 | */
|
148 | fromJSON(jsonObject, options) {
|
149 | // set default options
|
150 | options = options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions;
|
151 |
|
152 | if (options && options.ergo === true) {
|
153 | const theClass = jsonObject.$class.$coll[0];
|
154 | jsonObject = jsonObject.$data;
|
155 | jsonObject.$class = theClass;
|
156 | }
|
157 | if(!jsonObject.$class) {
|
158 | throw new Error('Invalid JSON data. Does not contain a $class type identifier.');
|
159 | }
|
160 |
|
161 | const classDeclaration = this.modelManager.getType(jsonObject.$class);
|
162 |
|
163 | // create a new instance, using the identifier field name as the ID.
|
164 | let resource;
|
165 | if (classDeclaration instanceof TransactionDeclaration) {
|
166 | resource = this.factory.newTransaction(classDeclaration.getNamespace(),
|
167 | classDeclaration.getName(),
|
168 | jsonObject[classDeclaration.getIdentifierFieldName()] );
|
169 | } else if (classDeclaration instanceof EventDeclaration) {
|
170 | resource = this.factory.newEvent(classDeclaration.getNamespace(),
|
171 | classDeclaration.getName(),
|
172 | jsonObject[classDeclaration.getIdentifierFieldName()] );
|
173 | } else if (classDeclaration instanceof ConceptDeclaration) {
|
174 | resource = this.factory.newConcept(classDeclaration.getNamespace(),
|
175 | classDeclaration.getName(),
|
176 | jsonObject[classDeclaration.getIdentifierFieldName()] );
|
177 | } else if (classDeclaration instanceof EnumDeclaration) {
|
178 | throw new Error('Attempting to create an ENUM declaration is not supported.');
|
179 | } else {
|
180 | resource = this.factory.newResource( classDeclaration.getNamespace(),
|
181 | classDeclaration.getName(),
|
182 | jsonObject[classDeclaration.getIdentifierFieldName()] );
|
183 | }
|
184 |
|
185 | // populate the resource based on the jsonObject
|
186 | // by walking the classDeclaration
|
187 | const parameters = {};
|
188 | parameters.jsonStack = new TypedStack(jsonObject);
|
189 | parameters.resourceStack = new TypedStack(resource);
|
190 | parameters.modelManager = this.modelManager;
|
191 | parameters.factory = this.factory;
|
192 | const populator = new JSONPopulator(options.acceptResourcesForRelationships === true, options.ergo === true, options.utcOffset);
|
193 | classDeclaration.accept(populator, parameters);
|
194 |
|
195 | // validate the resource against the model
|
196 | if(options.validate) {
|
197 | resource.validate();
|
198 | }
|
199 |
|
200 | return resource;
|
201 | }
|
202 |
|
203 | /**
|
204 | * Alternative instanceof that is reliable across different module instances
|
205 | * @see https://github.com/hyperledger/composer-concerto/issues/47
|
206 | *
|
207 | * @param {object} object - The object to test against
|
208 | * @returns {boolean} - True, if the object is an instance of a Serializer
|
209 | */
|
210 | static [Symbol.hasInstance](object){
|
211 | return typeof object !== 'undefined' && object !== null && Boolean(object._isSerializer);
|
212 | }
|
213 | }
|
214 |
|
215 | module.exports = Serializer;
|