Skip to content

Commit

Permalink
Move propPath predicate functions to path (evilsoft#407)
Browse files Browse the repository at this point in the history
* move propPathEq to pathEq

* move propPathSatisfies to pathSatisfies

* update docs to remove pathProp pred functions

* fix spelling errors from review
  • Loading branch information
evilsoft authored Jun 16, 2019
1 parent ead0292 commit 2cfb42f
Show file tree
Hide file tree
Showing 13 changed files with 375 additions and 102 deletions.
4 changes: 2 additions & 2 deletions docs/src/pages/docs/functions/logic-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ import constant from 'crocks/combinators/constant'
import isEmpty from 'crocks/predicates/isEmpty'
import not from 'crocks/logic/not'
import propSatisfies from 'crocks/predicates/propSatisfies'
import propPathSatisfies from 'crocks/predicates/propPathSatisfies'
import pathSatisfies from 'crocks/predicates/pathSatisfies'

or(constant(true), constant(true), 'ignored')
//=> true
Expand All @@ -411,7 +411,7 @@ const createResponse = (users, error = '') => ({
// hasData :: Response -> Boolean
const hasData = or(
propSatisfies('error', isEmpty),
propPathSatisfies([ 'response', 'users' ], not(isEmpty))
pathSatisfies([ 'response', 'users' ], not(isEmpty))
)

hasData(createResponse([ { name: 'User 1' } ]))
Expand Down
8 changes: 4 additions & 4 deletions docs/src/pages/docs/functions/predicate-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
description: "Predicate Functions API"
layout: "notopic"
title: "Predicate Functions"
functions: ["hasprop", "hasproppath", "isalt", "isalternative", "isapplicative", "isapply", "isarray", "isbifunctor", "isboolean", "iscategory", "ischain", "iscontravariant", "isDate", "isdefined", "isempty", "isextend", "isfalse", "isfalsy", "isfoldable", "isfunction", "isfunctor", "isinteger", "isiterable", "ismonad", "ismonoid", "isnil", "isnumber", "isobject", "isplus", "isprofunctor", "ispromise", "issame", "issametype", "issemigroup", "issemigroupoid", "issetoid", "isstring", "istraversable", "istrue", "istruthy", "propeq", "proppatheq", "proppathsatisfies", "propsatisfies"]
functions: ["hasprop", "hasproppath", "isalt", "isalternative", "isapplicative", "isapply", "isarray", "isbifunctor", "isboolean", "iscategory", "ischain", "iscontravariant", "isDate", "isdefined", "isempty", "isextend", "isfalse", "isfalsy", "isfoldable", "isfunction", "isfunctor", "isinteger", "isiterable", "ismonad", "ismonoid", "isnil", "isnumber", "isobject", "isplus", "isprofunctor", "ispromise", "issame", "issametype", "issemigroup", "issemigroupoid", "issetoid", "isstring", "istraversable", "istrue", "istruthy", "patheq", "pathsatisfies", "propeq", "propsatisfies"]
weight: 40
---

Expand Down Expand Up @@ -56,13 +56,13 @@ description of their truth:
* `isTraversable :: a -> Boolean`: an ADT that provides `map` and `traverse` methods
* `isTrue :: a -> Boolean`: a value that is strictly equal to `true`
* `isTruthy :: a -> Boolean`: a value that is considered to be [`truthy`][truthy]
* `pathEq :: [ String | Integer ] -> a -> Object -> Boolean`: an `Object` that contains the provided key in the traversal path, with a value equal to the provided value. (equality by value)
* `pathSatisfies :: [ String | Integer ] -> ((a -> Boolean) | Pred) -> Object -> Boolean`: an `Object` that contains the provided key in the traversal path with a value that passes the provided predicate.
* `propEq :: (String | Integer) -> a -> Object -> Boolean`: an `Object` that contains the provided key with a value equal to the provided value. (equality by value)
* `propPathEq :: [ String | Integer ] -> a -> Object -> Boolean`: an `Object` that contains the provided key in the traversal path, with a value equal to the provided value. (equality by value)
* `propPathSatisfies :: [ String | Integer ] -> ((a -> Boolean) | Pred) -> Object -> Boolean`: an `Object` that contains the provided key in the traversal path with a value that passes the provided predicate.
* `propSatisfies :: (String | Integer) -> ((a -> Boolean) | Pred) -> Object -> Boolean`: an `Object` that contains the provided key with a value that passes the provided predicate.

[pred]: ../crocks/Pred.html
[ifelse]: logic-functions.html#ifelse
[safe]: helpers.html#safe
[truthy]: https://developer.mozilla.org/en-US/docs/Glossary/Truthy
[falsy]: https://developer.mozilla.org/en-US/docs/Glossary/Falsy
[falsy]: https://developer.mozilla.org/en-US/docs/Glossary/Falsy
4 changes: 4 additions & 0 deletions src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ const isSymbol = require('./predicates/isSymbol')
const isTraversable = require('./predicates/isTraversable')
const isTrue = require('./predicates/isTrue')
const isTruthy = require('./predicates/isTruthy')
const pathEq = require('./predicates/pathEq')
const pathSatisfies = require('./predicates/pathSatisfies')
const propEq = require('./predicates/propEq')
const propPathEq = require('./predicates/propPathEq')
const propSatisfies = require('./predicates/propSatisfies')
Expand Down Expand Up @@ -425,6 +427,8 @@ test('entry', t => {
t.equal(crocks.isTraversable, isTraversable, 'provides the isTraversable predicate')
t.equal(crocks.isTrue, isTrue, 'provides the isTrue predicate')
t.equal(crocks.isTruthy, isTruthy, 'provides the isTruthy predicate')
t.equal(crocks.pathSatisfies, pathSatisfies, 'provides the pathSatisfies predicate')
t.equal(crocks.pathEq, pathEq, 'provides the pathEq predicate')
t.equal(crocks.propEq, propEq, 'provides the propEq predicate')
t.equal(crocks.propPathEq, propPathEq, 'provides the propEq predicate')
t.equal(crocks.propSatisfies, propSatisfies, 'provides the propSatisfies predicate')
Expand Down
2 changes: 2 additions & 0 deletions src/predicates/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ module.exports = {
isTraversable: require('./isTraversable'),
isTrue: require('./isTrue'),
isTruthy: require('./isTruthy'),
pathEq: require('./pathEq'),
pathSatisfies: require('./pathSatisfies'),
propEq: require('./propEq'),
propPathEq: require('./propPathEq'),
propSatisfies: require('./propSatisfies'),
Expand Down
4 changes: 4 additions & 0 deletions src/predicates/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const isString = require('./isString')
const isTraversable = require('./isTraversable')
const isTrue = require('./isTrue')
const isTruthy = require('./isTruthy')
const pathEq = require('./pathEq')
const pathSatisfies = require('./pathSatisfies')
const propEq = require('./propEq')
const propPathEq = require('./propPathEq')
const propSatisfies = require('./propSatisfies')
Expand Down Expand Up @@ -88,6 +90,8 @@ test('predicates entry', t => {
t.equal(index.isTraversable, isTraversable, 'provides the isTraversable predicate')
t.equal(index.isTrue, isTrue, 'provides the isTrue predicate')
t.equal(index.isTruthy, isTruthy, 'provides the isTruthy predicate')
t.equal(index.pathEq, pathEq, 'provides the pathEq predicate')
t.equal(index.pathSatisfies, pathSatisfies, 'provides the pathSatisfies predicate')
t.equal(index.propEq, propEq, 'provides the propEq predicate')
t.equal(index.propPathEq, propPathEq, 'provides the propEq predicate')
t.equal(index.propSatisfies, propSatisfies, 'provides the propSatisfies predicate')
Expand Down
59 changes: 59 additions & 0 deletions src/predicates/pathEq.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/** @license ISC License (c) copyright 2019 original and current authors */
/** @author Karthik Iyengar (karthikiyengar) */
/** @author Ian Hofmann-Hicks */

const curry = require('../core/curry')
const equals = require('../core/equals')
const isArray = require('../core/isArray')
const isDefined = require('../core/isDefined')
const isEmpty = require('../core/isEmpty')
const isInteger = require('../core/isInteger')
const isNil = require('../core/isNil')
const isString = require('../core/isString')

const err = name =>
`${name}: First argument must be an Array of non-empty Strings or Integers`

function fn(name) {
// pathEq :: [ String | Number ] -> a -> Object -> Boolean
function pathEq(keys, value, target) {
if(!isArray(keys)) {
throw new TypeError(err(name))
}

if(isNil(target)) {
return false
}

let acc = target
for(let i = 0; i < keys.length; i++) {
const key = keys[i]

if(!(isString(key) && !isEmpty(key) || isInteger(key))) {
throw new TypeError(err(name))
}

if(isNil(acc)) {
return false
}

acc = acc[key]

if(!isDefined(acc)) {
return false
}
}

return equals(acc, value)
}

return curry(pathEq)
}

const pathEq =
fn('pathEq')

pathEq.origFn =
fn

module.exports = pathEq
109 changes: 109 additions & 0 deletions src/predicates/pathEq.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const test = require('tape')
const helpers = require('../test/helpers')
const isFunction = require('../core/isFunction')
const unit = require('../core/_unit')

const bindFunc = helpers.bindFunc

const pathEq = require('./pathEq')

test('pathEq function', t => {
const fn = pathEq([ 'a' ])
const empty = pathEq([])

t.ok(isFunction(pathEq), 'is a function')

t.equals(fn(undefined, undefined), false, 'returns false with undefined as third argument')
t.equals(fn(null, null), false, 'returns false with null as third argument')
t.equals(fn(NaN, NaN), false, 'returns false with NaN as third argument')

t.equals(empty(undefined, undefined), false, 'returns false with empty array on undefined')
t.equals(empty(null, null), false, 'returns false with empty array on null')
t.equals(empty(NaN, NaN), false, 'returns false with empty array on NaN')

t.end()
})

test('pathEq errors', t => {
const fn = bindFunc(x => pathEq(x, null, {}))

const err = /pathEq: First argument must be an Array of non-empty Strings or Integers/
t.throws(fn(undefined), err, 'throws with undefined in first argument')
t.throws(fn(null), err, 'throws with null in first argument')
t.throws(fn(0), err, 'throws with falsey number in first argument')
t.throws(fn(1), err, 'throws with truthy number in first argument')
t.throws(fn(''), err, 'throws with falsey string in first argument')
t.throws(fn('string'), err, 'throws with truthy string in first argument')
t.throws(fn(false), err, 'throws with false in first argument')
t.throws(fn(true), err, 'throws with true in first argument')
t.throws(fn(unit), err, 'throws with function in first argument')
t.throws(fn({}), err, 'throws with an object in first argument')

t.throws(fn([ undefined ]), err, 'throws with undefined in first argument array')
t.throws(fn([ null ]), err, 'throws with null in first argument array')
t.throws(fn([ NaN ]), err, 'throws with NaN in first argument array')
t.throws(fn([ false ]), err, 'throws with false in first argument array')
t.throws(fn([ true ]), err, 'throws with true in first argument array')
t.throws(fn([ 1.2345 ]), err, 'throws with float in first argument array')
t.throws(fn([ '' ]), err, 'throws with empty string in first argument array')
t.throws(fn([ unit ]), err, 'throws with function in first argument array')
t.throws(fn([ [] ]), err, 'throws with Array in first argument array')
t.throws(fn([ {} ]), err, 'throws with Object in first argument array')

t.end()
})

test('pathEq object traversal', t => {
const fn = pathEq([ 'a', 'b' ])
const empty = pathEq([])

t.equals(fn('', { a: { b: '' } }), true, 'returns true when keypath found and values are equal')
t.equals(fn(null, { a: { b: null } }), true, 'returns true when comparing to null values that are present')
t.equals(fn(NaN, { a: { b: NaN } }), true, 'returns true when comparing to NaN values that are present')

t.equals(fn(true, { a: { c: true } }), false, 'returns false when keypath not found')
t.equals(fn('0', { a: { b: 0 } }), false, 'returns false when keypath is found and values are not equal')
t.equals(fn(undefined, { a: { b: undefined } }), false, 'returns false when comparing undefined values that are present')
t.equals(empty(23, { a: 23 }), false, 'returns false when value does not match object to traverse when path is empty')

t.equals(fn(undefined, { a: undefined }), false, 'returns false when undefined in keypath')
t.equals(fn(null, { a: null }), false, 'returns false when null in keypath')
t.equals(fn(NaN, { a: NaN }), false, 'returns false when NaN in keypath')

t.equals(empty({ a: 23 }, { a: 23 }), true, 'returns true when value matchs object to traverse when path is empty')

t.end()
})

test('pathEq array traversal', t => {
const fn = pathEq([ 1, '0' ])
const empty = pathEq([])

t.equals(fn('', [ false, [ '' ] ]), true, 'returns true when keypath found and values are equal')
t.equals(fn(null, [ '', [ null ] ]), true, 'returns true when comparing to null values that are present')
t.equals(fn(NaN, [ '', [ NaN ] ]), true, 'returns true when comparing to NaN values that are present')
t.equals(empty([ [ null ] ], [ [ null ] ]), true, 'returns true when value matchs array to traverse when path is empty')

t.equals(fn('string', [ 'string' ]), false, 'returns false when keypath not found')
t.equals(fn('1', [ true, [ 1 ] ]), false, 'returns false when keypath is found and values are not equal')
t.equals(fn(undefined, [ 1, [ undefined ] ]), false, 'returns false when compairing undefined values that are present')
t.equals(empty(23, [ 23 ]), false, 'returns false when value does not match array to traverse when path is empty')

t.equals(fn(undefined, [ 1, undefined ]), false, 'returns false when undefined in keypath')
t.equals(fn(null, [ true, null ]), false, 'returns false when null in keypath')
t.equals(fn(undefined, [ '45', [ undefined ] ]), false, 'returns false when comparing undefined values that are present')

t.end()
})

test('pathEq mixed traversal', t => {
const value = 'bubbles'

const fn = pathEq([ 'a', 1 ], value)

t.equals(fn({ a: [ 0, value ] }), true, 'returns false when found with mixed path and values are equal')
t.equals(fn({ a: [ value, 42 ] }), false, 'returns false when found with mixed path and values are not equal')
t.equals(fn({ c: [ value ] }), false, 'returns false when not found with mixed path')

t.end()
})
61 changes: 61 additions & 0 deletions src/predicates/pathSatisfies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/** @license ISC License (c) copyright 2019 original and current authors */
/** @author Ian Hofmann-Hicks (evilsoft) */

const curry = require('../core/curry')
const isArray = require('../core/isArray')
const isEmpty = require('../core/isEmpty')
const isInteger = require('../core/isInteger')
const isNil = require('../core/isNil')
const isPredOrFunc = require('../core/isPredOrFunc')
const isString = require('../core/isString')
const predOrFunc = require('../core/predOrFunc')

const err = name =>
`${name}: First argument must be an Array of non-empty Strings or Integers`

function fn(name) {
// pathSatisfies: [ (String | Integer) ] -> (a -> Boolean) -> b -> Boolean
// pathSatisfies: [ (String | Integer) ] -> Pred a -> b -> Boolean
function pathSatisfies(keys, pred, x) {
if(!isArray(keys)) {
throw new TypeError(err(name))
}

if(!isPredOrFunc(pred)) {
throw new TypeError(
`${name}: Second argument must be a Pred or predicate Function`
)
}

if(isNil(x)) {
return false
}

let target = x
for(let i = 0; i < keys.length; i++) {
const key = keys[i]

if(!(isString(key) && !isEmpty(key) || isInteger(key))) {
throw new TypeError(err(name))
}

if(isNil(target)) {
return false
}

target = target[key]
}

return !!predOrFunc(pred, target)
}

return curry(pathSatisfies)
}

const pathSatisfies =
fn('pathSatisfies')

pathSatisfies.origFn =
fn

module.exports = pathSatisfies
Loading

0 comments on commit 2cfb42f

Please sign in to comment.