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(*): Support string length validation #647

Merged
merged 8 commits into from
May 22, 2023
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace [email protected]

concept Thing {
o Integer number
o String string length=[2,10]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace [email protected]

concept Thing {
o Integer number
o String string length=[1,10]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace [email protected]

concept Thing {
o Integer number
o String string length=[3,10]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace [email protected]

concept Thing {
o Integer number
o String string length=[2,100]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace [email protected]

concept Thing {
o Integer number
o String string length=[2,8]
}
62 changes: 62 additions & 0 deletions packages/concerto-analysis/test/unit/compare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,65 @@ test('should detect a string validator being changed on a property', async () =>
]));
expect(results.result).toBe(CompareResult.MAJOR);
});

test('should detect a string length validator being added to a property', async () => {
const [a, b] = await getModelFiles('validators.cto', 'string-validator-length-added.cto');
const results = new Compare().compare(a, b);
expect(results.findings).toEqual(expect.arrayContaining([
expect.objectContaining({
key: 'property-validator-added',
message: 'A string validator was added to the field "string" in the concept "Thing"'
})
]));
expect(results.result).toBe(CompareResult.MAJOR);
});

test('should detect a string length validator being removed from a property', async () => {
const [a, b] = await getModelFiles('string-validator-length-added.cto', 'validators.cto');
const results = new Compare().compare(a, b);
expect(results.findings).toEqual(expect.arrayContaining([
expect.objectContaining({
key: 'property-validator-removed',
message: 'A string validator was removed from the field "string" in the concept "Thing"'
})
]));
expect(results.result).toBe(CompareResult.PATCH);
});

test('should not detect a string length validator being changed on a property (compatible minLength bound)', async () => {
const [a, b] = await getModelFiles('string-validator-length-added.cto', 'string-validator-length-changed-lowercompat.cto');
const results = new Compare().compare(a, b);
expect(results.findings).toEqual([]);
expect(results.result).toBe(CompareResult.NONE);
});

test('should detect a string length validator being changed on a property (incompatible minLength bound)', async () => {
const [a, b] = await getModelFiles('string-validator-length-added.cto', 'string-validator-length-changed-lowerincompat.cto');
const results = new Compare().compare(a, b);
expect(results.findings).toEqual(expect.arrayContaining([
expect.objectContaining({
key: 'property-validator-changed',
message: 'A string validator for the field "string" in the concept "Thing" was changed and is no longer compatible'
})
]));
expect(results.result).toBe(CompareResult.MAJOR);
});

test('should not detect a string validator being changed on a property (compatible maxLength bound)', async () => {
const [a, b] = await getModelFiles('string-validator-length-added.cto', 'string-validator-length-changed-uppercompat.cto');
const results = new Compare().compare(a, b);
expect(results.findings).toEqual([]);
expect(results.result).toBe(CompareResult.NONE);
});

test('should detect a string validator being changed on a property (incompatible maxLength bound)', async () => {
const [a, b] = await getModelFiles('string-validator-length-added.cto', 'string-validator-length-changed-upperincompat.cto');
const results = new Compare().compare(a, b);
expect(results.findings).toEqual(expect.arrayContaining([
expect.objectContaining({
key: 'property-validator-changed',
message: 'A string validator for the field "string" in the concept "Thing" was changed and is no longer compatible'
})
]));
expect(results.result).toBe(CompareResult.MAJOR);
});
5 changes: 3 additions & 2 deletions packages/concerto-core/lib/introspect/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ class Field extends Property {
}
break;
case 'String':
if (this.ast.validator) {
if (this.ast.validator || this.ast.lengthValidator) {
this.validator = new StringValidator(
this,
this.ast.validator
this.ast.validator,
this.ast.lengthValidator
);
}
break;
Expand Down
4 changes: 2 additions & 2 deletions packages/concerto-core/lib/introspect/scalardeclaration.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ class ScalarDeclaration extends Declaration {
}
break;
case 'String':
if(this.ast.validator) {
this.validator = new StringValidator(this, this.ast.validator);
if(this.ast.validator || this.ast.lengthValidator) {
this.validator = new StringValidator(this, this.ast.validator, this.ast.lengthValidator);
}
break;
}
Expand Down
93 changes: 80 additions & 13 deletions packages/concerto-core/lib/introspect/stringvalidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

'use strict';

const { isNull } = require('../util');
const Validator = require('./validator');

// Types needed for TypeScript generation.
Expand All @@ -37,17 +38,40 @@ class StringValidator extends Validator{
* Create a StringValidator.
* @param {Object} field - the field or scalar declaration this validator is attached to
* @param {Object} validator - The validation string. This must be a regex
* @param {Object} lengthValidator - The length validation string - [minLength,maxLength] (inclusive).
*
* @throws {IllegalModelException}
*/
constructor(field, validator) {
constructor(field, validator, lengthValidator) {
super(field, validator);
try {
const CustomRegExp = field?.parent?.getModelFile()?.getModelManager()?.options?.regExp || RegExp;
this.regex = new CustomRegExp(validator.pattern, validator.flags);
this.minLength = null;
this.maxLength = null;
this.regex = null;

if (lengthValidator) {
this.minLength = lengthValidator?.minLength;
this.maxLength = lengthValidator?.maxLength;

if(this.minLength === null && this.maxLength === null) {
// can't specify no upper and lower value
this.reportError(field.getName(), 'Invalid string length, minLength and-or maxLength must be specified.');
} else if (this.minLength < 0 || this.maxLength < 0) {
this.reportError(field.getName(), 'minLength and-or maxLength must be positive integers.');
} else if (this.minLength === null || this.maxLength === null) {
// this is fine and means that we don't need to check whether minLength > maxLength
} else if(this.minLength > this.maxLength) {
this.reportError(field.getName(), 'minLength must be less than or equal to maxLength.');
}
}
catch(exception) {
this.reportError(field.getName(), exception.message);

if (validator) {
try {
const CustomRegExp = field?.parent?.getModelFile()?.getModelManager()?.options?.regExp || RegExp;
this.regex = new CustomRegExp(validator.pattern, validator.flags);
}
catch(exception) {
this.reportError(field.getName(), exception.message);
}
}
}

Expand All @@ -60,14 +84,37 @@ class StringValidator extends Validator{
*/
validate(identifier, value) {
if(value !== null) {
if(!this.regex.test(value)) {
this.reportError(identifier, 'Value \'' + value + '\' failed to match validation regex: ' + this.regex);
//Enforce string length rule first
if(this.minLength !== null && value.length < this.minLength) {
this.reportError(identifier, `The string length of '${value}' should be at least ${this.minLength} characters.`);
}
if(this.maxLength !== null && value.length > this.maxLength) {
this.reportError(identifier, `The string length of '${value}' should not exceed ${this.maxLength} characters.`);
}

if (this.regex && !this.regex.test(value)) {
this.reportError(identifier, `Value '${value}' failed to match validation regex: ${this.regex}`);
}
}
}

/**
* Returns the RegExp object associated with the string validator
* Returns the minLength for this validator, or null if not specified
* @returns {number} the min length or null
*/
getMinLength() {
return this.minLength;
}
/**
* Returns the maxLength for this validator, or null if not specified
* @returns {number} the max length or null
*/
getMaxLength() {
return this.maxLength;
}

/**
* Returns the RegExp object associated with the string validator, or null if not specified
* @returns {RegExp} the RegExp object
*/
getRegex() {
Expand All @@ -85,13 +132,33 @@ class StringValidator extends Validator{
compatibleWith(other) {
if (!(other instanceof StringValidator)) {
return false;
} else if (this.validator.pattern !== other.validator.pattern) {
}

if (this.validator?.pattern !== other.validator?.pattern) {
return false;
} else if (this.validator?.flags !== other.validator?.flags) {
return false;
} else if (this.validator.flags !== other.validator.flags) {
}

const thisMinLength = this.getMinLength();
const otherMinLength = other.getMinLength();
if (isNull(thisMinLength) && !isNull(otherMinLength)) {
return false;
} else if (!isNull(thisMinLength) && !isNull(otherMinLength)) {
if (thisMinLength < otherMinLength) {
return false;
}
}
const thisMaxLength = this.getMaxLength();
const otherMaxLength = other.getMaxLength();
if (isNull(thisMaxLength) && !isNull(otherMaxLength)) {
return false;
} else {
return true;
} else if (!isNull(thisMaxLength) && !isNull(otherMaxLength)) {
if (thisMaxLength > otherMaxLength) {
return false;
}
}
return true;
}
}

Expand Down
9 changes: 6 additions & 3 deletions packages/concerto-core/lib/serializer/instancegenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,13 @@ class InstanceGenerator {
case 'Boolean':
return parameters.valueGenerator.getBoolean();
default:
if(fieldOrScalarDeclaration.validator){
return parameters.valueGenerator.getRegex(fieldOrScalarDeclaration.validator.regex);
if(fieldOrScalarDeclaration.validator?.regex){
return parameters.valueGenerator.getRegex(fieldOrScalarDeclaration.validator.regex,
fieldOrScalarDeclaration.validator.minLength,
fieldOrScalarDeclaration.validator.maxLength);
}
return parameters.valueGenerator.getString();
return parameters.valueGenerator.getString(fieldOrScalarDeclaration.validator?.minLength,
fieldOrScalarDeclaration.validator?.maxLength);
}
}

Expand Down
Loading