Skip to content

jamesdiacono/JSValid

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 

Repository files navigation

JSValid

JSValid is a functional approach to validation in JavaScript. It provides a minimal, composable and extensible way to express and enforce value constraints.

JSValid specifies the signature of a special kind of function, called a validator. Validators accept as a single argument the subject, which is the value to be validated, and return an array of violations. An empty array indicates a valid subject.

A violation is an object with the following properties, of which only message is mandatory:

  • message: A string describing the violation.
  • path: An array of keys describing where the violation occurred (for example, ["people", 0]).
  • code: A string uniquely identifying the type of violation.
  • a: Exhibit A.
  • b: Exhibit B.

A factory is any function that returns a validator function. Here is a demonstration showing how factories, validators and violations work together.

// A validator is made by invoking a factory.

const my_validator = valid.object({
    question: valid.string(),
    answer: valid.integer()
});

// Invoking the validator with a subject returns an array.

const my_violations_array = my_validator({
    question: "What is the answer to life, the universe and everything?",
    answer: "Love."
});

// The array may be inspected for violations.

// [
//     {
//         message: "Not of type integer.",
//         path: ["answer"]
//         code: "not_type_a",
//         a: "integer"
//     }
// ]

The factories

An object containing several factory functions is exported by jsvalid.js:

import valid from "./jsvalid.js";
const {
    boolean,
    number,
    integer,
    string,
    function,
    array,
    object,

    wun_of,
    all_of,
    not,
    literal,
    any
} = valid;

Incidentally, these factories complement the specifiers provided by JSCheck, a testing tool written by Douglas Crockford.

valid.boolean()

The boolean validator permits only true and false.

valid.number()

The number validator permits only numbers that satisfy Number.isFinite. This excludes Infinity and NaN.

valid.number(minimum, maximum, exclude_minimum, exclude_maximum)

Specifying either of minimum or maximum imposes bounds on the subject. Bounds are inclusive unless either of exclude_minimum or exclude_maximum are true.

function valid_latitude() {
    return valid.number(-90, 90);
}
function valid_longitude() {
    return valid.number(-180, 180, false, true);
}
function valid_weight() {
    return valid.number(0);
}

valid.integer()

The integer validator permits only numbers that satisfy Number.isSafeInteger.

valid.integer(minimum, maximum)

Specifying either of minimum or maximum imposes inclusive bounds on the subject.

valid.string()

The string validator permits only strings.

valid.string(regular_expression)

The subject must conform to the regular_expression.

function valid_tracking_number() {
    return valid.string(/^[0-9]{24}$/);
}

valid.string(length_validator)

The length of the subject must conform to the length_validator.

function valid_note() {
    return valid.string(valid.integer(1, 140));
}

valid.function(length_validator)

The function validator permits only functions. The arity of the subject must conform to the length_validator, if it is specified.

valid.array()

The array validator permits only arrays, as determined by Array.isArray.

valid.array(validator, length_validator)

Each element in the subject must conform to the validator. The length of the array must conform to the length_validator, if it is specified.

valid.array(validator_array, length_validator, rest_validator)

Each element in the subject must conform to the validator at the corresponding position in the validator_array. If the length_validator is omitted, the subject must be the same length as the validator_array.

function valid_location() {
    return valid.array([valid_longitude(), valid_latitude()]);
}
function valid_mail_journey() {
    return valid.array(valid_location(), valid.integer(1));
}

It is possible that the length_validator may permit a subject longer than the validator_array. In such a case, each surplus element of the subject must conform to the rest_validator. If the rest_validator is undefined, the sequence of surplus elements must conform to the sequence of validators formed by repeating the validator_array.

valid.object()

The object validator permits only bona fide objects, not null or arrays.

valid.object(required_properties, optional_properties, allow_strays)

The required_properties and optional_properties parameters are objects containing validators. Either parameter may be undefined.

The value of each property on the subject must conform to the corresponding validator in required_properties or optional_properties. Where no corresponding validator is found, the property is permitted only if allow_strays is true. Additionally, the subject must contain every key found on required_properties.

function valid_parcel() {
    return valid.object(
        {
            id: valid_tracking_number(),
            size: "parcel",
            weight: valid_weight()
        },
        {
            delivery_advice: valid_note()
        }
    );
}

valid.object(key_validator, value_validator)

Each key found on the subject by Object.keys must conform to the key_validator. Each corresponding value must conform to the value_validator.

All keys are permitted if the key_validator is undefined. Likewise, all values are permitted if the value_validator is undefined.

function valid_tracking_info() {
    return valid.object(valid_tracking_number(), valid_mail_journey());
}

valid.wun_of(validator_array)

The wun_of validator permits only values that conform to at least wun of the validators in the validator_array.

function valid_size() {
    return valid.wun_of(["letter", "parcel", "postcard"]);
}

A definition of the word "wun" may be found here.

valid.wun_of(validator_object, classifier)

The appropriate validator is chosen according to some characteric of the subject. This helps to produce a more compact violations array.

The validator_object contains the validators. The property names are the classifications. The classifier function is called with the subject, and ideally returns a classification string (or number). The classifier may indicate that the subject is unclassifiable by throwing an exception or returning a value that is not a string (or number).

The subject is permitted if it classifies as and conforms to wun of the validators.

function valid_mail() {
    return valid.wun_of(
        {
            letter: valid_letter(),
            parcel: valid_parcel(),
            postcard: valid_postcard()
        },
        function classifier(subject) {
            return subject.size;
        }
    );
}

valid.all_of(validator_array, exhaustive)

The all_of validator permits only values that conform to every validator in the validator_array. It runs the validators in sequence, stopping at the first violation (unless exhaustive is true).

valid.not(validator)

The not validator permits only values that do not conform to the validator.

function valid_flat_mail() {
    return valid.not(valid_parcel());
}

valid.literal(expected_value)

The literal validator permits only values equal to the expected_value. The === operator is used to determine equality unless either of the values is NaN, in which case Number.isNaN is used.

Some factories provided by JSValid accept validators as arguments. Where a non-function is provided in place of a validator, it is automatically wrapped with valid.literal. Thus, the expression

valid.string(valid.literal(1))

may be written more succinctly as

valid.string(1)

Consequently, valid.literal is only needed in cases where expected_value is a function or undefined.

valid.any()

The any validator permits any value.

But what about...?

It is easy to make your own validators. Here is a factory that returns a validator that permits only multiples of n.

function valid_multiple_of(n) {
    return function (subject) {
        return (
            subject % n === 0
            ? []
            : [{message: `Not a multiple of ${n}.`}]
        );
    };
}