Skip to content

Commit

Permalink
feat(map): add serialisation for map<string, string> (#654)
Browse files Browse the repository at this point in the history
* feat(map): add serialization for map<string, string>

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): add type defintions

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): bump test coverage

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): fix

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): fix

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): refactor validation visit

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): test coverage

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): test coverage

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): fix

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): make property optional

Signed-off-by: jonathan.casey <[email protected]>

* move check to validation and add test coverage

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): validates against dollar class property

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): update test cases

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): fixes grammar / spelling

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): pr comment - refactor expression

Signed-off-by: jonathan.casey <[email protected]>

* feat(map): uses optional chaining for null check

Signed-off-by: jonathan.casey <[email protected]>

* test(map): add json deserialization tests

Signed-off-by: Matt Roberts <[email protected]>

---------

Signed-off-by: jonathan.casey <[email protected]>
Signed-off-by: Matt Roberts <[email protected]>
Co-authored-by: Matt Roberts <[email protected]>
  • Loading branch information
jonathan-casey and mttrbrts authored Jul 17, 2023
1 parent 7926fae commit 39bae78
Show file tree
Hide file tree
Showing 19 changed files with 475 additions and 49 deletions.
20 changes: 16 additions & 4 deletions packages/concerto-core/lib/modelutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,27 +191,39 @@ class ModelUtil {
}

/**
* Returns the true if the given field is an enumerated type
* Returns true if the given field is an enumerated type
* @param {Field} field - the string
* @return {boolean} true if the field is declared as an enumeration
* @private
*/
static isEnum(field) {
const modelFile = field.getParent().getModelFile();
const typeDeclaration = modelFile.getType(field.getType());
return (typeDeclaration !== null && typeDeclaration.isEnum());
return typeDeclaration?.isEnum();
}

/**
* Returns the true if the given field is a Scalar type
* Returns true if the given field is an map type
* @param {Field} field - the string
* @return {boolean} true if the field is declared as an map
* @private
*/
static isMap(field) {
const modelFile = field.getParent().getModelFile();
const typeDeclaration = modelFile.getType(field.getType());
return typeDeclaration?.isMapDeclaration?.();
}

/**
* Returns true if the given field is a Scalar type
* @param {Field} field - the Field to test
* @return {boolean} true if the field is declared as an scalar
* @private
*/
static isScalar(field) {
const modelFile = field.getParent().getModelFile();
const declaration = modelFile.getType(field.getType());
return (declaration !== null && declaration.isScalarDeclaration?.());
return declaration?.isScalarDeclaration?.();
}

/**
Expand Down
8 changes: 5 additions & 3 deletions packages/concerto-core/lib/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,20 @@ class Serializer {

// create a new instance, using the identifier field name as the ID.
let resource;
if (classDeclaration.isTransaction()) {
if (classDeclaration.isTransaction?.()) {
resource = this.factory.newTransaction(classDeclaration.getNamespace(),
classDeclaration.getName(),
jsonObject[classDeclaration.getIdentifierFieldName()] );
} else if (classDeclaration.isEvent()) {
} else if (classDeclaration.isEvent?.()) {
resource = this.factory.newEvent(classDeclaration.getNamespace(),
classDeclaration.getName(),
jsonObject[classDeclaration.getIdentifierFieldName()] );
} else if (classDeclaration.isConcept()) {
} 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 {
Expand Down
53 changes: 34 additions & 19 deletions packages/concerto-core/lib/serializer/instancegenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class InstanceGenerator {
visit(thing, parameters) {
if (thing.isClassDeclaration?.()) {
return this.visitClassDeclaration(thing, parameters);
} else if (thing.isMapDeclaration?.()) {
return this.visitMapDeclaration(thing, parameters);
} else if (thing.isRelationship?.()) {
return this.visitRelationshipDeclaration(thing, parameters);
} else if (thing.isTypeScalar?.()) {
Expand Down Expand Up @@ -152,31 +154,33 @@ class InstanceGenerator {
}
}

let classDeclaration = parameters.modelManager.getType(type);
let declaration = parameters.modelManager.getType(type);

if (classDeclaration.isEnum()) {
let enumValues = classDeclaration.getOwnProperties();
if (declaration.isEnum()) {
let enumValues = declaration.getOwnProperties();
return parameters.valueGenerator.getEnum(enumValues).getName();
}

classDeclaration = this.findConcreteSubclass(classDeclaration);
declaration = this.findConcreteSubclass(declaration);

let id = null;
if (classDeclaration.isIdentified()) {
let idFieldName = classDeclaration.getIdentifierFieldName();
let idField = classDeclaration.getProperty(idFieldName);
if (idField?.isTypeScalar?.()){
idField = idField.getScalarField();
}
if(idField?.validator?.regex){
id = parameters.valueGenerator.getRegex(fieldOrScalarDeclaration.validator.regex);
} else {
id = this.generateRandomId(classDeclaration);
if (!declaration.isMapDeclaration?.()) {
let id = null;
if (declaration.isIdentified()) {
let idFieldName = declaration.getIdentifierFieldName();
let idField = declaration.getProperty(idFieldName);
if (idField?.isTypeScalar?.()){
idField = idField.getScalarField();
}
if(idField?.validator?.regex){
id = parameters.valueGenerator.getRegex(fieldOrScalarDeclaration.validator.regex);
} else {
id = this.generateRandomId(declaration);
}
}
let resource = parameters.factory.newResource(declaration.getNamespace(), declaration.getName(), id);
parameters.stack.push(resource);
}
let resource = parameters.factory.newResource(classDeclaration.getNamespace(), classDeclaration.getName(), id);
parameters.stack.push(resource);
return classDeclaration.accept(this, parameters);
return declaration.accept(this, parameters);
}

/**
Expand All @@ -190,7 +194,7 @@ class InstanceGenerator {
* @throws {Error} if no concrete subclasses exist.
*/
findConcreteSubclass(declaration) {
if (!declaration.isAbstract()) {
if (declaration.isMapDeclaration?.() || !declaration.isAbstract()) {
return declaration;
}

Expand Down Expand Up @@ -229,6 +233,17 @@ class InstanceGenerator {
}
}

/**
* Visitor design pattern
* @param {MapDeclaration} mapDeclaration - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visitMapDeclaration(mapDeclaration, parameters) {
return parameters.valueGenerator.getMap();
}

/**
* Generate a random ID for a given type.
* @private
Expand Down
21 changes: 20 additions & 1 deletion packages/concerto-core/lib/serializer/jsongenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class JSONGenerator {
return this.visitClassDeclaration(thing, parameters);
} else if (thing.isRelationship?.()) {
return this.visitRelationshipDeclaration(thing, parameters);
}else if (thing.isMapDeclaration?.()) {
return this.visitMapDeclaration(thing, parameters);
} else if (thing.isTypeScalar?.()) {
return this.visitField(thing.getScalarField(), parameters);
} else if (thing.isField?.()) {
Expand All @@ -74,6 +76,18 @@ class JSONGenerator {
}
}

/**
* Visitor design pattern
* @param {MapDeclaration} mapDeclaration - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visitMapDeclaration(mapDeclaration, parameters) {
const obj = parameters.stack.pop();
return { $class: obj.$class, value: Object.fromEntries(obj.value)};
}

/**
* Visitor design pattern
* @param {ClassDeclaration} classDeclaration - the object being visited
Expand Down Expand Up @@ -148,7 +162,12 @@ class JSONGenerator {
result = this.convertToJSON(field, obj);
} else if (ModelUtil.isEnum(field)) {
result = this.convertToJSON(field, obj);
} else {
} else if (ModelUtil.isMap(field)) {
parameters.stack.push(obj);
const mapDeclaration = parameters.modelManager.getType(field.getFullyQualifiedTypeName());
result = mapDeclaration.accept(this, parameters);
}
else {
parameters.stack.push(obj);
const classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType());
result = classDeclaration.accept(this, parameters);
Expand Down
59 changes: 42 additions & 17 deletions packages/concerto-core/lib/serializer/jsonpopulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ class JSONPopulator {

if (thing.isClassDeclaration?.()) {
return this.visitClassDeclaration(thing, parameters);
} else if (thing.isMapDeclaration?.()) {
return this.visitMapDeclaration(thing, parameters);
} else if (thing.isRelationship?.()) {
return this.visitRelationshipDeclaration(thing, parameters);
} else if (thing.isTypeScalar?.()) {
Expand Down Expand Up @@ -160,6 +162,29 @@ class JSONPopulator {
return resourceObj;
}

/**
* Visitor design pattern
* @param {MapDeclaration} mapDeclaration - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visitMapDeclaration(mapDeclaration, parameters) {
const jsonObj = parameters.jsonStack.pop();
parameters.path ?? (parameters.path = new TypedStack('$'));
const path = parameters.path.stack.join('');

if(!jsonObj.$class) {
throw new Error(`Invalid JSON data at "${path}". Map value does not contain a $class type identifier.`);
}

if(!jsonObj.value) {
throw new Error(`Invalid JSON data at "${path}". Map value does not contain a value property.`);
}

return { $class: jsonObj.$class, value: new Map(Object.entries(jsonObj.value)) };
}

/**
* Visitor design pattern
* @param {Field} field - the object being visited
Expand Down Expand Up @@ -197,7 +222,7 @@ class JSONPopulator {
convertItem(field, jsonItem, parameters) {
let result = null;

if(!field.isPrimitive() && !field.isTypeEnum()) {
if(!field.isPrimitive?.() && !field.isTypeEnum?.()) {
let typeName = jsonItem.$class;
if(!typeName) {
// If the type name is not specified in the data, then use the
Expand All @@ -207,26 +232,26 @@ class JSONPopulator {
}

// This throws if the type does not exist.
const classDeclaration = parameters.modelManager.getType(typeName);
const declaration = parameters.modelManager.getType(typeName);

// create a new instance, using the identifier field name as the ID.
let subResource = null;
if (!declaration.isMapDeclaration?.()) {

// if this is identifiable, then we create a resource
if(classDeclaration.isIdentified()) {
subResource = parameters.factory.newResource(classDeclaration.getNamespace(),
classDeclaration.getName(), jsonItem[classDeclaration.getIdentifierFieldName()] );
}
else {
// otherwise we create a concept
subResource = parameters.factory.newConcept(classDeclaration.getNamespace(),
classDeclaration.getName() );
}
// create a new instance, using the identifier field name as the ID.
let subResource = null;

result = subResource;
parameters.resourceStack.push(subResource);
// if this is identifiable, then we create a resource
if (declaration.isIdentified()) {
subResource = parameters.factory.newResource(declaration.getNamespace(),
declaration.getName(), jsonItem[declaration.getIdentifierFieldName()] );
} else {
// otherwise we create a concept
subResource = parameters.factory.newConcept(declaration.getNamespace(),
declaration.getName());
}
parameters.resourceStack.push(subResource);
}
parameters.jsonStack.push(jsonItem);
classDeclaration.accept(this, parameters);
result = declaration.accept(this, parameters);
}
else {
result = this.convertToObject(field, jsonItem, parameters);
Expand Down
54 changes: 51 additions & 3 deletions packages/concerto-core/lib/serializer/resourcevalidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ class ResourceValidator {
return this.visitEnumDeclaration(thing, parameters);
} else if (thing.isClassDeclaration?.()) {
return this.visitClassDeclaration(thing, parameters);
} else if (thing.isRelationship?.()) {
} else if (thing.isMapDeclaration?.()) {
return this.visitMapDeclaration(thing, parameters);
}else if (thing.isRelationship?.()) {
return this.visitRelationshipDeclaration(thing, parameters);
} else if (thing.isTypeScalar?.()) {
return this.visitField(thing.getScalarField(), parameters);
Expand Down Expand Up @@ -104,6 +106,36 @@ class ResourceValidator {
return null;
}

/**
* Visitor design pattern
*
* @param {MapDeclaration} mapDeclaration - the object being visited
* @param {Object} parameters - the parameter
* @private
*/
visitMapDeclaration(mapDeclaration, parameters) {
const obj = parameters.stack.pop();

if (!((obj.value instanceof Map))) {
throw new Error('Expected a Map, but found ' + JSON.stringify(obj));
}

if (obj.$class !== mapDeclaration.getFullyQualifiedName()) {
throw new Error(`$class value must match ${mapDeclaration.getFullyQualifiedName()}`);
}

obj.value.forEach((value, key) => {
if(!ModelUtil.isSystemProperty(key)) {
if (typeof key !== 'string') {
ResourceValidator.reportInvalidMap(parameters.rootResourceIdentifier, mapDeclaration, obj);
}
if (typeof value !== 'string') {
ResourceValidator.reportInvalidMap(parameters.rootResourceIdentifier, mapDeclaration, obj);
}
}
});
}

/**
* Visitor design pattern
* @param {ClassDeclaration} classDeclaration - the object being visited
Expand Down Expand Up @@ -450,7 +482,7 @@ class ResourceValidator {
/**
* Throw a new error for a model violation.
* @param {string} id - the identifier of this instance.
* @param {classDeclaration} classDeclaration - the declaration of the classs
* @param {ClassDeclaration} classDeclaration - the declaration of the class
* @param {Object} value - the value of the field.
* @private
*/
Expand All @@ -466,7 +498,23 @@ class ResourceValidator {
/**
* Throw a new error for a model violation.
* @param {string} id - the identifier of this instance.
* @param {RelationshipDeclaration} relationshipDeclaration - the declaration of the classs
* @param {MapDeclaration} mapDeclaration - the declaration of the map
* @param {Object} value - the value of the field.
* @private
*/
static reportInvalidMap(id, mapDeclaration, value) {
let formatter = Globalize.messageFormatter('resourcevalidator-invalidmap');
throw new ValidationException(formatter({
resourceId: id,
classFQN: mapDeclaration.getFullyQualifiedName(),
invalidValue: value.toString()
}));
}

/**
* Throw a new error for a model violation.
* @param {string} id - the identifier of this instance.
* @param {RelationshipDeclaration} relationshipDeclaration - the declaration of the class
* @param {Object} value - the value of the field.
* @private
*/
Expand Down
Loading

0 comments on commit 39bae78

Please sign in to comment.