Skip to content

Commit

Permalink
perf(customPropTypes): Memoize suggest (Semantic-Org#1184)
Browse files Browse the repository at this point in the history
cache finding suggestions for prop words
  • Loading branch information
ayastreb committed Oct 2, 2017
1 parent 9c97ed9 commit 53c5749
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 24 deletions.
49 changes: 25 additions & 24 deletions src/lib/customPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@ export const as = (...args) => PropTypes.oneOfType([
PropTypes.func,
])(...args)

/* eslint-disable max-nested-callbacks */
const findBestSuggestions = _.memoize((propValueWords, suggestions) => _.flow(
_.map((suggestion) => {
const suggestionWords = suggestion.split(' ')

const propValueScore = _.flow(
_.map(x => _.map(y => leven(x, y), suggestionWords)),
_.map(_.min),
_.sum,
)(propValueWords)

const suggestionScore = _.flow(
_.map(x => _.map(y => leven(x, y), propValueWords)),
_.map(_.min),
_.sum,
)(suggestionWords)

return { suggestion, score: propValueScore + suggestionScore }
}),
_.sortBy(['score', 'suggestion']),
_.take(3),
)(suggestions))
/* eslint-enable max-nested-callbacks */

/**
* Similar to PropTypes.oneOf but shows closest matches.
* Word order is ignored allowing `left chevron` to match `chevron left`.
Expand All @@ -32,30 +56,7 @@ export const suggest = suggestions => (props, propName, componentName) => {

// find best suggestions
const propValueWords = propValue.split(' ')

/* eslint-disable max-nested-callbacks */
const bestMatches = _.flow(
_.map((suggestion) => {
const suggestionWords = suggestion.split(' ')

const propValueScore = _.flow(
_.map(x => _.map(y => leven(x, y), suggestionWords)),
_.map(_.min),
_.sum,
)(propValueWords)

const suggestionScore = _.flow(
_.map(x => _.map(y => leven(x, y), propValueWords)),
_.map(_.min),
_.sum,
)(suggestionWords)

return { suggestion, score: propValueScore + suggestionScore }
}),
_.sortBy(['score', 'suggestion']),
_.take(3),
)(suggestions)
/* eslint-enable max-nested-callbacks */
const bestMatches = findBestSuggestions(propValueWords, suggestions)

// skip if a match scored 0
// since we're matching on words (classNames) this allows any word order to pass validation
Expand Down
43 changes: 43 additions & 0 deletions test/specs/lib/customPropTypes-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { customPropTypes } from 'src/lib'

describe('suggest prop type', () => {
it('should throw error when non-array argument given', () => {
const propType = customPropTypes.suggest('foo')
expect(() => propType({ name: 'bar' }, 'name', 'FooComponent')).to.throw(
Error,
/Invalid argument supplied to suggest, expected an instance of array./,
)
})

it('should return undefined when prop is valid', () => {
const propType = customPropTypes.suggest(['foo', 'bar', 'baz'])
expect(propType({ name: 'bar' }, 'name', 'FooComponent')).to.equal(undefined)
})

it('should return Error with suggestions when prop is invalid', () => {
const propType = customPropTypes.suggest(['foo', 'bar', 'baz'])
const props = { name: 'bad', title: 'bat words' }

const resultFooComponent = propType(props, 'name', 'FooComponent')
expect(resultFooComponent).to.be.an.instanceof(Error)
expect(resultFooComponent.message).to
.equal(`Invalid prop \`name\` of value \`bad\` supplied to \`FooComponent\`.
Instead of \`bad\`, did you mean:
- bar
- baz
- foo
`)

const resultBarComponent = propType(props, 'title', 'BarComponent')
expect(resultBarComponent).to.be.an.instanceof(Error)
expect(resultBarComponent.message).to
.equal(`Invalid prop \`title\` of value \`bat words\` supplied to \`BarComponent\`.
Instead of \`bat words\`, did you mean:
- bar
- baz
- foo
`)
})
})

0 comments on commit 53c5749

Please sign in to comment.