diff --git a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js index 7c11e79..da4bfe7 100644 --- a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js +++ b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js @@ -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' @@ -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(); diff --git a/test/codegen/fromcto/jsonschema/jsonschemavisitor.js b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js index 5ff763f..0523559 100644 --- a/test/codegen/fromcto/jsonschema/jsonschemavisitor.js +++ b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js @@ -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 ); diff --git a/test/verification/jsonschema.validate.test.js b/test/verification/jsonschema.validate.test.js index 032a693..d54bb4c 100644 --- a/test/verification/jsonschema.validate.test.js +++ b/test/verification/jsonschema.validate.test.js @@ -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); }