@wmfs/j2119
Version:
A general-purpose validator generator that uses RFC2119-style assertions as input.
190 lines (158 loc) • 5.88 kB
JavaScript
const XRegExp = require('xregexp')
const oxford = require('./oxford')
const MUST = '(?<modal>MUST|MAY|MUST NOT)'
const TYPES = require('./types')
const RELATIONS = [
'', 'equal to', 'greater than', 'less than',
'greater than or equal to', 'less than or equal to'
].join('|')
const RELATION = `((?<relation>${RELATIONS})\\s+)`
const S = '"[^"]*"' // string
const V = '\\S+' // non-string value: number, true, false, null
const RELATIONAL = `${RELATION}(?<target>${S}|${V})`
const CHILD_ROLE = ';\\s+((its\\s+(?<child_type>value))|' +
'each\\s+(?<child_type_each>field|element))' +
'\\s+is\\s+an?\\s+' +
'"(?<child_role>[^"]+)"'
function definePredicate () {
const strings = oxford.re(S, { capture_name: 'strings' })
const enums = `one\\s+of\\s${strings}`
return `(${RELATIONAL}|${enums})`
}
const predicate = definePredicate()
class LineMatcher {
constructor (root) {
this.roles = []
this.addRole(root)
} // constructor
addRole (role) {
this.roles.push(role)
this.roleMatcher = this.roles.join('|')
this.reconstruct()
} // addRole
buildRoleDef (line) {
return this.build(this.roledefMatch, line)
} // buildRoleDef
buildConstraint (line) {
const constraint = this.build(this.constraintMatch, line)
// original Ruby code uses duplicate capture group name, which XRegExp disallows
// so patch result across if present
if (constraint.child_type_each) constraint.child_type = constraint.child_type_each
return constraint
} // buildConstraint
buildOnlyOne (line) {
return this.build(this.onlyOneMatch, line)
} // buildOnlyOne
buildEachOfs (line) {
const { groups: eachesLine } = XRegExp.exec(line, this.eachOfMatch)
const eaches = oxford.breakRoleList(this, eachesLine.each_of)
return eaches.map(e => `A ${e} ${eachesLine.trailer}`)
} // buildEachOfs
isRoleDefLine (line) {
return line.match(this.roledefLine)
} // isRoleDefLine
isConstraintLine (line) {
return line.match(this.constraintStart)
} // isConstraintLine
isOnlyOneMatchLine (line) {
return line.match(this.onlyOneStart)
} // isOnlyOneMatchLine
isEachOfLine (line) {
return line.match(this.eachOfStart)
} // isEachOfLine
/// ///////////////
build (re, line) {
const { groups: match } = XRegExp.exec(line, re)
const matchNames = [...Object.keys(match)].slice(match.length).filter(n => !['index', 'input'].includes(n))
const data = { }
matchNames.forEach(name => {
data[name] = (match[name] !== undefined) ? match[name] : null
})
return data
} // build
/// ///////////////
reconstruct () {
this.makeTypeRegex()
// conditional clause
const excludedRoles = 'not\\s+' +
oxford.re(this.roleMatcher, {
capture_name: 'excluded',
use_article: true
}) +
'\\s+'
const conditional = `which\\s+is\\s+${excludedRoles}`
// regex for matching constraint lines
const cStart = `^An?\\s+(?<role>${this.roleMatcher})\\s+(${conditional})?${MUST}\\s+have\\s+an?\\s+`
const fieldList = 'one\\s+of\\s+' +
oxford.re('"[^"]+"', {
capture_name: 'field_list'
})
const cMatch = cStart +
`((?<type>${this.typeRegex})\\s+)?` +
'field\\s+named\\s+' +
`(("(?<field_name>[^"]+)")|(${fieldList}))` +
`(\\s+whose\\s+value\\s+MUST\\s+be\\s+${predicate})?` +
`(${CHILD_ROLE})?` +
'\\.'
// regexp for matching lines of the form
// "An X MUST have only one of "Y", "Z", and "W".
// There's a pattern here, building a separate regex rather than
// adding more complexity to @constraint_matcher. Any further
// additions should be done this way, and
const ooStart = '^An?\\s+' +
`(?<role>${this.roleMatcher})\\s+` +
`${MUST}\\s+have\\s+only\\s+`
const ooFieldList = 'one\\s+of\\s+' +
oxford.re('"[^"]+"', {
capture_name: 'field_list',
connector: 'and'
})
const ooMatch = `${ooStart}${ooFieldList}`
// regex for matching role-def lines
const valMatch = 'whose\\s+"(?<fieldtomatch>[^"]+)"' +
'\\s+field\'s\\s+value\\s+is\\s+' +
'(?<valtomatch>("[^"]*")|([^"\\s]\\S+))\\s+'
const withAMatch = 'with\\s+an?\\s+"(?<with_a_field>[^"]+)"\\s+field\\s'
const rdMatch = '^An?\\s+' +
`(?<role>${this.roleMatcher})\\s+` +
`((?<val_match_present>${valMatch})|(${withAMatch}))?` +
'is\\s+an?\\s+' +
'"(?<newrole>[^"]*)"\\.\\s*$'
this.roledefLine = XRegExp('is\\s+an?\\s+"[^"]*"\\.\\s*$')
this.roledefMatch = XRegExp(rdMatch)
this.constraintStart = XRegExp(cStart)
this.constraintMatch = XRegExp(cMatch)
this.onlyOneStart = XRegExp(ooStart)
this.onlyOneMatch = XRegExp(ooMatch)
const eoStart = '^Each\\s+of\\s'
this.eoMatch = eoStart +
oxford.re(this.roleMatcher, {
capture_name: 'each_of',
use_article: true,
connector: 'and'
}) +
'\\s+(?<trailer>.*)$'
this.eachOfStart = XRegExp(eoStart)
this.eachOfMatch = XRegExp(this.eoMatch)
} // reconstruct
makeTypeRegex () {
// add modified numeric types
const types = [...TYPES]
const numberTypes = [TYPES.float, TYPES.integer, TYPES.numeric]
const numberModifiers = ['positive', 'negative', 'nonnegative']
numberTypes.forEach(numberType =>
numberModifiers.forEach(numberModifier =>
types.push(`${numberModifier}-${numberType}`)
)
)
// add array types
const arrayTypes = types.map(t => `${t}-array`)
const nonemptyArrayTypes = arrayTypes.map(t => `nonempty-${t}`)
types.push(...arrayTypes)
types.push(...nonemptyArrayTypes)
const nullableTypes = types.map(t => `nullable-${t}`)
types.push(...nullableTypes)
this.typeRegex = types.join('|')
} // makeTypeRegex
} // Matcher
module.exports = root => new LineMatcher(root)