npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@josephluck/valley

v0.0.1

Published

Functional validation

Downloads

4

Readme

npm version CircleCI Status

Contents

Features

  • Simple and easy to use validation
  • Takes type-safety seriously, with maximum inference
  • Multiple constraints per-field
  • Asynchronous constraints
  • Fp-ts compatible version available

Installation

With yarn:

yarn add @josephluck/valley

With npm:

npm i --save @josephluck/valley

Usage

The most basic example is a validator with synchronous constraints:

import { makeValidator } from "@josephluck/valley";

/**
 * Set up some rules. These can (and should be) shared
 */
const rules = {
  greaterThan: (expected: number) => (value: number) =>
    value > expected
      ? void null
      : `Expected ${value} to be greater than ${expected}`,
  divisibleBy: (div: number) => (value: number) =>
    value % div === 0
      ? void null
      : `Expected ${value} to be divisible by ${div}`,
};

/**
 * Set up a type representing your form fields.
 * This isn't strictly necessary as Valley can infer the shape of your form
 * fields from the constraints you pass it, however it can be convenient to see
 * the shape of your form fields explicitly.
 */
type Fields = {
  twenty: number;
  thirty: number;
  fourty: number;
};

/**
 * Create a validate function passing in a set of constraints per field.
 */
const validate = makeValidator<Fields>({
  twenty: [rules.divisibleBy(10), rules.greaterThan(25)],
  thirty: [rules.divisibleBy(10), rules.greaterThan(25)],
  fourty: rules.divisibleBy(10),
});

/**
 * Run the validate function over the constraints. Each constraint will run in
 * the order passed, and the first to fail will be passed back as a message.
 */
const errors = validate({
  twenty: 20,
  thirty: 30,
  fourty: 40,
});

console.log(errors);
// { twenty: "Expected 20 to be greater than 25", thirty: undefined, fourty: undefined }

Constraints can access other field values:

const rules = {
  confirmPassword: (
    value: string,
    _key: string,
    fields: { password: string; [key: string]: any }
  ) => (value === fields.password ? void null : "Passwords do not match"),
};
type Fields = {
  email: string;
  password: string;
  confirmPassword: string;
};
const validate = makeValidator<Fields>({
  email: [],
  password: [],
  confirmPassword: rules.confirmPassword,
});
const errors = validate({
  email: "[email protected]",
  password: "BobsDaBest",
  confirmPassword: "BobsDaWorst",
});
console.log(errors);
// { email: undefined, password: undefined, confirmPassword: "Passwords do not match" }

Constraints can be asynchronous:

type Fields = {
  email: string;
};
/**
 * Usually these constraints would all be packaged up in a separate module for
 * reusability, and imported where needed. Otherwise it looks a bit gnarly
 * in-line...
 */
const validate = makeValidator<Fields>({
  email: [
    async (value) => {
      try {
        if (!value.includes(".") || !value.includes("@")) {
          throw new Error("Not a valid email");
        }
      } catch (err) {
        return err.message;
      }
    },
    async (value) => {
      try {
        const response = await api.getAccount(value);
        if (response.data) {
          throw new Error("Account already exists");
        }
      } catch (err) {
        return err.message;
      }
    },
  ],
});
const errors = await validate({
  email: "[email protected]",
});
console.log(errors);
// { email: "Account already exists }

Functional usage

Valley is compatible with fp-ts using the fp variant of makeValidator. This is useful if you're working with fp-ts's Either or TaskEither types with pipe to validate data before operating on it. If you want something more robust (with a higher learning curve), I'd recommend io-ts.

Constraints have to return Either<string, V> or TaskEither<string, V> depending on whether your constraint is synchronous or asynchronous (where T represents the field's value, which should be returned by the constraint function if the field passes validation).

If all constraints return Eithers, the validation function will return Either<Record<keyof Fields, string>, Fields>. If any constraint return a TaskEither, the validation function will return TaskEither<Record<keyof Fields, string>, Fields>. The left side of the either is the failed case containing validation messages, and the right side is the fields which are passed through if all constraints for all fields pass.

Here's an example of synchronous validation using Eithers:

import * as E from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/pipeable";
import { makeValidator } from "@josephluck/valley/fp";

const isOfType = (type: string) => <T>(value: T): E.Either<string, T> =>
  typeof value === type ? E.right(value) : E.left(`Expected a ${type}`);

const isString = isOfType("string");

const isNumber = isOfType("number");

const isEqualTo = <T>(expected: T) => <V extends T>(
  value: V
): E.Either<string, V> =>
  value === expected
    ? E.right(value)
    : E.left(`Expected ${value} to equal ${expected}`);

const isGreaterThan = <T>(expected: T) => <V extends T>(
  value: V
): E.Either<string, V> =>
  value > expected
    ? E.right(value)
    : E.left(`Expected ${value} to be greater than ${expected}`);

type Fields = {
  name: string;
  age: number;
};

const validate = makeValidator<Fields>({
  name: [isString, isEqualTo("Bob")],
  age: [isNumber, isGreaterThan(40)],
});

pipe(
  validate({
    name: "Bob",
    age: 32,
  }),
  E.fold(
    (errors) => {
      /**
       * If any constraint fails
       */
      console.log(errors);
      // { age: "Expected 32 to be greater than 40", name: undefined }
    },
    (fields) => {
      /**
       * If all constraints pass
       */
      console.log(fields);
      // { name: "Bob", age: 32 }
    }
  )
);

If any constraint returns a TaskEither, the validate function returns a TaskEither:

import * as E from "fp-ts/lib/Either";
import * as TE from "fp-ts/lib/TaskEither";
import { pipe } from "fp-ts/lib/pipeable";
import { makeValidator } from "@josephluck/valley/fp";

const isOfType = (type: string) => <T>(value: T): E.Either<string, T> =>
  typeof value === type ? E.right(value) : E.left(`Expected a ${type}`);

const isString = isOfType("string");

const isNumber = isOfType("number");

const isEqualTo = <T>(expected: T) => <V extends T>(
  value: V
): E.Either<string, V> =>
  value === expected
    ? E.right(value)
    : E.left(`Expected ${value} to equal ${expected}`);

const isGreaterThan = <T>(expected: T) => <V extends T>(
  value: V
): E.Either<string, V> =>
  value > expected
    ? E.right(value)
    : E.left(`Expected ${value} to be greater than ${expected}`);

const asyncIsEqualTo = <T>(expected: T) => (value: T) =>
  TE.tryCatch(
    async () => {
      if (value !== expected) {
        throw new Error(`Expected ${value} to equal ${expected}`);
      }
      return value;
    },
    (err: Error) => err.message
  );

const asyncIsGreaterThan = (expected: number) => (value: number) =>
  TE.tryCatch(
    async () => {
      if (value < expected) {
        throw new Error(`Expected ${value} to be greater than ${expected}`);
      }
      return value;
    },
    (err: Error) => err.message
  );

type Fields = {
  name: string;
  age: number;
};

const validate = makeValidator<Fields>({
  name: [isNumber, asyncIsEqualTo("Bob")],
  age: [asyncIsGreaterThan(40), isString],
});

const pipeline = pipe(
  validate({ name: "Bob", age: 50 }),
  TE.mapLeft((errors) => {
    /**
     * If any constraint fails
     */
    console.log(errors);
    // { name: "Expected a number", age: "Expected a string" }
  }),
  TE.map((fields) => {
    /**
     * If all constraints pass
     */
    console.log(fields);
    // { name: "Bob", age: 50 }
  })
);
await pipeline();