/* * 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 ClassDeclaration = require('../introspect/classdeclaration'); const Field = require('../introspect/field'); const RelationshipDeclaration = require('../introspect/relationshipdeclaration'); const Resource = require('../model/resource'); const Typed = require('../model/typed'); const Concept = require('../model/concept'); const Relationship = require('../model/relationship'); const ModelUtil = require('../modelutil'); const Util = require('../util'); /** * Converts the contents of a Resource to JSON. The parameters * object should contain the keys * 'writer' - the JSONWriter instance to use to accumulate the JSON text. * 'stack' - the TypedStack of objects being processed. It should * start with a Resource. * 'modelManager' - the ModelManager to use. * @private * @class * @memberof module:composer-common */ class JSONGenerator { /** * Constructor. * @param {boolean} [convertResourcesToRelationships] Convert resources that * are specified for relationship fields into relationships, false by default. * @param {boolean} [permitResourcesForRelationships] Permit resources in the * place of relationships (serializing them as resources), false by default. */ constructor(convertResourcesToRelationships, permitResourcesForRelationships) { this.convertResourcesToRelationships = convertResourcesToRelationships; this.permitResourcesForRelationships = permitResourcesForRelationships; } /** * Visitor design pattern * @param {Object} thing - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visit(thing, parameters) { if (thing instanceof ClassDeclaration) { return this.visitClassDeclaration(thing, parameters); } else if (thing instanceof RelationshipDeclaration) { return this.visitRelationshipDeclaration(thing, parameters); } else if (thing instanceof Field) { return this.visitField(thing, parameters); } else { throw new Error('Unrecognised ' + JSON.stringify(thing) ); } } /** * Visitor design pattern * @param {ClassDeclaration} classDeclaration - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visitClassDeclaration(classDeclaration, parameters) { parameters.writer.openObject(); parameters.writer.writeKeyStringValue('$class', classDeclaration.getFullyQualifiedName()); const obj = parameters.stack.pop(); if(!((obj instanceof Resource) || (obj instanceof Concept))) { throw new Error('Expected a Resource or a Concept, but found ' + obj ); } const properties = classDeclaration.getProperties(); for(let n=0; n < properties.length; n++) { const property = properties[n]; const value = obj[property.getName()]; if(!Util.isNull(value)) { parameters.stack.push(value); property.accept(this,parameters); } } parameters.writer.closeObject(); return null; } /** * Visitor design pattern * @param {Field} field - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visitField(field, parameters) { const obj = parameters.stack.pop(); parameters.writer.writeKey(field.getName()); if(field.isArray()) { parameters.writer.openArray(); for(let n=0; n < obj.length; n++) { const item = obj[n]; if(!field.isPrimitive() && !ModelUtil.isEnum(field)) { parameters.writer.writeComma(); parameters.stack.push(item, Typed); const classDecl = parameters.modelManager.getType(item.getFullyQualifiedType()); classDecl.accept(this, parameters); } else { parameters.writer.writeArrayValue(this.convertToJSON(field,item)); } } parameters.writer.closeArray(); } else if(field.isPrimitive() || ModelUtil.isEnum(field)) { parameters.writer.writeValue(this.convertToJSON(field,obj)); } else { parameters.stack.push(obj); const classDeclaration = parameters.modelManager.getType(field.getFullyQualifiedTypeName()); classDeclaration.accept(this, parameters); } return null; } /** * Converts a primtive object to JSON text. * * @param {Field} field - the field declaration of the object * @param {Object} obj - the object to convert to text * @return {string} the text representation */ convertToJSON(field, obj) { switch(field.getType()) { case 'DateTime': { return `"${obj.toISOString()}"`; } case 'Integer': case 'Long': case 'Double': case 'Boolean':{ return `${obj.toString()}`; } default: { return `"${obj.toString()}"`; } } } /** * Visitor design pattern * @param {RelationshipDeclaration} relationshipDeclaration - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visitRelationshipDeclaration(relationshipDeclaration, parameters) { const obj = parameters.stack.pop(); parameters.writer.writeKey(relationshipDeclaration.getName()); if(relationshipDeclaration.isArray()) { parameters.writer.openArray(); for(let n=0; n < obj.length; n++) { const item = obj[n]; if (this.permitResourcesForRelationships && item instanceof Resource) { let fqi = item.getFullyQualifiedIdentifier(); if (parameters.seenResources.has(fqi)) { let relationshipText = this.getRelationshipText(relationshipDeclaration, item ); parameters.writer.writeStringValue(relationshipText); } else { parameters.seenResources.add(fqi); parameters.writer.writeComma(); parameters.stack.push(item, Resource); const classDecl = parameters.modelManager.getType(relationshipDeclaration.getFullyQualifiedTypeName()); classDecl.accept(this, parameters); parameters.seenResources.delete(fqi); } } else { let relationshipText = this.getRelationshipText(relationshipDeclaration, item ); parameters.writer.writeArrayStringValue(relationshipText); } } parameters.writer.closeArray(); } else if (this.permitResourcesForRelationships && obj instanceof Resource) { let fqi = obj.getFullyQualifiedIdentifier(); if (parameters.seenResources.has(fqi)) { let relationshipText = this.getRelationshipText(relationshipDeclaration, obj ); parameters.writer.writeStringValue(relationshipText); } else { parameters.seenResources.add(fqi); parameters.stack.push(obj, Resource); const classDecl = parameters.modelManager.getType(relationshipDeclaration.getFullyQualifiedTypeName()); classDecl.accept(this, parameters); parameters.seenResources.delete(fqi); } } else { let relationshipText = this.getRelationshipText(relationshipDeclaration, obj ); parameters.writer.writeStringValue(relationshipText); } return null; } /** * * Returns the persistent format for a relationship. * @param {RelationshipDeclaration} relationshipDeclaration - the relationship being persisted * @param {Relationship} relationship - the text for the item * @returns {string} the text to use to persist the relationship */ getRelationshipText(relationshipDeclaration, relationship) { let identifiable; if(relationship instanceof Relationship) { identifiable = relationship; } else if (this.convertResourcesToRelationships && relationship instanceof Resource) { identifiable = relationship; } else if (this.permitResourcesForRelationships && relationship instanceof Resource) { identifiable = relationship; } else { throw new Error('Did not find a relationship for ' + relationshipDeclaration.getFullyQualifiedTypeName() + ' found ' + relationship ); } let relationshipText = identifiable.getIdentifier(); if(relationshipDeclaration.getNamespace() !== identifiable.getNamespace() ) { relationshipText = identifiable.getFullyQualifiedIdentifier(); } return relationshipText; } } module.exports = JSONGenerator;