UNPKG

12.7 kBJavaScriptView Raw
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'use strict';
16
17const ClassDeclaration = require('../introspect/classdeclaration');
18const Field = require('../introspect/field');
19const RelationshipDeclaration = require('../introspect/relationshipdeclaration');
20const Relationship = require('../model/relationship');
21const Util = require('../util');
22const ModelUtil = require('../modelutil');
23const ValidationException = require('./validationexception');
24const Moment = require('moment-mini');
25
26/**
27 * Check if a given property name is a system property, e.g. '$class'.
28 * @param {String} name property name.
29 * @return {boolean} true for a system property; otherwise false.
30 * @private
31 */
32function isSystemProperty(name) {
33 return name.startsWith('$');
34}
35
36/**
37 * Get all properties on a resource object that both have a value and are not system properties.
38 * @param {Object} resourceData JSON object representation of a resource.
39 * @return {Array} property names.
40 * @private
41 */
42function getAssignableProperties(resourceData) {
43 return Object.keys(resourceData).filter((property) => {
44 return !isSystemProperty(property) && !Util.isNull(resourceData[property]);
45 });
46}
47
48/**
49 * Assert that all resource properties exist in a given class declaration.
50 * @param {Array} properties Property names.
51 * @param {ClassDeclaration} classDeclaration class declaration.
52 * @throws {ValidationException} if any properties are not defined by the class declaration.
53 * @private
54 */
55function validateProperties(properties, classDeclaration) {
56 const expectedProperties = classDeclaration.getProperties().map((property) => property.getName());
57 const invalidProperties = properties.filter((property) => !expectedProperties.includes(property));
58 if (invalidProperties.length > 0) {
59 const errorText = `Unexpected properties for type ${classDeclaration.getFullyQualifiedName()}: ` +
60 invalidProperties.join(', ');
61 throw new ValidationException(errorText);
62 }
63}
64
65/**
66 * Populates a Resource with data from a JSON object graph. The JSON objects
67 * should be the result of calling Serializer.toJSON and then JSON.parse.
68 * The parameters object should contain the keys
69 * 'stack' - the TypedStack of objects being processed. It should
70 * start with the root object from JSON.parse.
71 * 'factory' - the Factory instance to use for creating objects.
72 * 'modelManager' - the ModelManager instance to use to resolve classes
73 * @private
74 * @class
75 * @memberof module:concerto-core
76 */
77class JSONPopulator {
78
79 /**
80 * Constructor.
81 * @param {boolean} [acceptResourcesForRelationships] Permit resources in the
82 * place of relationships, false by default.
83 * @param {boolean} [ergo] target ergo.
84 */
85 constructor(acceptResourcesForRelationships, ergo) {
86 this.acceptResourcesForRelationships = acceptResourcesForRelationships;
87 this.ergo = ergo;
88 }
89
90 /**
91 * Visitor design pattern
92 * @param {Object} thing - the object being visited
93 * @param {Object} parameters - the parameter
94 * @return {Object} the result of visiting or null
95 * @private
96 */
97 visit(thing, parameters) {
98 if (thing instanceof ClassDeclaration) {
99 return this.visitClassDeclaration(thing, parameters);
100 } else if (thing instanceof RelationshipDeclaration) {
101 return this.visitRelationshipDeclaration(thing, parameters);
102 } else if (thing instanceof Field) {
103 return this.visitField(thing, parameters);
104 } else {
105 throw new Error('Unrecognised ' + JSON.stringify(thing) );
106 }
107 }
108
109 /**
110 * Visitor design pattern
111 * @param {ClassDeclaration} classDeclaration - the object being visited
112 * @param {Object} parameters - the parameter
113 * @return {Object} the result of visiting or null
114 * @private
115 */
116 visitClassDeclaration(classDeclaration, parameters) {
117 const jsonObj = parameters.jsonStack.pop();
118 const resourceObj = parameters.resourceStack.pop();
119
120 const properties = getAssignableProperties(jsonObj);
121 validateProperties(properties, classDeclaration);
122
123 properties.forEach((property) => {
124 const value = jsonObj[property];
125 parameters.jsonStack.push(value);
126 const classProperty = classDeclaration.getProperty(property);
127 resourceObj[property] = classProperty.accept(this,parameters);
128 });
129
130 return resourceObj;
131 }
132
133 /**
134 * Visitor design pattern
135 * @param {Field} field - the object being visited
136 * @param {Object} parameters - the parameter
137 * @return {Object} the result of visiting or null
138 * @private
139 */
140 visitField(field, parameters) {
141 const jsonObj = parameters.jsonStack.pop();
142 let result = null;
143
144 if(field.isArray()) {
145 result = [];
146 for(let n=0; n < jsonObj.length; n++) {
147 const jsonItem = jsonObj[n];
148 result.push(this.convertItem(field,jsonItem, parameters));
149 }
150 }
151 else {
152 result = this.convertItem(field,jsonObj, parameters);
153 }
154
155 return result;
156 }
157
158 /**
159 *
160 * @param {Field} field - the field of the item being converted
161 * @param {Object} jsonItem - the JSON object of the item being converted
162 * @param {Object} parameters - the parameters
163 * @return {Object} - the populated object.
164 */
165 convertItem(field, jsonItem, parameters) {
166 let result = null;
167
168 if(!field.isPrimitive() && !field.isTypeEnum()) {
169 let typeName = jsonItem.$class;
170 if(!typeName) {
171 // If the type name is not specified in the data, then use the
172 // type name from the model. This will only happen in the case of
173 // a sub resource inside another resource.
174 typeName = field.getFullyQualifiedTypeName();
175 }
176
177 // This throws if the type does not exist.
178 const classDeclaration = parameters.modelManager.getType(typeName);
179
180 // create a new instance, using the identifier field name as the ID.
181 let subResource = null;
182
183 // if this is identifiable, then we create a resource
184 if(!classDeclaration.isConcept()) {
185 subResource = parameters.factory.newResource(classDeclaration.getNamespace(),
186 classDeclaration.getName(), jsonItem[classDeclaration.getIdentifierFieldName()] );
187 }
188 else {
189 // otherwise we create a concept
190 subResource = parameters.factory.newConcept(classDeclaration.getNamespace(),
191 classDeclaration.getName() );
192 }
193
194 result = subResource;
195 parameters.resourceStack.push(subResource);
196 parameters.jsonStack.push(jsonItem);
197 classDeclaration.accept(this, parameters);
198 }
199 else {
200 result = this.convertToObject(field,jsonItem);
201 }
202
203 return result;
204 }
205
206 /**
207 * Converts a primtive object to JSON text.
208 *
209 * @param {Field} field - the field declaration of the object
210 * @param {Object} json - the JSON object to convert to a Concerto Object
211 * @return {string} the text representation
212 */
213 convertToObject(field, json) {
214 let result = null;
215
216 switch(field.getType()) {
217 case 'DateTime':
218 if (Moment.isMoment(json)) {
219 result = json;
220 } else {
221 result = new Moment.parseZone(json);
222 }
223 break;
224 case 'Integer':
225 case 'Long':
226 result = this.ergo ? parseInt(json.nat) : parseInt(json);
227 break;
228 case 'Double':
229 result = parseFloat(json);
230 break;
231 case 'Boolean':
232 result = (json === true || json === 'true');
233 break;
234 case 'String':
235 result = json.toString();
236 break;
237 default: {
238 // everything else should be an enumerated value...
239 if (this.ergo) {
240 // unpack the enum
241 let current = json.data;
242 while (!current.left) {
243 current = current.right;
244 }
245 result = current.left;
246 } else {
247 result = json;
248 }
249 }
250 }
251 return result;
252 }
253
254 /**
255 * Visitor design pattern
256 * @param {RelationshipDeclaration} relationshipDeclaration - the object being visited
257 * @param {Object} parameters - the parameter
258 * @return {Object} the result of visiting or null
259 * @private
260 */
261 visitRelationshipDeclaration(relationshipDeclaration, parameters) {
262 const jsonObj = parameters.jsonStack.pop();
263 let result = null;
264
265 let typeFQN = relationshipDeclaration.getFullyQualifiedTypeName();
266 let defaultNamespace = ModelUtil.getNamespace(typeFQN);
267 if(!defaultNamespace) {
268 defaultNamespace = relationshipDeclaration.getNamespace();
269 }
270 let defaultType = ModelUtil.getShortName(typeFQN);
271
272 if(relationshipDeclaration.isArray()) {
273 result = [];
274 for(let n=0; n < jsonObj.length; n++) {
275 let jsonItem = jsonObj[n];
276 if (typeof jsonItem === 'string') {
277 result.push(Relationship.fromURI(parameters.modelManager, jsonItem, defaultNamespace, defaultType ));
278 } else {
279 if (!this.acceptResourcesForRelationships) {
280 throw new Error('Invalid JSON data. Found a value that is not a string: ' + jsonObj + ' for relationship ' + relationshipDeclaration);
281 }
282
283 // this isn't a relationship, but it might be an object!
284 if(!jsonItem.$class) {
285 throw new Error('Invalid JSON data. Does not contain a $class type identifier: ' + jsonItem + ' for relationship ' + relationshipDeclaration );
286 }
287
288 const classDeclaration = parameters.modelManager.getType(jsonItem.$class);
289
290 // create a new instance, using the identifier field name as the ID.
291 let subResource = parameters.factory.newResource(classDeclaration.getNamespace(),
292 classDeclaration.getName(), jsonItem[classDeclaration.getIdentifierFieldName()] );
293 parameters.jsonStack.push(jsonItem);
294 parameters.resourceStack.push(subResource);
295 classDeclaration.accept(this, parameters);
296 result.push(subResource);
297 }
298 }
299 }
300 else {
301 if (typeof jsonObj === 'string') {
302 result = Relationship.fromURI(parameters.modelManager, jsonObj, defaultNamespace, defaultType );
303 } else {
304 if (!this.acceptResourcesForRelationships) {
305 throw new Error('Invalid JSON data. Found a value that is not a string: ' + jsonObj + ' for relationship ' + relationshipDeclaration);
306 }
307
308 // this isn't a relationship, but it might be an object!
309 if(!jsonObj.$class) {
310 throw new Error('Invalid JSON data. Does not contain a $class type identifier: ' + jsonObj + ' for relationship ' + relationshipDeclaration );
311 }
312
313 const classDeclaration = parameters.modelManager.getType(jsonObj.$class);
314
315 // create a new instance, using the identifier field name as the ID.
316 let subResource = parameters.factory.newResource(classDeclaration.getNamespace(),
317 classDeclaration.getName(), jsonObj[classDeclaration.getIdentifierFieldName()] );
318 parameters.jsonStack.push(jsonObj);
319 parameters.resourceStack.push(subResource);
320 classDeclaration.accept(this, parameters);
321 result = subResource;
322 }
323 }
324 return result;
325 }
326}
327
328module.exports = JSONPopulator;