Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(map): Extend Type support #677

Merged
merged 41 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2029833
feat(map): add to introspection API
jonathan-casey Aug 1, 2023
1539a64
feat(map): extend JsonGenerator for Map Types
jonathan-casey Aug 1, 2023
fcd0b20
feat(map): extend JsonPopulator for Map Types
jonathan-casey Aug 1, 2023
a3f247a
feat(map): extend ResourceValidator for Map Types
jonathan-casey Aug 1, 2023
4817266
feat(map): add test cases for Map serialisation <> deserialisation
jonathan-casey Aug 1, 2023
d0802b1
feat(map): cleanup
jonathan-casey Aug 1, 2023
916069d
feat(map): add type definitions
jonathan-casey Aug 1, 2023
6c23986
feat(map): satisfy version checker
jonathan-casey Aug 1, 2023
fb3c393
feat(map): remove variable assignment for chai import
jonathan-casey Aug 1, 2023
a788e44
feat(map): satisfy version checker
jonathan-casey Aug 1, 2023
34fbf74
feat(map): update test cases to match on error handling changes
jonathan-casey Aug 1, 2023
116b180
feat(map): cleanup
jonathan-casey Aug 1, 2023
4478693
feat(map): cleanup test cases
jonathan-casey Aug 1, 2023
ffe486a
test(map): add tests for missed cases
mttrbrts Aug 2, 2023
389449e
feat(map): fixes metamodel parsing for MapDeclarations
jonathan-casey Aug 3, 2023
bfbe382
feat(map): printer update reads new metamodel map shape
jonathan-casey Aug 3, 2023
9570985
feat(map): updates test data for new metamodel design
jonathan-casey Aug 3, 2023
57408b5
feat(map): initial rework of introspection
jonathan-casey Aug 4, 2023
42668f9
feat(map): add more test coverage
jonathan-casey Aug 9, 2023
5f627f8
feat(map): improve error messaging & cleanup
jonathan-casey Aug 9, 2023
b5d6c83
feat(map): adds more test coverage for MapKeyType introspection
jonathan-casey Aug 9, 2023
882bf5a
feat(map): adds more test coverage for MapValueType introspection
jonathan-casey Aug 9, 2023
85f8f10
feat(map): refactor serialization
jonathan-casey Aug 10, 2023
2f8d622
feat(map): refactor serialization test coverage
jonathan-casey Aug 10, 2023
427658f
feat(map): cleanup
jonathan-casey Aug 10, 2023
539e393
feat(map): DRY up code in the JSONPopulator
jonathan-casey Aug 10, 2023
bdeeb02
feat(map): adds coverage to new introspection functions
jonathan-casey Aug 10, 2023
b01e444
feat(map): adds type definitions
jonathan-casey Aug 10, 2023
9bce014
feat(map): updates changelog
jonathan-casey Aug 10, 2023
56204f6
fix: Private identifiers are only available when targeting ECMAScript…
jonathan-casey Aug 10, 2023
0d495d5
fix: updates changelog
jonathan-casey Aug 10, 2023
f72a3d0
feat(map): moves isValidMapKey & isValidMapValue to ModelUtils
jonathan-casey Aug 14, 2023
32951fc
feat(map): removes unreachable default case
jonathan-casey Aug 14, 2023
a4d3749
feat(map): removes unreachable code
jonathan-casey Aug 14, 2023
af42e11
feat(map): adds more test coverage
jonathan-casey Aug 14, 2023
926f91d
feat(map): remove dead code
jonathan-casey Aug 14, 2023
04b0798
feat(map): adds JSDoc @private
jonathan-casey Aug 14, 2023
d446e1d
feat(map): adds type defs & change log
jonathan-casey Aug 14, 2023
19dfcf7
feat(map): improve error message
jonathan-casey Aug 16, 2023
74cb997
feat(map): add util method
jonathan-casey Aug 16, 2023
08ce775
fix: update type def
jonathan-casey Aug 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/concerto-core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ class MapKeyType extends Decorated {
+ MapDeclaration getParent()
+ string getType()
+ String toString()
+ boolean isKey()
+ boolean isValue()
}
class MapValueType extends Decorated {
+ void constructor(MapDeclaration,Object) throws IllegalModelException
Expand All @@ -182,6 +184,8 @@ class MapValueType extends Decorated {
+ MapDeclaration getParent()
+ string getType()
+ String toString()
+ boolean isKey()
+ boolean isValue()
}
+ ModelManager newMetaModelManager()
+ object validateMetaModel()
Expand Down
3 changes: 3 additions & 0 deletions packages/concerto-core/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
# Note that the latest public API is documented using JSDocs and is available in api.txt.
#

Version 3.8.1 {794268f69b81f05f711d38a9ef1a7833} 2023-08-1
- Add To MapKeyType, MapValue functionality

Version 3.7.0 {a97cb6ebd45679354ba4da1940d2bb8d} 2023-05-19
- Add MapDeclaration, MapKeyType, AggregateValueType

Expand Down
18 changes: 18 additions & 0 deletions packages/concerto-core/lib/introspect/mapkeytype.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,24 @@ class MapKeyType extends Decorated {
toString() {
return 'MapKeyType {id=' + this.getType() + '}';
}

/**
* Returns true if this class is the definition of a Map Key.
*
* @return {boolean} true if the class is a Map Key
*/
isKey() {
return true;
}

/**
* Returns true if this class is the definition of a Map Value.
*
* @return {boolean} true if the class is a Map Value
*/
isValue() {
return false;
}
}

module.exports = MapKeyType;
18 changes: 18 additions & 0 deletions packages/concerto-core/lib/introspect/mapvaluetype.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,24 @@ class MapValueType extends Decorated {
return 'MapValueType {id=' + this.getType() + '}';
}

/**
* Returns true if this class is the definition of a Map Key.
*
* @return {boolean} true if the class is a Map Key
*/
isKey() {
return false;
}

/**
* Returns true if this class is the definition of a Map Value.
*
* @return {boolean} true if the class is a Map Value
*/
isValue() {
return true;
}

}

module.exports = MapValueType;
35 changes: 34 additions & 1 deletion packages/concerto-core/lib/serializer/jsongenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,40 @@ class JSONGenerator {
*/
visitMapDeclaration(mapDeclaration, parameters) {
const obj = parameters.stack.pop();
return Object.fromEntries(obj);

// initialise Map with $class property
let map = new Map([['$class',obj.get('$class')]]);

obj.forEach((value, key) => {

if (typeof key === 'object') {
let decl = mapDeclaration.getModelFile()
.getAllDeclarations()
.find(decl => decl.name === key.getType());

// convert declaration to JSON representation
parameters.stack.push(key);
const jsonKey = decl.accept(this, parameters);

key = JSON.stringify(jsonKey);
}

if (typeof value === 'object') {
let decl = mapDeclaration.getModelFile()
.getAllDeclarations()
.find(decl => decl.name === value.getType());

// convert declaration to JSON representation
parameters.stack.push(value);
const jsonValue = decl.accept(this, parameters);

value = JSON.stringify(jsonValue);
}

map.set(key, value);
});

return Object.fromEntries(map);
}

/**
Expand Down
57 changes: 54 additions & 3 deletions packages/concerto-core/lib/serializer/jsonpopulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,64 @@ class JSONPopulator {
* @private
*/
visitMapDeclaration(mapDeclaration, parameters) {
const jsonObj = parameters.jsonStack.pop();
let jsonObj = parameters.jsonStack.pop();
parameters.path ?? (parameters.path = new TypedStack('$'));

// throws if Map contains private reserved properties as keys.
// Throws if Map contains reserved properties - a Map containing reserved Properties should not be serialized.
getAssignableProperties(jsonObj, mapDeclaration);

return new Map(Object.entries(jsonObj));
jsonObj = new Map(Object.entries(jsonObj));

let map = new Map();

jsonObj.forEach((value, key) => {

if (key === '$class') {
map.set(key, value);
return;
}

// If its a Non-Primitive, its likely a ClassDeclaration which needs visiting.
if (!ModelUtil.isSystemProperty(key) && !ModelUtil.isPrimitiveType(mapDeclaration.getKey().getType())) {
// get the Key declaration.
let decl = mapDeclaration.getModelFile()
.getAllDeclarations()
.find(decl => decl.name === mapDeclaration.getKey().getType());

// parse the Object, and visit the declaration.
if (decl?.isClassDeclaration()) {
let subResource = parameters.factory.newConcept(decl.getNamespace(),
decl.getName(), decl.getIdentifierFieldName() );

parameters.jsonStack.push(JSON.parse(key));
parameters.resourceStack.push(subResource);

key = decl.accept(this, parameters);
}
}

if (!ModelUtil.isPrimitiveType(mapDeclaration.getValue().getType())) {
// get the Value declaration.
let decl = mapDeclaration.getModelFile()
.getAllDeclarations()
.find(decl => decl.name === mapDeclaration.getValue().getType());

// parse the Object, and visit the declaration.
if (decl?.isClassDeclaration() ) {
let subResource = parameters.factory.newConcept(decl.getNamespace(),
decl.getName(), decl.getIdentifierFieldName() );

parameters.jsonStack.push(JSON.parse(value));
parameters.resourceStack.push(subResource);

value = decl.accept(this, parameters);
}
}

map.set(key, value);
});

return map;
}

/**
Expand Down
98 changes: 73 additions & 25 deletions packages/concerto-core/lib/serializer/resourcevalidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const ModelUtil = require('../modelutil');
const ValidationException = require('./validationexception');
const Globalize = require('../globalize');

const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
dayjs.extend(utc);

/**
* <p>
* Validates a Resource or Field against the models defined in the ModelManager.
Expand Down Expand Up @@ -106,39 +110,99 @@ class ResourceValidator {
return null;
}


/**
* Check a Type that is declared as a Map Type.
* @param {Object} type - the type in scope for validation, can be MapTypeKey or MapTypeValue
* @param {Object} value - the object being validated
* @param {Object} parameters - the parameter
* @param {Map} mapDeclaration - the object being visited
* @private
*/
checkMapType(type, value, parameters, mapDeclaration, ) {

if (!ModelUtil.isPrimitiveType(type.getType())) {

// thing might be a Concept, Scalar String, Scalar DateTime
let thing = mapDeclaration.getModelFile()
.getAllDeclarations()
.find(decl => decl.name === type.getType());

// a ClassDeclaration used in the context of a Map Key must be identified.
if (type.isKey() && thing.isClassDeclaration() && !thing.isIdentified()) {
throw new Error('Map Key must be an Identified Class Declaration');
}

// if Key or Value is Scalar, get the Base Type of the Scalar for primitive validation.
if (ModelUtil.isScalar(mapDeclaration.getKey())) {
type = thing.getType();
}

if (thing.isClassDeclaration()) {
parameters.stack.push(value);
thing.accept(this, parameters);
return;
}
} else {
// otherwise its a primitive
type = type.getType();

}

// validate the primitive
switch(type) {
case 'String':
if (typeof value !== 'string') {
throw new Error(`Model violation in ${mapDeclaration.getFullyQualifiedName()}. Expected Type of String but found '${value}' instead.`);
}
break;
case 'DateTime':
if (!dayjs.utc(value).isValid()) {
throw new Error(`Model violation in ${mapDeclaration.getFullyQualifiedName()}. Expected Type of DateTime but found '${value}' instead.`);
}
break;
case 'Boolean':
if (typeof value !== 'boolean') {
const type = typeof value;
throw new Error(`Model violation in ${mapDeclaration.getFullyQualifiedName()}. Expected Type of Boolean but found ${type} instead, for value '${value}'.`);
}
break;
}
}

/**
* 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();

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

if(!obj.has('$class')) {
if (!obj.has('$class')) {
throw new Error('Invalid Map. Map must contain a properly formatted $class property');
}

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


obj.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);
}
if (!ModelUtil.isSystemProperty(key)) {
this.checkMapType(mapDeclaration.getKey(), key, parameters, mapDeclaration);
this.checkMapType(mapDeclaration.getValue(), value, parameters, mapDeclaration);
}
});

return null;
}

/**
Expand Down Expand Up @@ -500,22 +564,6 @@ class ResourceValidator {
}));
}

/**
* Throw a new error for a model violation.
* @param {string} id - the identifier of this instance.
* @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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* limitations under the License.
*/

namespace [email protected] //todo makek sure namesapce is same for all test concepts
namespace [email protected]

concept Person identified {}

Expand Down
Loading