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

shapely

v0.2.3

Published

Runtime type checker with tagged unions

Downloads

143

Readme

Shapely

Build Status

Runtime javascript type checker with support for records and tagged unions.

Use cases

I use Shapely in projects where:

  • I can't use static type checkers.
  • I can use static type checkers, but I still need to validate the shape of some data in runtime, because:
    • I have a client/server setup and I need to ensure that the server API calls and their responses follow predefined protocols.
    • I have a set of functions (possibly written by different team members) and I need to ensure that all of their return values follow a certain protocol.

Example

Here we have a simple client/server setup:

  • The server implements three functions: getCartItems, getItemById, and addItemToCart, and they are written by different team members.
  • Each of these functions is expected to:
    • Return {kind: 'Success', payload: any} when invoked successfully.
    • Return {kind: 'Failure', message: String} when invoked unsuccessfully.
  • We need to validate that each function actually follows this protocol every time it's invoked. We'll validate that both on the server and on the client.
// ServerResponse.js

// This file defines the server's resopnse protocol,
// and it'll be used both by the server and the client.

import {record, union, any} from 'shapely';

// This is basically a tagged union that defines the two
// possible shapes of the server's response data. The equivalent
// flow type would look like this:
// type ServerResponse =
//    {kind: 'Success', payload: mixed} |
//    {kind: 'Failure', message: string}
export const ServerResponse = union(
  record({
    kind: 'Success',
    payload: any
  }),

  record({
    kind: 'Failure',
    message: String
  })
);
// server.js

// The server imports the three functions, listens for requests from the client,
// routes each request to the correct function, and then validates the returned value
// of that function before sending it to the client.

import {ServerResponse} from './ServerResponse';
import {isValid} from 'shapely';

import {getCartItems} from './fns/getCartItems';
import {getItemById} from './fns/getItemById';
import {addItemToCart} from './fns/addItemToCart';
const fns = {getCartItems, getItemById, addItemToCart};

onRequest((fnName, args, respond) => {
  var responsePromise;
  if (fns[fnName]) {
    responsePromise = fns[fnName](args);
  } else {
    responsePromise = Promise.reject({
      kind: 'Failure',
      message: `Unkown function ${fnName}`
    });
  }

  function handleResponse(responseData) {
    if (isValid(ServerResponse, responseData)) {
      respond(responseData);
    } else {
      respond({kind: 'Failure', message: 'Internal server error'});
    }
  };

  return responsePromise.then(handleResponse, handleResponse);
});
// client.js

// The client simply sends requests to the server,
// and returns a promise that either fulfills or rejects,
// based on the server's response.

import {isValid} from 'shapely';
import {ServerResponse} from './ServerResponse';

export function request(fnName, args) {
  return callServer(fnName, args)
  .then((response) => {
    if (!isValid(ServerResponse, response)) {
      return Promise.reject('Invalid server response');
    }

    if (response.kind === 'Success') {
      return response.payload;
    } else {
      return Promise.reject(response);
    }
  });
};

Usage

validate()

Takes a validator and a value:

  • If valid, returns the same value.
  • If invalid, throws.

Example:

import {validate} from 'shapely';

validate(String, 'a'); // returns 'a'
validate(String, 10); // throws

isValid()

Takes a validator and a value. Returns true/false.

Example:

import {isValid} from 'shapely';

isValid(String, 'a'); // returns true
isValid(String, 10); // returns false

Strings, Numbers, Booleans

import {isValid} from 'shapely';

isValid(String, 'a'); // returns true
isValid(Number, 10); // returns true
isValid(Boolean, false) // returns true

Strict String Equality

import {isValid} from 'shapely';

isValid('a', 'a'); // returns true since they are equal
isValid('a', 'b'); // returns false

Any

import {isValid, any} from 'shapely';

isValid(any, ...); // returns true for any value, including null

Unions

Unions are just a collection of other validators:

import {isValid, union} from 'shapely';

const StringOrNumber = union(String, Number);

isValid(StringOrNumber, 'a'); // returns true
isValid(StringOrNumber, 10); // returns true
isValid(StringOrNumber, {}); // returns false

Records

Records check if an object has certain keys and whether those keys match their respective validators.

import {isValid, record} from 'shapely';

const LaunchVehicle = record({
  model: String,
  year: Number
});

isValid(LaunchVehicle, {model: 'Falcon 9', year: 2010}); // returns true
isValid(LaunchVehicle, {model: 'Falcon 9', year: 2010, extras: 'whatever'}); // returns true, as only the expected keys are checked
isValid(LaunchVehicle, {model: 'Falcon 9', year: false}); // returns false

Tagged Unions

Tagged unions are just unions of tagged records:

import {union, record, any} from 'shapely';

const Response = union(
  record({
    kind: 'Success',
    payload: any
  }),

  record({
    kind: 'Failure',
    message: String
  })
);

isValid(Response, {kind: 'Success', payload: 10}); // returns true
isValid(Response, {kind: 'Failure', message: 'Bad args'}); // returns true
isValid(Response, {kind: 'Failure'}); // returns false

Arrays

import {arrayOf, union, isValid} from 'shapely';

const ArrayOfNumbersOrStrings = arrayOf(union(Number, String));

isValid(ArrayOfNumbersOrStrings, [10, 11, 12]); // returns true
isValid(ArrayOfNumbersOrStrings, ['a', 'b', 'c']); // returns true
isValid(ArrayOfNumbersOrStrings, ['a', 'b', 12]); // returns true
isValid(ArrayOfNumbersOrStrings, [10, 11, {}]); // returns false

Maps

import {mapOf, isValid} from 'shapely';

const MapOfBooleans = mapOf(Boolean);

isValid(MapOfBooleans, {a: true, b: false}); // returns true
isValid(MapOfBooleans, {a: true, b: 'a'}); // returns false

Optional values

import {record, optional} from 'shapely';

const LaunchVehicle = record({
  name: String,
  year: Number,
  firstLaunchYear: optional(Number)
});

isValid(
  LaunchVehicle,
  {name: 'Falcon 9', year: 2010, firstLaunchYear: 2010}
  ); // returns true

isValid(
  LaunchVehicle,
  {name: 'Falcon 5', year: 2002}
  ); // returns true

Nil

import {isValid, nil} from 'shapely';

isValid(nil, null); // returns true
isValid(nil, undefined); // returns true
isValid(nil, 'a'); // returns false

Recursive data types

import {record, union, deferred, nil, any} from 'shapely';

const BinaryTree = union(
  nil,
  record({
    val: any,
    left: deferred(=> BinaryTree), // we use deferred() because at this line, BinaryTree is not defined yet.
    right: deferred(=> BinaryTree),
  })
);

isValid(
  BinaryTree,
  {
    val: 10,
    left: {
      val: 8,
      left: {val: 4},
      right: {val: 9}
    },
    right: {
      val: 13,
      right: {val: 15}
    }
  }
  ); // returns true

Custom validators

You can make your own custom validators, provided they implement the Validator interface:

type Validator = {
  isValid (val: mixed): boolean;
  getValidationResult (val: mixed): ValidationResult;
}

type ValidationResult =
  {isValid: 'true'} |
  {isValid: 'false', message: string, score: number}

Example:

import {isValid} from 'shapely';

const Tuple = {
  isValid(val) {
    return Array.isArray(val) && val.length === 2;
  }

  getValidationResult(val) {
    if (!Array.isArray(val)) {
      return {
        isValid: 'false',
        message: `Array expected, ${typeof val} given.`,
        score: 0 // 0 means a complete type mismatch
      };
    } else if (val.lenth === 2) {
      return {isValid: 'true'};
    } else {
      return {
        isValid: 'false',
        message: `A tuple 2 two elements. ${val.length} given`,
        score: 1 // 1 means the general type matches, but the details don't.
      };
    }
  }
};

isValid(Tuple, [1, 2]); // returns true
isValid(Tuple, [1]); // returns false
validate(Tuple, [1, 2]); // returns [1, 2]
validate(Tuple, [1]); // throws Error: val.length is ecpected to be 2. 1 given

Development

Shapely is itself written in Flow, but like all flow code, it still compiles if you don't have flow on your system. We use Babel for compilation, Webpack for bundling, and LiveScript for writing the tests.

To watch the files and recompile:

$ cd path/to/shapely
$ npm run dev

To run the tests:

$ npm test

or:
$ npm run test:watch

License

MIT