UNPKG

11.4 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 Resource = require('../model/resource');
21const Identifiable = require('../model/identifiable');
22const Typed = require('../model/typed');
23const Concept = require('../model/concept');
24const ModelUtil = require('../modelutil');
25const Util = require('../util');
26
27/**
28 * Converts the contents of a Resource to JSON. The parameters
29 * object should contain the keys
30 * 'stack' - the TypedStack of objects being processed. It should
31 * start with a Resource.
32 * 'modelManager' - the ModelManager to use.
33 * @private
34 * @class
35 * @memberof module:concerto-core
36 */
37class JSONGenerator {
38
39 /**
40 * Constructor.
41 * @param {boolean} [convertResourcesToRelationships] Convert resources that
42 * are specified for relationship fields into relationships, false by default.
43 * @param {boolean} [permitResourcesForRelationships] Permit resources in the
44 * place of relationships (serializing them as resources), false by default.
45 * @param {boolean} [deduplicateResources] If resources appear several times
46 * in the object graph only the first instance is serialized, with only the $id
47 * written for subsequent instances, false by default.
48 * @param {boolean} [convertResourcesToId] Convert resources that
49 * are specified for relationship fields into their id, false by default.
50 * @param {boolean} [ergo] target ergo.
51 */
52 constructor(convertResourcesToRelationships, permitResourcesForRelationships, deduplicateResources, convertResourcesToId, ergo) {
53 this.convertResourcesToRelationships = convertResourcesToRelationships;
54 this.permitResourcesForRelationships = permitResourcesForRelationships;
55 this.deduplicateResources = deduplicateResources;
56 this.convertResourcesToId = convertResourcesToId;
57 this.ergo = ergo;
58 }
59
60 /**
61 * Visitor design pattern
62 * @param {Object} thing - the object being visited
63 * @param {Object} parameters - the parameter
64 * @return {Object} the result of visiting or null
65 * @private
66 */
67 visit(thing, parameters) {
68 if (thing instanceof ClassDeclaration) {
69 return this.visitClassDeclaration(thing, parameters);
70 } else if (thing instanceof RelationshipDeclaration) {
71 return this.visitRelationshipDeclaration(thing, parameters);
72 } else if (thing instanceof Field) {
73 return this.visitField(thing, parameters);
74 } else {
75 throw new Error('Unrecognised ' + JSON.stringify(thing));
76 }
77 }
78
79 /**
80 * Visitor design pattern
81 * @param {ClassDeclaration} classDeclaration - the object being visited
82 * @param {Object} parameters - the parameter
83 * @return {Object} the result of visiting or null
84 * @private
85 */
86 visitClassDeclaration(classDeclaration, parameters) {
87
88 const obj = parameters.stack.pop();
89 if (!((obj instanceof Resource) || (obj instanceof Concept))) {
90 throw new Error('Expected a Resource or a Concept, but found ' + obj);
91 }
92
93 let result = {};
94 let id = null;
95
96 if (obj instanceof Identifiable && this.deduplicateResources) {
97 id = obj.toURI();
98 if( parameters.dedupeResources.has(id)) {
99 return id;
100 }
101 else {
102 parameters.dedupeResources.add(id);
103 }
104 }
105
106 result.$class = classDeclaration.getFullyQualifiedName();
107 if(this.deduplicateResources && id) {
108 result.$id = id;
109 }
110
111 // Walk each property of the class declaration
112 const properties = classDeclaration.getProperties();
113 for (let index in properties) {
114 const property = properties[index];
115 const value = obj[property.getName()];
116 if (!Util.isNull(value)) {
117 parameters.stack.push(value);
118 result[property.getName()] = property.accept(this, parameters);
119 }
120 }
121
122 return result;
123 }
124
125 /**
126 * Visitor design pattern
127 * @param {Field} field - the object being visited
128 * @param {Object} parameters - the parameter
129 * @return {Object} the result of visiting or null
130 * @private
131 */
132 visitField(field, parameters) {
133 const obj = parameters.stack.pop();
134 let result;
135 if (field.isArray()) {
136 let array = [];
137 // Walk the object
138 for (let index in obj) {
139 const item = obj[index];
140 if (!field.isPrimitive() && !ModelUtil.isEnum(field)) {
141 parameters.stack.push(item, Typed);
142 const classDeclaration = parameters.modelManager.getType(item.getFullyQualifiedType());
143 array.push(classDeclaration.accept(this, parameters));
144 } else {
145 array.push(this.convertToJSON(field, item));
146 }
147 }
148 result = array;
149 } else if (field.isPrimitive()) {
150 result = this.convertToJSON(field, obj);
151 } else if (ModelUtil.isEnum(field)) {
152 if (this.ergo) {
153 // Boxes an enum value to the expected combination of sum types
154 const enumDeclaration = field.getParent().getModelFile().getType(field.getType());
155 const enumName = enumDeclaration.getFullyQualifiedName();
156 const properties = enumDeclaration.getProperties();
157 let either = { 'left' : obj };
158 for(let n=0; n < properties.length; n++) {
159 const property = properties[n];
160 if(property.getName() === obj) {
161 break;
162 } else {
163 either = { 'right' : either };
164 }
165 }
166 result = { 'type' : [enumName], 'data': either };
167 } else {
168 result = this.convertToJSON(field, obj);
169 }
170 } else {
171 parameters.stack.push(obj);
172 const classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType());
173 result = classDeclaration.accept(this, parameters);
174 }
175
176 return result;
177 }
178
179 /**
180 * Converts to JSON safe format.
181 *
182 * @param {Field} field - the field declaration of the object
183 * @param {Object} obj - the object to convert to text
184 * @return {Object} the text JSON safe representation
185 */
186 convertToJSON(field, obj) {
187 switch (field.getType()) {
188 case 'DateTime':
189 {
190 if (this.ergo) {
191 return obj;
192 } else {
193 return obj.isUtc() ? obj.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]') : obj.format('YYYY-MM-DDTHH:mm:ss.SSSZ');
194 }
195 }
196 case 'Integer':
197 case 'Long': {
198 if (this.ergo) {
199 return { nat: obj };
200 } else {
201 return obj;
202 }
203 }
204 case 'Double':
205 case 'Boolean':
206 default:
207 {
208 return obj;
209 }
210 }
211 }
212
213 /**
214 * Visitor design pattern
215 * @param {RelationshipDeclaration} relationshipDeclaration - the object being visited
216 * @param {Object} parameters - the parameter
217 * @return {Object} the result of visiting or null
218 * @private
219 */
220 visitRelationshipDeclaration(relationshipDeclaration, parameters) {
221 const obj = parameters.stack.pop();
222 let result;
223
224 if (relationshipDeclaration.isArray()) {
225 let array = [];
226 // walk the object
227 for (let index in obj) {
228 const item = obj[index];
229 if (this.permitResourcesForRelationships && item instanceof Resource) {
230 let fqi = item.getFullyQualifiedIdentifier();
231 if (parameters.seenResources.has(fqi)) {
232 let relationshipText = this.getRelationshipText(relationshipDeclaration, item);
233 array.push(relationshipText);
234 } else {
235 parameters.seenResources.add(fqi);
236 parameters.stack.push(item, Resource);
237 const classDecl = parameters.modelManager.getType(relationshipDeclaration.getFullyQualifiedTypeName());
238 array.push(classDecl.accept(this, parameters));
239 parameters.seenResources.delete(fqi);
240 }
241 } else {
242 let relationshipText = this.getRelationshipText(relationshipDeclaration, item);
243 array.push(relationshipText);
244 }
245 }
246 result = array;
247 } else if (this.permitResourcesForRelationships && obj instanceof Resource) {
248 let fqi = obj.getFullyQualifiedIdentifier();
249 if (parameters.seenResources.has(fqi)) {
250 let relationshipText = this.getRelationshipText(relationshipDeclaration, obj);
251 result = relationshipText;
252 } else {
253 parameters.seenResources.add(fqi);
254 parameters.stack.push(obj, Resource);
255 const classDecl = parameters.modelManager.getType(relationshipDeclaration.getFullyQualifiedTypeName());
256 result = classDecl.accept(this, parameters);
257 parameters.seenResources.delete(fqi);
258 }
259 } else {
260 let relationshipText = this.getRelationshipText(relationshipDeclaration, obj);
261 result = relationshipText;
262 }
263 return result;
264 }
265
266 /**
267 * Returns the persistent format for a relationship.
268 * @param {RelationshipDeclaration} relationshipDeclaration - the relationship being persisted
269 * @param {Identifiable} relationshipOrResource - the relationship or the resource
270 * @returns {string} the text to use to persist the relationship
271 */
272 getRelationshipText(relationshipDeclaration, relationshipOrResource) {
273 if (relationshipOrResource instanceof Resource) {
274 const allowRelationships =
275 this.convertResourcesToRelationships || this.permitResourcesForRelationships;
276 if (!allowRelationships) {
277 throw new Error('Did not find a relationship for ' + relationshipDeclaration.getFullyQualifiedTypeName() + ' found ' + relationshipOrResource);
278 }
279 }
280 if (this.convertResourcesToId) {
281 return relationshipOrResource.getIdentifier();
282 } else {
283 return relationshipOrResource.toURI();
284 }
285 }
286}
287
288module.exports = JSONGenerator;