npm run setup
installs all the thingsnpm run lint
to check for styling issuesnpm run spec:dev
to have tests running while codingnpm run spec:coverage
to ensure coverage is within expected levelsnpm run test
to run all things that will run on CI
This page will act as the basis for the crocks
coding standards. The more
this document is followed, the less comments you will receive on your PR. If you
find anything that does not match these coding standards, feel free to create a
PR to align the code with them
Each main file (not .spec
) file has a header similiar to the below. If you add
a new file, put your own details in as well as the current year. If the file
already has a header there is no need to update it.
/** @license ISC License (c) copyright 2019 original and current authors */
/** @author Dale Francis (dalefrancis88) */
The following is an example of the normal structure of a core library file
using find
as an example of this structure. It is also a good example of
how each argument should be validated.
As stated above, the header should display the year of authorship along with the details of the author within the structure of the following license statement
/** @license ISC License (c) copyright 2018 original and current authors */
/** @author Dale Francis (dalefrancis88) */
The imports should be in two alphabetized groups with data types as the first group and functions as the second.
const Pred = require('../core/types').proxy('Pred')
const curry = require('../core/curry')
const isFoldable = require('../core/isFoldable')
const isFunction = require('../core/isFunction')
const isSameType = require('../core/isSameType')
const predOrFunc = require('../core/predOrFunc')
Local or destructured functions from your imports are done between the imports and the main function.
const { Just, Nothing } = require('.')
const accumulator = fn => (acc, cur) =>
!acc.found && predOrFunc(fn, cur) ? { found: true, value: cur } : acc
The main function must have a function signature written above using the
Hindley–Milner type notation. If you need help, just ask on
the Gitter channel. Each argument should be validated to be it's
expected type, throwing a TypeError
if it is not. Error messages should use a
consistent voice, preferring active over passive voices where possible:
Good: "First argument must be a Number"
Bad: "Number is required for first argument"
/** find :: Foldable f => ((a -> Boolean) | Pred) -> f a -> Maybe a */
function find(fn, foldable) {
if(!isFunction(fn) && !isSameType(Pred, fn)) {
throw new TypeError('find: First argument must be a Pred or predicate')
}
if(!isFoldable(foldable)) {
throw new TypeError('find: Second argument must be a Foldable')
}
const result = foldable.reduce(accumulator(fn), { found: false })
return result.found ? Just(result.value) : Nothing()
}
The final piece of the puzzle is exporting the main function. As a standard
all functions exported from crocks
are curried
module.exports = curry(find)
The following is an excerpt of the spec
file for find
. It shows the
structure as well as the expected level of testing for each function.
Imports are done very similiar to main functions with the minor change
that tape
is placed on it's own line.
const test = require('tape')
const Pred = require('../Pred')
const List = require('../core/List')
const Maybe = require('../core/Maybe')
const constant = require('../combinators/constant')
const find = require('./find')
const isFunction = require('../core/isFunction')
const isNumber = require('../core/isNumber')
const isSameType = require('../core/isSameType')
const { bindFunc } = require('../test/helpers')
const { fromArray } = List
The spec file will contain one or many collections of tests grouped by subject matter. For example testing input validation for an argument. We also generally test with the following set of values to ensure there is proper protection. Remember, writing tests is about regression and protection from future changes.
test('find is protected from bad fn', t => {
const fn = bindFunc(fn => find(fn, []))
const err = /^TypeError: find: First argument must be a Pred or predicate/
t.throws(fn(undefined), err, 'throws if fn is undefined')
t.throws(fn(null), err, 'throws if fn is null')
t.throws(fn(0), err, 'throws if fn is falsey number')
t.throws(fn(1), err, 'throws if fn is truthy number')
t.throws(fn(NaN), err, 'throws if fn is NaN')
t.throws(fn(''), err, 'throws if fn is falsey string')
t.throws(fn('string'), err, 'throws if fn is truthy string')
t.throws(fn(false), err, 'throws if fn is false')
t.throws(fn(true), err, 'throws if fn is true')
t.throws(fn({}), err, 'throws if fn is empty POJO')
t.throws(fn({ hi: 'there' }), err, 'throws if fn is non-empty POJO')
t.end()
})
If you become familiar with the documentation, its structure and format will become easy to replicate. The docs consist of two main document structures and within them a particular coding started for the sample code. For function list pages it is a simple structure of title, signature, description and example. The container types documentation is just a larger version of this standard.
A description should contain the direct import path to the function followed by the functions signature then a details description of the functions purpose and features using plain language. Any library function references should be linked
crocks/combinators/composeB
composeB :: (b -> c) -> (a -> b) -> a -> cProvides a means to describe a composition between two functions. it takes two functions and a value. Given
composeB(f, g)
, which is readf
afterg
, it will return a function that will take valuea
and apply it tog
, passing the result as an argument tof
, and will finally return the result off
. This allows only two functions, if you want to avoid things like:composeB(composeB(f, g), composeB(h, i))
then check outcompose
.
The imports
have three sections the first is a single line that is the subject
of the example, the second is an alphabetic list of and container types that
are required and the third is an alphabetic list of all the functions required.
We also out of habit will show some basic usage first, then launch into a more
real world scenario.
import composeB from 'crocks/combinators/composeB'
import Either from 'crocks/Either'
import ifElse from 'crocks/logic/ifElse'
import isString from 'crocks/predicates/isString'
When writing documentation examples start out with very simple usages to show how the functions works, followed by a more real-world scenario. Each function should also have a proper signature above it. When you invoke the function, it should also have the result of that invocation commented below.
// yell :: String -> String
const yell = x =>
`${x.toUpperCase()}!`
// safeYell :: a -> Either a String
const safeYell = ifElse(
isString,
composeB(Right, yell),
Left
)
safeYell('quite')
//=> Right "QUITE!"
safeYell(42)
//=> Left 42