Skip to content
Open
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
52 changes: 51 additions & 1 deletion lib/codegen/fromcto/jsonschema/jsonschemavisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,56 @@ const { ModelUtil } = require('@accordproject/concerto-core');
const { throwUnrecognizedType } = require('../../../common/util');
const RecursionDetectionVisitor = require('./recursionvisitor');

// Characters that may be escaped with a backslash in a unicode-mode regex
// (`-` may only be escaped inside a character class)
const UNICODE_ESCAPABLE = '^$\\.*+?()[]{}|/';

/**
* Returns a regex source that is also valid in unicode mode, which JSON Schema
* validators such as ajv use to compile `pattern`. Concerto regexes have no `u`
* flag, so they may contain identity escapes such as `\<` that are rejected in
* unicode mode. Those escapes match the literal character either way, so the
* backslash is dropped. Sources that are already valid are returned unchanged.
* @param {string} source - the regex source
* @return {string} the unicode-safe regex source
* @private
*/
function toUnicodeSafePattern(source) {
try {
new RegExp(source, 'u');
return source;
} catch (err) {
debug('Regex is not valid in unicode mode, removing identity escapes', source);
}
let result = '';
let inClass = false;
for (let i = 0; i < source.length; i++) {
const char = source[i];
const next = source[i + 1];
if (char === '\\' && next !== undefined) {
if (/[A-Za-z0-9]/.test(next) || UNICODE_ESCAPABLE.includes(next) || (inClass && next === '-')) {
result += char + next;
} else {
result += next;
}
i++;
} else {
if (char === '[') {
inClass = true;
} else if (char === ']') {
inClass = false;
}
result += char;
}
}
try {
new RegExp(result, 'u');
return result;
} catch (err) {
return source;
}
}

/**
* Convert the contents of a {@link ModelManager} to a JSON Schema, returning
* the schema for all types under the 'definitions' key. If the 'rootType'
Expand Down Expand Up @@ -89,7 +139,7 @@ class JSONSchemaVisitor {
} else if(validator) { // validator for uuid is not required.
// Note that regex flags are lost in this transformation
if (validator.getRegex()) {
jsonSchema.pattern = validator.getRegex().source;
jsonSchema.pattern = toUnicodeSafePattern(validator.getRegex().source);
}
if(validator.getMinLength()) {
jsonSchema.minLength = validator.getMinLength();
Expand Down
18 changes: 18 additions & 0 deletions test/codegen/fromcto/jsonschema/jsonschemavisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,24 @@ describe('JSONSchema (samples)', function () {
expect(schema.properties.myString.pattern).equal('abc.*');
});

it('should generate unicode-safe patterns from regexes with identity escapes', () => {
const modelManager = new ModelManager();
modelManager.addCTOModel(`namespace test@1.0.0
concept Test {
o String noAngleBrackets regex=/^[^\\<\\>]*$/
o String escapedDash regex=/^a\\-b$/
o String alreadySafe regex=/^[\\w\\-]+\\.\\d+$/
}`);
const visitor = new JSONSchemaVisitor();
const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Test'});
expect(schema.properties.noAngleBrackets.pattern).equal('^[^<>]*$');
expect(schema.properties.escapedDash.pattern).equal('^a-b$');
expect(schema.properties.alreadySafe.pattern).equal('^[\\w\\-]+\\.\\d+$');
Object.values(schema.properties)
.filter(property => property.pattern)
.forEach(property => expect(() => new RegExp(property.pattern, 'u')).to.not.throw());
});

it('should inline types for simple model', () => {
const modelManager = new ModelManager();
modelManager.addCTOModel( MODEL_SIMPLE );
Expand Down
5 changes: 5 additions & 0 deletions test/verification/jsonschema.validate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ async function verifyJsonSchemaCompiles(modelManager, visitorOptions = {}) {
const ajv = new Ajv({ strict: false });
try {
ajv.compile(schema);
// ajv only compiles definitions that are referenced, so compile
// each definition through a $ref to check all of them
Object.keys(schema.definitions || {}).forEach(name => {
ajv.compile({ ...schema, $ref: `#/definitions/${name}` });
});
} catch (err) {
throw new Error(err.message);
}
Expand Down