Skip to content

Commit

Permalink
fix(customPropTypes): improve perf of suggest
Browse files Browse the repository at this point in the history
  • Loading branch information
David Zukowski committed Nov 19, 2017
1 parent a2dfbad commit 27c3dcc
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 44 deletions.
100 changes: 58 additions & 42 deletions src/lib/customPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,31 @@ export const as = (...args) => PropTypes.oneOfType([
])(...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))
const findBestSuggestions = _.memoize((str, suggestions) => {
const propValueWords = str.split(' ')

return _.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 */

/**
Expand All @@ -42,33 +46,45 @@ const findBestSuggestions = _.memoize((propValueWords, suggestions) => _.flow(
* Useful for very large lists of options (e.g. Icon name, Flag name, etc.)
* @param {string[]} suggestions An array of allowed values.
*/
export const suggest = suggestions => (props, propName, componentName) => {
export const suggest = (suggestions) => {
if (!Array.isArray(suggestions)) {
throw new Error([
'Invalid argument supplied to suggest, expected an instance of array.',
` See \`${propName}\` prop in \`${componentName}\`.`,
].join(''))
throw new Error('Invalid argument supplied to suggest, expected an instance of array.')
}
const propValue = props[propName]

// skip if prop is undefined or is included in the suggestions
if (_.isNil(propValue) || propValue === false || _.includes(propValue, suggestions)) return
// Convert the suggestions list into a hash map for O(n) lookup times. Ensure
// the words in each key are sorted alphabetically so that we have a consistent
// way of looking up a value in the map, i.e. we can sort the words in the
// incoming propValue and look that up without having to check all permutations.
const suggestionsLookup = suggestions.reduce((acc, key) => {
acc[key.split(' ').sort().join(' ')] = true
return acc
}, {})

return (props, propName, componentName) => {
const propValue = props[propName]

// skip if prop is undefined or is included in the suggestions
if (!propValue || suggestionsLookup[propValue]) return

// check if the words were correct but ordered differently.
// Since we're matching for classNames we need to allow any word order
// to pass validation, e.g. `left chevron` vs `chevron left`.
const propValueSorted = propValue.split(' ').sort().join(' ')
if (suggestionsLookup[propValueSorted]) return

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

// skip if a match scored 0
// since we're matching on words (classNames) this allows any word order to pass validation
// e.g. `left chevron` vs `chevron left`
if (bestMatches.some(x => x.score === 0)) return
// skip if a match scored 0
if (bestMatches.some(x => x.score === 0)) return

return new Error([
`Invalid prop \`${propName}\` of value \`${propValue}\` supplied to \`${componentName}\`.`,
`\n\nInstead of \`${propValue}\`, did you mean:`,
bestMatches.map(x => `\n - ${x.suggestion}`).join(''),
'\n',
].join(''))
return new Error([
`Invalid prop \`${propName}\` of value \`${propValue}\` supplied to \`${componentName}\`.`,
`\n\nInstead of \`${propValue}\`, did you mean:`,
bestMatches.map(x => `\n - ${x.suggestion}`).join(''),
'\n',
].join(''))
}
}

/**
Expand Down
3 changes: 1 addition & 2 deletions test/specs/lib/customPropTypes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ 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(
expect(() => customPropTypes.suggest('foo')).to.throw(
Error,
/Invalid argument supplied to suggest, expected an instance of array./,
)
Expand Down

0 comments on commit 27c3dcc

Please sign in to comment.