Skip to content

Commit

Permalink
feat(conditions): Allow matching on semantic attributes instead of regex
Browse files Browse the repository at this point in the history
Includes a fix for direct branches matching "start" of string after bot prefix.
  • Loading branch information
timkinnane committed Aug 28, 2018
1 parent 1f74688 commit 0b6e785
Show file tree
Hide file tree
Showing 9 changed files with 679 additions and 24 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -91,6 +91,7 @@
"inquirer": "^6.0.0",
"mongoose": "^5.2.0",
"request": "^2.88.0",
"to-regex-range": "^4.0.2",
"winston": "^3.0.0",
"yargs": "^12.0.0"
},
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -14,6 +14,7 @@ export * from './lib/envelope'
export * from './lib/payload'
export * from './lib/path'
export * from './lib/branch'
export * from './lib/condition'
export * from './lib/bit'
export * from './lib/memory'
export * from './lib/store'
Expand Down
72 changes: 67 additions & 5 deletions src/lib/branch.spec.ts
Expand Up @@ -184,12 +184,74 @@ describe('[branch]', () => {
})
})
describe('TextBranch', () => {
it('.process adds matcher result to state', () => {
const fooBranch = new bot.TextBranch(/foo/, (state) => {
expect(state.match).to.eql('foo'.match(/foo/))
it('.process adds matcher result to state', async () => {
const branch = new bot.TextBranch(/foo/, () => null)
const text = 'foo'
const b = new bot.State({ message: new bot.TextMessage(user, text) })
await branch.process(b, middleware)
expect(b.match).to.eql('foo'.match(/foo/))
})
it('.process adds condition match results to state', async () => {
const conditions = [{ starts: 'foo' }, { ends: 'bar' }]
const text = 'foo bar'
const b = new bot.State({ message: new bot.TextMessage(user, text) })
const branch = new bot.TextBranch(conditions, () => null)
await branch.process(b, middleware)
expect(b.conditions.success).to.equal(true)
})
it('.process adds condition captures to branch in state', async () => {
const conditions = { door: { after: 'door number', range: '1-3' } }
const text = 'door number 3'
const b = new bot.State({ message: new bot.TextMessage(user, text) })
const branch = new bot.TextBranch(conditions, () => null)
await branch.process(b, middleware)
expect(b.conditions.captured).to.eql({ door: '3' })
})
it('.process branch with pre-constructed conditions', async () => {
const conditions = new bot.Conditions({
they: { contains: [`they're`, `their`, 'they'] }
}, {
ignorePunctuation: true
})
return fooBranch.process(new bot.State({
message: new bot.TextMessage(user, 'foo')
const text = `they're about ready aren't they`
const b = new bot.State({ message: new bot.TextMessage(user, text) })
const branch = new bot.TextBranch(conditions, () => null)
await branch.process(b, middleware)
expect(b.conditions.captured).to.eql({ they: `they're` })
})
it('.process unmatched if condition match falsy', async () => {
const conditions = {
question: { ends: '?' },
not: { starts: 'not' }
}
const text = `not a question!`
const b = new bot.State({ message: new bot.TextMessage(user, text) })
const branch = new bot.TextBranch(conditions, () => null)
await branch.process(b, middleware)
expect(typeof b.conditions).to.equal('undefined')
assert.notOk(b.match)
})
})
describe('TextDirectBranch', () => {
it('.process returns match if bot name prefixed', () => {
const direct = new bot.TextDirectBranch(/foo/, (b) => {
expect(b.match).to.eql('foo'.match(/foo/))
})
return direct.process(new bot.State({
message: new bot.TextMessage(user, `${bot.settings.get('name')} foo`)
}), middleware)
})
it('.process adds condition match results to state', () => {
const conditions = [{ starts: 'foo' }, { ends: 'bar' }]
const branch = new bot.TextDirectBranch(conditions, (b) => {
expect(b.match).to.equal(true)
expect(b.conditions.matches).to.eql({
0: /^foo/.exec('foo bar'),
1: /bar$/.exec('foo bar')
})
})
return branch.process(new bot.State({
message: new bot.TextMessage(user, `${bot.settings.get('name')} foo bar`)
}), middleware)
})
})
Expand Down
44 changes: 35 additions & 9 deletions src/lib/branch.ts
Expand Up @@ -135,32 +135,45 @@ export class CustomBranch extends Branch {

/** Text branch uses basic regex matching */
export class TextBranch extends Branch {
conditions: bot.Conditions

/** Create text branch for regex pattern */
constructor (
public regex: RegExp,
conditions: string | RegExp | bot.Condition | bot.Condition[] | bot.ConditionCollection | bot.Conditions,
callback: IBranchCallback | string,
options?: IBranch
) {
super(callback, options)
this.conditions = (conditions instanceof bot.Conditions)
? conditions
: new bot.Conditions(conditions)
}

/** Use async because matchers must return a promise */
/**
* Match message text against regex or composite conditions.
* Resolves with either single match result or cumulative condition success.
*/
async matcher (message: bot.Message) {
const match = message.toString().match(this.regex)
this.conditions.exec(message.toString())
const match = this.conditions.match
if (match) {
bot.logger.debug(`[branch] message "${message}" matched regex /${this.regex}/ ID ${this.id}`)
bot.logger.debug(`[branch] message "${message}" matched branch ${this.id} conditions`)
}
return match
}
}

/** Text Direct Branch pre-matches the text for bot name prefix */
/**
* Text Direct Branch pre-matches the text for bot name prefix.
* Once matched, it removes the direct pattern from the message text.
*/
export class TextDirectBranch extends TextBranch {
async matcher (message: bot.TextMessage) {
if (directPattern(/.*/).exec(message.toString())) {
if (directPattern().exec(message.toString())) {
message.text = message.text.replace(directPattern(), '')
return super.matcher(message)
} else {
return null
return false
}
}
}
Expand Down Expand Up @@ -199,7 +212,7 @@ export class NaturalLanguageBranch extends Branch {
/** Natural Language Direct Branch pre-matches the text for bot name prefix */
export class NaturalLanguageDirectBranch extends NaturalLanguageBranch {
async matcher (message: bot.TextMessage) {
if (directPattern(/.*/).exec(message.toString())) {
if (directPattern().exec(message.toString())) {
return super.matcher(message)
} else {
return undefined
Expand All @@ -212,7 +225,20 @@ export class NaturalLanguageDirectBranch extends NaturalLanguageBranch {
* - matches when alias is substring of name
* - matches when name is substring of alias
*/
export function directPattern (regex: RegExp) {
export function directPattern () {
const botName = bot.settings.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
if (!bot.settings.alias) {
return new RegExp(`^\\s*[@]?${botName}[:,]?\\s*`, 'i')
}
const botAlias = bot.settings.alias.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
if (botName.length > botAlias.length) {
return new RegExp(`^\\s*[@]?(?:${botName}[:,]?|${botAlias}[:,]?)\\s*`, 'i')
}
return new RegExp(`^\\s*[@]?(?:${botAlias}[:,]?|${botName}[:,]?)\\s*`, 'i')
}

/** Build a regular expression for bot's name combined with another regex */
export function directPatternCombined (regex: RegExp) {
const regexWithoutModifiers = regex.toString().split('/')
regexWithoutModifiers.shift()
const modifiers = regexWithoutModifiers.pop()
Expand Down

0 comments on commit 0b6e785

Please sign in to comment.