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 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
16,974 changes: 12,731 additions & 4,243 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/concerto-core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,11 @@ 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?)
}
+ string qualifyTypeName() throws Error
+ string resolveFullyQualifiedTypeName()
class TypeNotFoundException extends BaseException {
+ void constructor(string,string|,string,string)
+ string getTypeName()
Expand Down
2 changes: 2 additions & 0 deletions packages/concerto-core/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
# Note that the latest public API is documented using JSDocs and is available in api.txt.
#

Version 3.16.8 {7b282538e0319c3872928e133560441e} 2024-06-14
- Added `inferClass` option to Serializer.toJSON

Version 3.16.7 {8f455df1e788c4994f423d6e236bee21} 2024-05-01
- Added missing `strictQualifiedDateTimes` option to Serializer.fromJSON
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 @@ -259,7 +259,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 262 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 262 in packages/concerto-core/lib/basemodelmanager.js

View workflow job for this annotation

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

Unexpected console statement

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

View workflow job for this annotation

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

Unexpected console statement

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

View workflow job for this annotation

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

Unexpected console statement

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

View workflow job for this annotation

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

Unexpected console statement

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

View workflow job for this annotation

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

Unexpected console statement
}
}

Expand Down Expand Up @@ -592,9 +592,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 @@ -604,6 +602,7 @@
}

const classDecl = modelFile.getType(qualifiedName);

if (!classDecl) {
const formatter = Globalize.messageFormatter('modelmanager-gettype-notypeinns');
throw new TypeNotFoundException(qualifiedName, formatter({
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
26 changes: 24 additions & 2 deletions packages/concerto-core/lib/serializer/jsongenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ class JSONGenerator {
* @param {boolean} [ergo] - Deprecated - This is a dummy parameter to avoid breaking any consumers. It will be removed in a future release.
* are specified for relationship fields into their id, false by default.
* @param {number} [utcOffset] UTC Offset for DateTime values.
* @param {boolean} [inferClass] Only include $class in JSON when it cannot be inferred from model
*/
constructor(convertResourcesToRelationships, permitResourcesForRelationships, deduplicateResources, convertResourcesToId, ergo, utcOffset) {
constructor(convertResourcesToRelationships, permitResourcesForRelationships, deduplicateResources, convertResourcesToId, ergo, utcOffset, inferClass) {
this.convertResourcesToRelationships = convertResourcesToRelationships;
this.permitResourcesForRelationships = permitResourcesForRelationships;
this.deduplicateResources = deduplicateResources;
this.convertResourcesToId = convertResourcesToId;
this.utcOffset = utcOffset || 0;
this.inferClass = inferClass;
}

/**
Expand Down Expand Up @@ -142,7 +144,21 @@ class JSONGenerator {
}
}

result.$class = classDeclaration.getFullyQualifiedName();
// if we are in the inferClass mode and this is not the root instance
// we attempt to remove the $class
if(this.inferClass && parameters.property) {
const propertyFqn = parameters.property.getFullyQualifiedTypeName();
const isResourceInstanceOfPropertyType = propertyFqn === obj.getFullyQualifiedType();
if(isResourceInstanceOfPropertyType === false) {
const objAndFieldSameNs = ModelUtil.getNamespace(propertyFqn) === obj.getNamespace();
result.$class = objAndFieldSameNs ? obj.getType() : obj.getFullyQualifiedType();
}
}
else {
// if we don't have a field, then we are dealing with the root object
result.$class = obj.getFullyQualifiedType();
}

if(this.deduplicateResources && id) {
result.$id = id;
}
Expand All @@ -154,6 +170,7 @@ class JSONGenerator {
const value = obj[property.getName()];
if (!Util.isNull(value)) {
parameters.stack.push(value);
parameters.property = property;
result[property.getName()] = property.accept(this, parameters);
}
}
Expand All @@ -178,6 +195,7 @@ class JSONGenerator {
const item = obj[index];
if (!field.isPrimitive() && !ModelUtil.isEnum(field)) {
parameters.stack.push(item, Typed);
parameters.property = field;
const classDeclaration = parameters.modelManager.getType(item.getFullyQualifiedType());
array.push(classDeclaration.accept(this, parameters));
} else {
Expand All @@ -192,11 +210,13 @@ class JSONGenerator {
} else if (ModelUtil.isMap(field)) {
parameters.stack.push(obj);
const mapDeclaration = parameters.modelManager.getType(field.getFullyQualifiedTypeName());
parameters.property = field;
result = mapDeclaration.accept(this, parameters);
}
else {
parameters.stack.push(obj);
const classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType());
parameters.property = field;
result = classDeclaration.accept(this, parameters);
}

Expand Down Expand Up @@ -256,6 +276,7 @@ class JSONGenerator {
parameters.seenResources.add(fqi);
parameters.stack.push(item, Resource);
const classDecl = parameters.modelManager.getType(relationshipDeclaration.getFullyQualifiedTypeName());
parameters.property = relationshipDeclaration;
array.push(classDecl.accept(this, parameters));
parameters.seenResources.delete(fqi);
}
Expand All @@ -274,6 +295,7 @@ class JSONGenerator {
parameters.seenResources.add(fqi);
parameters.stack.push(obj, Resource);
const classDecl = parameters.modelManager.getType(relationshipDeclaration.getFullyQualifiedTypeName());
parameters.property = relationshipDeclaration;
result = classDecl.accept(this, parameters);
parameters.seenResources.delete(fqi);
}
Expand Down
58 changes: 39 additions & 19 deletions packages/concerto-core/lib/serializer/jsonpopulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,41 @@
}
}

/**
* Resolves the fully-qualified model name for a JSON object.
* @param {string} clazz the type name (FQN or short)
* @param {*} field a Field (which could be a Relationship)
* @returns {string} the fully qualified name of the object
* @throws {Error} if a short type name has not been imported
*/
function qualifyTypeName(clazz, field) {
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 field
const fqn = field.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 {*} field a Field (which could be a Relationship)
* @returns {string} the fully qualified name of the object, based on its explicit $class
* or a $class inferred from the model
*/
function resolveFullyQualifiedTypeName(obj, field) {
if(obj.$class) {
return qualifyTypeName(obj.$class, field);
}
else {
return field.getFullyQualifiedTypeName();
}
}

/**
* Populates a Resource with data from a JSON object graph. The JSON objects
* should be the result of calling Serializer.toJSON and then JSON.parse.
Expand Down Expand Up @@ -105,7 +140,7 @@
this.strictQualifiedDateTimes = strictQualifiedDateTimes;

if (process.env.TZ){
console.warn(`Environment variable 'TZ' is set to '${process.env.TZ}', this can cause unexpected behaviour when using unqualified date time formats.`);

Check warning on line 143 in packages/concerto-core/lib/serializer/jsonpopulator.js

View workflow job for this annotation

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

Unexpected console statement

Check warning on line 143 in packages/concerto-core/lib/serializer/jsonpopulator.js

View workflow job for this annotation

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

Unexpected console statement

Check warning on line 143 in packages/concerto-core/lib/serializer/jsonpopulator.js

View workflow job for this annotation

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

Unexpected console statement

Check warning on line 143 in packages/concerto-core/lib/serializer/jsonpopulator.js

View workflow job for this annotation

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

Unexpected console statement

Check warning on line 143 in packages/concerto-core/lib/serializer/jsonpopulator.js

View workflow job for this annotation

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

Unexpected console statement

Check warning on line 143 in packages/concerto-core/lib/serializer/jsonpopulator.js

View workflow job for this annotation

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

Unexpected console statement
}
}

Expand Down Expand Up @@ -266,13 +301,7 @@
let result = null;

if(!field.isPrimitive?.() && !field.isTypeEnum?.()) {
let typeName = jsonItem.$class;
if(!typeName) {
// If the type name is not specified in the data, then use the
// type name from the model. This will only happen in the case of
// a sub resource inside another resource.
typeName = field.getFullyQualifiedTypeName();
}
const typeName = resolveFullyQualifiedTypeName(jsonItem, field);

// This throws if the type does not exist.
const declaration = parameters.modelManager.getType(typeName);
Expand Down Expand Up @@ -410,13 +439,8 @@
if (!this.acceptResourcesForRelationships) {
throw new Error('Invalid JSON data. Found a value that is not a string: ' + jsonObj + ' for relationship ' + relationshipDeclaration);
}

// this isn't a relationship, but it might be an object!
if(!jsonItem.$class) {
throw new Error('Invalid JSON data. Does not contain a $class type identifier: ' + jsonItem + ' for relationship ' + relationshipDeclaration );
}

const classDeclaration = parameters.modelManager.getType(jsonItem.$class);
const typeName = resolveFullyQualifiedTypeName(jsonItem, relationshipDeclaration);
const classDeclaration = parameters.modelManager.getType(typeName);

// create a new instance, using the identifier field name as the ID.
let subResource = parameters.factory.newResource(classDeclaration.getNamespace(),
Expand All @@ -436,11 +460,7 @@
throw new Error('Invalid JSON data. Found a value that is not a string: ' + jsonObj + ' for relationship ' + relationshipDeclaration);
}

// this isn't a relationship, but it might be an object!
if(!jsonObj.$class) {
throw new Error('Invalid JSON data. Does not contain a $class type identifier: ' + jsonObj + ' for relationship ' + relationshipDeclaration );
}
const classDeclaration = parameters.modelManager.getType(jsonObj.$class);
const classDeclaration = parameters.modelManager.getType(resolveFullyQualifiedTypeName(jsonObj, relationshipDeclaration));

// create a new instance, using the identifier field name as the ID.
let subResource = parameters.factory.newResource(classDeclaration.getNamespace(),
Expand Down
Loading
Loading