@wmfs/j2119
Version:
A general-purpose validator generator that uses RFC2119-style assertions as input.
281 lines (254 loc) • 10.6 kB
JavaScript
/* eslint-env mocha */
const chai = require('chai')
chai.use(require('dirty-chai'))
const expect = chai.expect
const oxford = require('../lib/j2119/oxford')
const lineMatcher = require('../lib/j2119/line_matcher')
const XRegExp = require('xregexp')
describe('J2119 LineMatcher', () => {
const EACHOF_LINES = [
'Each of a Pass State, a Task State, a Choice State, and a Parallel State MAY have a boolean field named "End".',
'Each of a Succeed State and a Fail State is a "Terminal State".',
'Each of a Task State and a Parallel State MAY have an object-array field named "Catch"; each member is a "Catcher".'
]
const ROLES = ['Pass State', 'Task State', 'Choice State', 'Parallel State',
'Succeed State', 'Fail State', 'Task Tate']
describe('should spot Each-of lines', () => {
const cut = lineMatcher('message')
ROLES.forEach(role => cut.addRole(role))
EACHOF_LINES.forEach(line =>
it(line, () => {
expect(cut.eachOfMatch.test(line)).to.be.true()
})
)
})
it('should handle only-one-of lines', () => {
const line = 'A x MUST have only one of "Seconds", "SecondsPath", "Timestamp", and "TimestampPath".'
const cut = lineMatcher('x')
expect(cut.isOnlyOneMatchLine(line)).to.be.ok()
const { groups: m } = XRegExp.exec(line, cut.onlyOneMatch)
expect(m).to.be.ok()
expect(m.role).to.eql('x')
const s = m.field_list
const l = oxford.breakStringList(s)
expect(l).to.contain('Seconds', 'SecondsPath', 'Timestamp', 'TimestampPath')
expect(l.length).to.eql(4)
})
const SPLIT_EACHOF_LINES = [
[
'A Pass State MAY have a boolean field named "End".',
'A Task State MAY have a boolean field named "End".',
'A Choice State MAY have a boolean field named "End".',
'A Parallel State MAY have a boolean field named "End".'
],
[
'A Succeed State is a "Terminal State".',
'A Fail State is a "Terminal State".'
],
[
'A Task State MAY have an object-array field named "Catch"; each member is a "Catcher".',
'A Parallel State MAY have an object-array field named "Catch"; each member is a "Catcher".'
]
]
// each-of handling isn't complete
xdescribe('should properly disassemble each-of lines', () => {
const cut = lineMatcher('message')
ROLES.forEach(role => cut.addRole(role))
EACHOF_LINES.forEach(line => {
const wanted = SPLIT_EACHOF_LINES.shift()
oxford.breakRoleList(cut, line).forEach(oneLine => {
it(oneLine, () => {
expect(wanted.includes(oneLine)).to.be.ok()
})
})
})
})
const RDLINES = [
'A State whose "End" field\'s value is true is a "Terminal State".',
'Each of a Succeed State and a Fail state is a "Terminal State".',
'A Choice Rule with a "Variable" field is a "Comparison".'
]
describe('should spot role-def lines', () => {
const cut = lineMatcher('message')
RDLINES.forEach(line =>
it(line, () => {
expect(cut.isRoleDefLine(line)).to.be.ok()
})
)
})
const VALUE_BASED_ROLE_DEFS = [
'A State whose "End" field\'s value is true is a "Terminal State".',
'A State whose "Comment" field\'s value is "Hi" is a "Frobble".',
'A State with a "Foo" field is a "Bar".'
]
describe('should match value-based role defs', () => {
const cut = lineMatcher('State')
VALUE_BASED_ROLE_DEFS.forEach(line => {
it(line, () =>
expect(cut.roledefMatch.test(line)).to.be.ok()
)
})
it(VALUE_BASED_ROLE_DEFS[0], () => {
const { groups: m } = XRegExp.exec(VALUE_BASED_ROLE_DEFS[0], cut.roledefMatch)
expect(m.role).to.eql('State')
expect(m.fieldtomatch).to.eql('End')
expect(m.valtomatch).to.eql('true')
expect(m.newrole).to.eql('Terminal State')
expect(m.val_match_present).to.be.ok()
})
it(VALUE_BASED_ROLE_DEFS[1], () => {
const { groups: m } = XRegExp.exec(VALUE_BASED_ROLE_DEFS[1], cut.roledefMatch)
expect(m.role).to.eql('State')
expect(m.fieldtomatch).to.eql('Comment')
expect(m.valtomatch).to.eql('"Hi"')
expect(m.newrole).to.eql('Frobble')
expect(m.val_match_present).to.be.ok()
})
it(VALUE_BASED_ROLE_DEFS[2], () => {
const { groups: m } = XRegExp.exec(VALUE_BASED_ROLE_DEFS[2], cut.roledefMatch)
expect(m.role).to.eql('State')
expect(m.newrole).to.eql('Bar')
expect(m.with_a_field).to.be.ok()
})
})
it('should match is_a role defs', () => {
const cut = lineMatcher('Foo')
expect(cut.roledefMatch.test('A Foo is a "Bar".')).to.be.true()
})
it('should properly parse is_a role defs', () => {
const cut = lineMatcher('Foo')
cut.addRole('Bar')
const c = cut.buildRoleDef('A Foo is a "Bar".')
expect(c.val_match_present).to.be.null()
})
describe('should properly parse value-based role defs', () => {
const cut = lineMatcher('State')
it(VALUE_BASED_ROLE_DEFS[0], () => {
const c = cut.buildRoleDef(VALUE_BASED_ROLE_DEFS[0])
expect(c.role).to.eql('State')
expect(c.fieldtomatch).to.eql('End')
expect(c.valtomatch).to.eql('true')
expect(c.newrole).to.eql('Terminal State')
})
it(VALUE_BASED_ROLE_DEFS[1], () => {
const c = cut.buildRoleDef(VALUE_BASED_ROLE_DEFS[1])
expect(c.role).to.eql('State')
expect(c.fieldtomatch).to.eql('Comment')
expect(c.valtomatch).to.eql('"Hi"')
expect(c.newrole).to.eql('Frobble')
})
})
const LINES = [
'A message MUST have an object field named "States"; each field is a "State".',
'A message MUST have a negative-integer-array field named "StartAt".',
'A message MAY have a string-array field named "StartAt".',
'A message MUST NOT have a field named "StartAt".',
'A message MUST have a field named one of "StringEquals", "StringLessThan", "StringGreaterThan", "StringLessThanEquals", "StringGreaterThanEquals", "NumericEquals", "NumericLessThan", "NumericGreaterThan", "NumericLessThanEquals", "NumericGreaterThanEquals", "BooleanEquals", "TimestampEquals", "TimestampLessThan", "TimestampGreaterThan", "TimestampLessThanEquals", or "TimestampGreaterThanEquals".'
]
describe('should spot a simple constraint line', () => {
const cut = lineMatcher('message')
LINES.forEach(line =>
it(line, () => {
expect(cut.isConstraintLine(line)).to.be.ok()
})
)
})
describe('should spot a simple constraint line with new roles', () => {
const cut = lineMatcher('message')
cut.addRole('avatar')
const lines2 = LINES.map(line => line.replace('message', 'avatar'))
lines2.forEach(line =>
it(line, () =>
expect(cut.isConstraintLine(line)).to.be.ok()
)
)
})
const COND_LINES = [
'An R1 MUST have an object field named "States"; each field is a "State".',
'An R1 which is not an R2 MUST have an object field named "States"; each field is a "State".',
'An R1 which is not an R2 or an R3 MUST NOT have a field named "StartAt".',
'An R1 which is not an R2, an R3, or an R4 MUST NOT have a field named "StartAt".'
]
describe('should catch a conditional on a constraint', () => {
const excludes = [
undefined,
'an R2',
'an R2 or an R3',
'an R2, an R3, or an R4'
]
const cut = lineMatcher('R1')
cut.addRole('R2')
cut.addRole('R3')
cut.addRole('R4')
COND_LINES.forEach(line =>
it(line, () => {
expect(cut.constraintMatch.test(line)).to.be.ok()
const { groups: m } = XRegExp.exec(line, cut.constraintMatch)
expect(m.excluded).to.eql(excludes.shift())
})
)
})
describe('should match a reasonably complex constraint', () => {
const s = 'A State MUST have a string field named "Type" whose value MUST be one of "Pass", "Succeed", "Fail", "Task", "Choice", "Wait", or "Parallel".'
it(s, () => {
const cut = lineMatcher('State')
expect(cut.constraintMatch.test(s)).to.be.true()
})
const r = 'A Retrier MAY have a nonnegative-integer field named "MaxAttempts" whose value MUST be less than 99999999.'
it(r, () => {
const cut = lineMatcher('State')
cut.addRole('Retrier')
expect(cut.constraintMatch.test(r)).to.be.true()
})
})
it('should build an enum constraint object', () => {
const cut = lineMatcher('State')
const s = 'A State MUST have a string field named "Type" whose value MUST be one of "Pass", "Succeed", "Fail", "Task", "Choice", "Wait", or "Parallel".'
const c = cut.buildConstraint(s)
expect(c.role).to.eql('State')
expect(c.modal).to.eql('MUST')
expect(c.type).to.eql('string')
expect(c.field_name).to.eql('Type')
expect(c.relation).to.be.null()
expect(c.strings).to.eql('"Pass", "Succeed", "Fail", "Task", "Choice", "Wait", or "Parallel"')
expect(c.child_type).to.be.null()
})
it('should build a relational constraint object', () => {
const cut = lineMatcher('Retrier')
const s = 'A Retrier MAY have a nonnegative-integer field named "MaxAttempts" whose value MUST be less than 99999999.'
const c = cut.buildConstraint(s)
expect(c.role).to.eql('Retrier')
expect(c.modal).to.eql('MAY')
expect(c.type).to.eql('nonnegative-integer')
expect(c.field_name).to.eql('MaxAttempts')
expect(c.strings).to.be.null()
expect(c.relation).to.eql('less than')
expect(c.target).to.eql('99999999')
expect(c.child_type).to.be.null()
})
describe('should build a constraint object with child type', () => {
const s = 'A State Machine MUST have an object field named "States"; each field is a "State".'
it(s, () => {
const cut = lineMatcher('State Machine')
expect(cut.constraintMatch.test(s)).to.be.true()
const c = cut.buildConstraint(s)
expect(c.role).to.eql('State Machine')
expect(c.modal).to.eql('MUST')
expect(c.type).to.eql('object')
expect(c.field_name).to.eql('States')
expect(c.child_type).to.eql('field')
expect(c.child_role).to.eql('State')
})
const line = 'A State Machine MAY have an object field named "Not"; its value is a "FOO".'
it(line, () => {
const cut = lineMatcher('State Machine')
expect(cut.constraintMatch.test(line)).to.be.true()
const c = cut.buildConstraint(line)
expect(c.role).to.eql('State Machine')
expect(c.modal).to.eql('MAY')
expect(c.type).to.eql('object')
expect(c.field_name).to.eql('Not')
expect(c.child_role).to.eql('FOO')
})
})
})