Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: remove usages of lodash/fp in customPropTypes #4073

Merged
merged 4 commits into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/collections/Form/FormField.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ FormField.propTypes = {
* Extra FormField props are passed to the control component.
* Mutually exclusive with children.
*/
control: customPropTypes.some([
PropTypes.func,
control: PropTypes.oneOfType([
PropTypes.elementType,
PropTypes.oneOf(['button', 'input', 'select', 'textarea']),
]),

Expand Down
4 changes: 2 additions & 2 deletions src/elements/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ Button.propTypes = {
fluid: PropTypes.bool,

/** Add an Icon by name, props object, or pass an <Icon />. */
icon: customPropTypes.some([
icon: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string,
PropTypes.object,
Expand All @@ -258,7 +258,7 @@ Button.propTypes = {
inverted: PropTypes.bool,

/** Add a Label by text, props object, or pass a <Label />. */
label: customPropTypes.some([PropTypes.string, PropTypes.object, PropTypes.element]),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.element]),

/** A labeled button can format a Label or Icon to appear on the left or right. */
labelPosition: PropTypes.oneOf(['right', 'left']),
Expand Down
112 changes: 45 additions & 67 deletions src/lib/customPropTypes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash/fp'
import _ from 'lodash'
import PropTypes from 'prop-types'

import leven from './leven'

const typeOf = (...args) => Object.prototype.toString.call(...args)
Expand Down Expand Up @@ -33,27 +34,31 @@ export const suggest = (suggestions) => {
const findBestSuggestions = _.memoize((str) => {
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)
return _.take(
_.sortBy(
_.map(suggestions, (suggestion) => {
const suggestionWords = suggestion.split(' ')

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

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

return { suggestion, score: propValueScore + suggestionScore }
}),
['score', 'suggestion'],
),
3,
)
})
/* eslint-enable max-nested-callbacks */

Expand Down Expand Up @@ -110,7 +115,9 @@ export const disallow = (disallowedProps) => (props, propName, componentName) =>
}

// skip if prop is undefined
if (_.isNil(props[propName]) || props[propName] === false) return
if (_.isNil(props[propName]) || props[propName] === false) {
return
}

// find disallowed props with values
const disallowed = disallowedProps.reduce((acc, disallowedProp) => {
Expand Down Expand Up @@ -146,53 +153,24 @@ export const every = (validators) => (props, propName, componentName, ...rest) =
)
}

const errors = _.flow(
_.map((validator) => {
if (typeof validator !== 'function') {
throw new Error(
`every() argument "validators" should contain functions, found: ${typeOf(validator)}.`,
)
}
return validator(props, propName, componentName, ...rest)
}),
_.compact,
)(validators)
const errors = []

// we can only return one error at a time
return errors[0]
}
validators.forEach((validator) => {
if (typeof validator !== 'function') {
throw new Error(
`every() argument "validators" should contain functions, found: ${typeOf(validator)}.`,
)
}

/**
* Ensure a prop adherers to at least one of the given prop type validators.
* @param {function[]} validators An array of propType functions.
*/
export const some = (validators) => (props, propName, componentName, ...rest) => {
if (!Array.isArray(validators)) {
throw new Error(
[
'Invalid argument supplied to some, expected an instance of array.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '),
)
}
const error = validator(props, propName, componentName, ...rest)

const errors = _.compact(
_.map(validators, (validator) => {
if (!_.isFunction(validator)) {
throw new Error(
`some() argument "validators" should contain functions, found: ${typeOf(validator)}.`,
)
}
return validator(props, propName, componentName, ...rest)
}),
)

// fail only if all validators failed
if (errors.length === validators.length) {
const error = new Error('One of these validators must pass:')
error.message += `\n${_.map(errors, (err, i) => `[${i + 1}]: ${err.message}`).join('\n')}`
return error
}
if (error) {
errors.push(error)
}
})

// we can only return one error at a time
return errors[0]
}

/**
Expand Down
133 changes: 113 additions & 20 deletions test/specs/lib/customPropTypes-test.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,135 @@
import PropTypes from 'prop-types'
import { customPropTypes } from 'src/lib'

describe('suggest prop type', () => {
it('should throw error when non-array argument given', () => {
expect(() => customPropTypes.suggest('foo')).to.throw(
Error,
/Invalid argument supplied to suggest, expected an instance of array./,
)
import { consoleUtil, sandbox } from 'test/utils'

/* eslint-disable no-console */

describe('customPropTypes', () => {
beforeEach(() => {
consoleUtil.disable()
sandbox.spy(console, 'error')
})

it('should return undefined when prop is valid', () => {
const propType = customPropTypes.suggest(['foo', 'bar', 'baz'])
expect(propType({ name: 'bar' }, 'name', 'FooComponent')).to.equal(undefined)
describe('every', () => {
it('should throw error when a non-array argument given', () => {
PropTypes.checkPropTypes(
{
name: customPropTypes.every('foo'),
},
{ name: 'foo' },
'name',
'FooComponent',
)

console.error.should.have.been.calledWithMatch(
/Invalid argument supplied to every, expected an instance of array/,
)
})

it('should throw error when a non-function argument given', () => {
PropTypes.checkPropTypes(
{
name: customPropTypes.every([{}]),
},
{ name: 'foo' },
'name',
'FooComponent',
)

console.error.should.have.been.calledWithMatch(
/argument "validators" should contain functions/,
)
})

it('should execute all validators', () => {
PropTypes.checkPropTypes(
{
name: customPropTypes.every([PropTypes.string]),
},
{ name: 1 },
'name',
'FooComponent',
)

console.error.should.have.been.calledWithMatch(
/Invalid name `name` of type `number` supplied/,
)
})

it('should execute all validators including custom', () => {
PropTypes.checkPropTypes(
{
name: customPropTypes.every([customPropTypes.suggest(['foo']), PropTypes.number]),
},
{ name: 'bar' },
'name',
'FooComponent',
)

console.error.should.have.been.calledWithMatch(/Instead of `bar`, did you mean:/)
})
})

it('should return Error with suggestions when prop is invalid', () => {
const propType = customPropTypes.suggest(['foo', 'bar', 'baz'])
const props = { name: 'bad', title: 'bat words' }
describe('suggest', () => {
it('should throw error when non-array argument given', () => {
expect(() => customPropTypes.suggest('foo')).to.throw(
Error,
/Invalid argument supplied to suggest, expected an instance of array./,
)
})

it('should return undefined when prop is valid', () => {
PropTypes.checkPropTypes(
{
name: customPropTypes.suggest(['foo', 'bar', 'baz']),
},
{ name: 'foo' },
'name',
'FooComponent',
)

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\`.
console.error.should.have.not.been.called()
})

it('should return Error with suggestions when prop is invalid', () => {
PropTypes.checkPropTypes(
{
name: customPropTypes.suggest(['foo', 'bar', 'baz']),
},
{ name: 'bad' },
'name',
'FooComponent',
)

console.error.should.have.been
.calledWithMatch(`Invalid prop \`name\` of value \`bad\` supplied to \`FooComponent\`.

Instead of \`bad\`, did you mean:
- bar
- baz
- foo
`)
})

it('should return Error with suggestions when prop contains multiple words and is invalid', () => {
PropTypes.checkPropTypes(
{
name: customPropTypes.suggest(['foo', 'bar', 'baz']),
},
{ name: 'bat words' },
'name',
'FooComponent',
)

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\`.
console.error.should.have.been
.calledWithMatch(`Invalid prop \`name\` of value \`bat words\` supplied to \`FooComponent\`.

Instead of \`bat words\`, did you mean:
- bar
- baz
- foo
`)
})
})
})