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(serializer): add inferClass option #861

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
17,065 changes: 12,765 additions & 4,300 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/concerto-core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ class SecurityException extends BaseException {
class Serializer {
+ void constructor(Factory,ModelManager,object?)
+ void setDefaultOptions(Object)
+ Object toJSON(Resource,Object?,boolean?,boolean?,boolean?,boolean?,boolean?,number?) throws Error
+ Object toJSON(Resource,Object?,boolean?,boolean?,boolean?,boolean?,boolean?,number?,boolean?) throws Error
+ Resource fromJSON(Object,Object?,boolean,boolean,number?,boolean?)
}
class TypeNotFoundException extends BaseException {
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.17.2 {e4ee8bebe41decbd4ce1cabbf678c1a7} 2024-06-14
- Added `inferClass` option to Serializer.toJSON

Version 3.17.1 {ddc91ebd1ff660b421b60302d1e92271} 2024-06-21
- Added 'enableAliasedType' option to BaseModelManager
- Aliased types mapped to FQN in modelfile
Expand Down
3 changes: 1 addition & 2 deletions packages/concerto-core/lib/basemodelmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@
if (this.isStrict()) {
throw new MetamodelException(err.message);
} else {
console.warn('Invalid metamodel found. This will throw an exception in a future release. ', err.message);

Check warning on line 272 in packages/concerto-core/lib/basemodelmanager.js

View workflow job for this annotation

GitHub Actions / Unit Tests (16.x, macOS-latest)

Unexpected console statement

Check warning on line 272 in packages/concerto-core/lib/basemodelmanager.js

View workflow job for this annotation

GitHub Actions / Unit Tests (18.x, macOS-latest)

Unexpected console statement
}
}

Expand Down Expand Up @@ -602,9 +602,7 @@
* @throws {TypeNotFoundException} - if the type cannot be found or is a primitive type.
*/
getType(qualifiedName) {

const namespace = ModelUtil.getNamespace(qualifiedName);

const modelFile = this.getModelFile(namespace);
if (!modelFile) {
const formatter = Globalize.messageFormatter('modelmanager-gettype-noregisteredns');
Expand All @@ -614,6 +612,7 @@
}

const classDecl = modelFile.getType(qualifiedName);

if (!classDecl) {
const formatter = Globalize.messageFormatter('modelmanager-gettype-notypeinns');
throw new TypeNotFoundException(qualifiedName, formatter({
Expand Down
7 changes: 7 additions & 0 deletions packages/concerto-core/lib/introspect/mapkeytype.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,21 @@ class MapKeyType extends Decorated {
processType(ast) {
switch(ast.$class) {
case `${MetaModelNamespace}.DateTimeMapKeyType`:
case 'DateTimeMapKeyType':
this.type = 'DateTime';
break;
case `${MetaModelNamespace}.StringMapKeyType`:
case 'StringMapKeyType':
this.type = 'String';
break;
case `${MetaModelNamespace}.ObjectMapKeyType`:
case 'ObjectMapKeyType':
this.type = String(this.ast.type.name);
break;
default:
throw new IllegalModelException(
`Must be one of DateTimeMapKeyType, StringMapKeyType, ObjectMapKeyType. Invalid type: ${ast.$classe}, for MapDeclaration ${this.parent.name}`
);
}
}

Expand Down
10 changes: 9 additions & 1 deletion packages/concerto-core/lib/introspect/mapvaluetype.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ class MapValueType extends Decorated {
* @private
*/
processType(ast) {
let decl;
switch(this.ast.$class) {
case `${MetaModelNamespace}.ObjectMapValueType`:
case 'ObjectMapValueType':

// ObjectMapValueType must have TypeIdentifier.
if (!('type' in ast)) {
Expand All @@ -111,23 +111,31 @@ class MapValueType extends Decorated {

break;
case `${MetaModelNamespace}.BooleanMapValueType`:
case 'BooleanMapValueType':
this.type = 'Boolean';
break;
case `${MetaModelNamespace}.DateTimeMapValueType`:
case 'DateTimeMapValueType':
this.type = 'DateTime';
break;
case `${MetaModelNamespace}.StringMapValueType`:
case 'StringMapValueType':
this.type = 'String';
break;
case `${MetaModelNamespace}.IntegerMapValueType`:
case 'IntegerMapValueType':
this.type = 'Integer';
break;
case `${MetaModelNamespace}.LongMapValueType`:
case 'LongMapValueType':
this.type = 'Long';
break;
case `${MetaModelNamespace}.DoubleMapValueType`:
case 'DoubleMapValueType':
this.type = 'Double';
break;
default:
throw new IllegalModelException(`ObjectMapValueType $class is unsupport ${this.ast.$class} for declaration ${this.parent.name}`);
}
}

Expand Down
91 changes: 40 additions & 51 deletions packages/concerto-core/lib/introspect/modelfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@
const Decorated = require('./decorated');
const { Warning, ErrorCodes } = require('@accordproject/concerto-util');

const CONCEPT_STEREOTYPES = {
'Asset': AssetDeclaration,
'Transaction': TransactionDeclaration,
'Event': EventDeclaration,
'Participant': ParticipantDeclaration
};
const DECLARATIONS = {
'EnumDeclaration' : EnumDeclaration,
'MapDeclaration' : MapDeclaration,
'ConceptDeclaration': ConceptDeclaration
};
const SCALARS = ['BooleanScalar', 'IntegerScalar', 'LongScalar', 'DoubleScalar', 'StringScalar', 'DateTimeScalar'];

// Types needed for TypeScript generation.
/* eslint-disable no-unused-vars */
/* istanbul ignore next */
Expand Down Expand Up @@ -707,6 +720,15 @@
}
}

/**
* Populates from a Resource instance, created from the metamodel AST
* @param {object} resource - the Resource obtained from the parser
* @private
*/
fromResource(resource) {
console.log(resource.getFullyQualifiedType());

Check warning on line 729 in packages/concerto-core/lib/introspect/modelfile.js

View workflow job for this annotation

GitHub Actions / Unit Tests (16.x, macOS-latest)

Unexpected console statement

Check warning on line 729 in packages/concerto-core/lib/introspect/modelfile.js

View workflow job for this annotation

GitHub Actions / Unit Tests (18.x, macOS-latest)

Unexpected console statement
}

/**
* Populate from an AST
* @param {object} ast - the AST obtained from the parser
Expand Down Expand Up @@ -743,6 +765,8 @@
this.enforceImportVersioning(imp);
switch(imp.$class) {
case `${MetaModelNamespace}.ImportAll`:
case 'ImportAll':

if (this.getModelManager().isStrict()){
throw new Error('Wilcard Imports are not permitted in strict mode.');
}
Expand All @@ -755,6 +779,10 @@
this.importWildcardNamespaces.push(imp.namespace);
break;
case `${MetaModelNamespace}.ImportTypes`:
case 'ImportTypes':
imp.types.forEach( type => {
this.importShortNames.set(type, `${imp.namespace}.${type}`);
});
if (this.getModelManager().isAliasedTypeEnabled()) {
const aliasedTypes = new Map();
if (imp.aliasedTypes) {
Expand Down Expand Up @@ -802,65 +830,26 @@
for(let n=0; n < ast.declarations.length; n++) {
// Make sure to clone since we may add super type
let thing = Object.assign({}, ast.declarations[n]);
// we use short class names as the fully-qualified name may have been removed if we are 'inferClass' mode
const shortName = ModelUtil.getShortName(thing.$class);
const conceptStereoType = Object.keys(CONCEPT_STEREOTYPES).find(k => `${k}Declaration` === shortName);
const declaration = conceptStereoType ? undefined : Object.keys(DECLARATIONS).find(k => k === shortName);
const scalar = (conceptStereoType || declaration) ? undefined : SCALARS.includes(shortName);

if(thing.$class === `${MetaModelNamespace}.AssetDeclaration`) {
// Default super type for asset
if (!thing.superType) {
thing.superType = {
$class: `${MetaModelNamespace}.TypeIdentified`,
name: 'Asset',
};
}
this.declarations.push( new AssetDeclaration(this, thing) );
}
else if(thing.$class === `${MetaModelNamespace}.TransactionDeclaration`) {
// Default super type for transaction
if (!thing.superType) {
thing.superType = {
$class: `${MetaModelNamespace}.TypeIdentified`,
name: 'Transaction',
};
}
this.declarations.push( new TransactionDeclaration(this, thing) );
}
else if(thing.$class === `${MetaModelNamespace}.EventDeclaration`) {
// Default super type for event
if(conceptStereoType) {
if (!thing.superType) {
thing.superType = {
$class: `${MetaModelNamespace}.TypeIdentified`,
name: 'Event',
name: conceptStereoType,
};
}
this.declarations.push( new EventDeclaration(this, thing) );
}
else if(thing.$class === `${MetaModelNamespace}.ParticipantDeclaration`) {
// Default super type for participant
if (!thing.superType) {
thing.superType = {
$class: `${MetaModelNamespace}.TypeIdentified`,
name: 'Participant',
};
}
this.declarations.push( new ParticipantDeclaration(this, thing) );
}
else if(thing.$class === `${MetaModelNamespace}.EnumDeclaration`) {
this.declarations.push( new EnumDeclaration(this, thing) );
}
else if(thing.$class === `${MetaModelNamespace}.MapDeclaration`) {
this.declarations.push( new MapDeclaration(this, thing) );
this.declarations.push(new CONCEPT_STEREOTYPES[conceptStereoType](this, thing) );
}
else if(thing.$class === `${MetaModelNamespace}.ConceptDeclaration`) {
this.declarations.push( new ConceptDeclaration(this, thing) );
else if(declaration) {
this.declarations.push( new DECLARATIONS[declaration](this, thing) );
}
else if([
`${MetaModelNamespace}.BooleanScalar`,
`${MetaModelNamespace}.IntegerScalar`,
`${MetaModelNamespace}.LongScalar`,
`${MetaModelNamespace}.DoubleScalar`,
`${MetaModelNamespace}.StringScalar`,
`${MetaModelNamespace}.DateTimeScalar`,
].includes(thing.$class)) {
this.declarations.push( new ScalarDeclaration(this, thing) );
else if(scalar) {
this.declarations.push(new ScalarDeclaration(this, thing) );
}
else {
let formatter = Globalize('en').messageFormatter('modelfile-constructor-unrecmodelelem');
Expand Down
82 changes: 61 additions & 21 deletions packages/concerto-core/lib/modelutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ const reservedProperties = [
...privateReservedProperties
];

const MAP_KEYS = [
'StringMapKeyType',
'DateTimeMapKeyType',
'ObjectMapKeyType',
];

const MAP_VALUES = [
'BooleanMapValueType',
'DateTimeMapValueType',
'StringMapValueType',
'IntegerMapValueType',
'LongMapValueType',
'DoubleMapValueType',
'ObjectMapValueType'
];

/**
* Internal Model Utility Class
* <p><a href="./diagrams-private/modelutil.svg"><img src="./diagrams-private/modelutil.svg" style="height:100%;"/></a></p>
Expand All @@ -66,6 +82,40 @@ const reservedProperties = [
* @memberof module:concerto-core
*/
class ModelUtil {
/**
* Resolves the fully-qualified model name for a JSON object.
* @param {string} clazz the type name (FQN or short)
* @param {*} property a Property (which could be a Field or Relationship)
* @returns {string} the fully qualified name of the object
* @throws {Error} if a short type name has not been imported
*/
static qualifyTypeName(clazz, property) {
const ns = ModelUtil.getNamespace(clazz);
if (ns.length > 0) {
return clazz; // already FQN
}
else {
// a short name, we use the namespace from the type of the property
const fqn = property.getFullyQualifiedTypeName();
return ModelUtil.getFullyQualifiedName(ModelUtil.getNamespace(fqn), clazz);
}
}

/**
* Resolves the fully-qualified model name for a JSON object.
* @param {*} obj an object with an optional $class
* @param {*} property a Property (which could be a Field or Relationship)
* @returns {string} the fully qualified name of the object, based on its explicit $class
* or a $class inferred from the model
*/
static resolveFullyQualifiedTypeName(obj, property) {
if (obj.$class) {
return ModelUtil.qualifyTypeName(obj.$class, property);
}
else {
return property.getFullyQualifiedTypeName();
}
}
/**
* Returns everything after the last dot, if present, of the source string
* @param {string} fqn - the source string
Expand Down Expand Up @@ -117,17 +167,17 @@ class ModelUtil {
* @returns {ParseNamespaceResult} the result of parsing
*/
static parseNamespace(ns) {
if(!ns) {
if (!ns) {
throw new Error('Namespace is null or undefined.');
}

const parts = ns.split('@');
if(parts.length > 2) {
if (parts.length > 2) {
throw new Error(`Invalid namespace ${ns}`);
}

if(parts.length === 2) {
if(!semver.valid(parts[1])) {
if (parts.length === 2) {
if (!semver.valid(parts[1])) {
throw new Error(`Invalid namespace ${ns}`);
}
}
Expand Down Expand Up @@ -264,7 +314,7 @@ class ModelUtil {
* @returns {string} the fully qualified name minus the namespace version
*/
static removeNamespaceVersionFromFullyQualifiedName(fqn) {
if(ModelUtil.isPrimitiveType(fqn)) {
if (ModelUtil.isPrimitiveType(fqn)) {
return fqn;
}
const ns = ModelUtil.getNamespace(fqn);
Expand Down Expand Up @@ -302,11 +352,8 @@ class ModelUtil {
* @return {boolean} true if the Key is a valid Map Key
*/
static isValidMapKey(key) {
return [
`${MetaModelNamespace}.StringMapKeyType`,
`${MetaModelNamespace}.DateTimeMapKeyType`,
`${MetaModelNamespace}.ObjectMapKeyType`,
].includes(key.$class);
const shortName = ModelUtil.getShortName(key.$class);
return MAP_KEYS.includes(shortName);
}

/**
Expand All @@ -316,8 +363,8 @@ class ModelUtil {
* @return {boolean} true if the Key is a valid Map Key Scalar type
*/
static isValidMapKeyScalar(decl) {
return (decl?.isScalarDeclaration?.() && decl?.ast.$class === `${MetaModelNamespace}.StringScalar`) ||
(decl?.isScalarDeclaration?.() && decl?.ast.$class === `${MetaModelNamespace}.DateTimeScalar`);
return (decl?.isScalarDeclaration?.() && decl?.ast.$class === `${MetaModelNamespace}.StringScalar`) ||
(decl?.isScalarDeclaration?.() && decl?.ast.$class === `${MetaModelNamespace}.DateTimeScalar`);
}

/**
Expand All @@ -327,15 +374,8 @@ class ModelUtil {
* @return {boolean} true if the Value is a valid Map Value
*/
static isValidMapValue(value) {
return [
`${MetaModelNamespace}.BooleanMapValueType`,
`${MetaModelNamespace}.DateTimeMapValueType`,
`${MetaModelNamespace}.StringMapValueType`,
`${MetaModelNamespace}.IntegerMapValueType`,
`${MetaModelNamespace}.LongMapValueType`,
`${MetaModelNamespace}.DoubleMapValueType`,
`${MetaModelNamespace}.ObjectMapValueType`
].includes(value.$class);
const shortName = ModelUtil.getShortName(value.$class);
return MAP_VALUES.includes(shortName);
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/concerto-core/lib/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const { utcOffset: defaultUtcOffset } = DateTimeUtil.setCurrentTime();
const baseDefaultOptions = {
validate: true,
utcOffset: defaultUtcOffset,
inferClass: false
};

// Types needed for TypeScript generation.
Expand Down Expand Up @@ -92,6 +93,8 @@ class Serializer {
* @param {boolean} [options.convertResourcesToId] - Convert resources that
* are specified for relationship fields into their id, false by default.
* @param {number} [options.utcOffset] - UTC Offset for DateTime values.
* @param {boolean} [options.inferClass] - Only create $class in JSON when it
* cannot be inferred from the model, false by default
* @return {Object} - The Javascript Object that represents the resource
* @throws {Error} - throws an exception if resource is not an instance of
* Resource or fails validation.
Expand Down Expand Up @@ -123,6 +126,7 @@ class Serializer {
options.convertResourcesToId === true,
false,
options.utcOffset,
options.inferClass === true
);

parameters.stack.clear();
Expand Down
Loading
Loading