Git Product home page Git Product logo

fusspot's Introduction

@mapbox/fusspot

Build Status

Fusspot is a tiny runtime type-assertion library.

It can run in the browser as well as Node, and it's lightweight, flexible, and extensible.

Table Of Contents

Why does this exist?

Many existing runtime type-assertion libraries solve a special problem, like form validation or component props, or aren't great for usage in the browser, because of size or syntax. We wanted something similar to React's prop-types but not attached to a specific use case. So we ended up creating Fusspot.

Installation

npm install @mapbox/fusspot

Usage

Basic

In the example below we have a simple validation of an object and its properties. The outermost validator v.shape checks the shape of the object and then runs the inner validator v.arrayOf(v.string) to validate the value of names property.

@mapbox/fusspot exports a single object for its API. In the examples below we name it v (for "validator").

const v = require("@mapbox/fusspot");

assertObj = v.assert(
  v.shape({
    names: v.arrayOf(v.string)
  })
);

assertObj({ names: ["ram", "harry"] }); // pass
assertObj({ names: ["john", 987] }); // fail
assertObj({ names: "john" }); // fail

Required

By default null and undefined are acceptable values for all validators. To not allow null/undefined as acceptable values, you can pass your validator to v.required to create a new validator which rejects undefined/null.

// without v.required
assertObj = v.assert(
  v.shape({
    name: v.string
  })
);
assertObj({}); // pass
assertObj({ name: 'ram' }); // pass
assertObj({ name: undefined }); // pass
assertObj({ name: null }); // pass
assertObj({ name: 9 }); // fail

// with v.required
strictAssertObj = v.assert(
  v.shape({
    name: v.required(v.string)
  })
);

strictAssertObj({}); // fail
strictAssertObj({ name: 'ram' }); // pass
strictAssertObj({ name: undefined }); // fail
strictAssertObj({ name: null }); // fail
strictAssertObj({ name: 9 }); // fail

Composition

You can compose any of the Higher-Order Validators to make complex validators.

const personAssert = v.assert(
  v.shape({
    name: v.required(v.string),
    pets: v.arrayOf(
      v.shape({
        name: v.required(v.string),
        type: v.oneOf("dog", "pig", "cow", "bird")
      })
    )
  })
);

// assertion passes
personAssert({
  name: "john",
  pets: [
    {
      name: "susy",
      type: "bird"
    }
  ]
});

// assertion fails
personAssert({
  name: "harry",
  pets: [
    {
      name: "john",
      type: "mechanic"
    }
  ]
});
// Throws an error
//   pets.0.type must be "dog", "pig", "cow", or "bird".
const personAssert = v.assert(
  v.shape({
    prop: v.shape({
      person: v.shape({
        name: v.oneOfType(v.arrayOf(v.string), v.string)
      })
    })
  })
);

// assertion passes
personAssert({ prop: { person: { name: ["j", "d"] } } });
personAssert({ prop: { person: { name: ["jd"] } } });

// assertion fails
personAssert({ prop: { person: { name: 9 } } });
// Throws an error
//   prop.person.name must be an array or string.

Assertions

v.assert(rootValidator, options)

Returns a function which accepts an input value to be validated. This function throws an error if validation fails else returns void.

Parameters

  • rootValidator: The root validator to assert values with.
  • options: An options object or a string. If it is a string, it will be interpreted as options.description.
  • options.description: A string to prefix every error message with. For example, if description is myFunc and a string is invalid, the error message with be myFunc: value must be a string. (Formerly options.apiName, which works the same but is deprecated.)
v.assert(v.equal(5))(5); // undefined
v.assert(v.equal(5), { description: "myFunction" })(10); // Error: myFunction: value must be 5.
v.assert(v.equal(5), 'Price')(10); // Error: Price: value must be 5.

Primitive Validators

v.any

This is mostly useful in combination with a higher-order validator like v.arrayOf;

const assert = v.assert(v.any);
assert(false); // pass
assert("str"); // pass
assert(8); // pass
assert([1, 2, 3]); // pass

v.boolean

const assert = v.assert(v.boolean);
assert(false); // pass
assert("true"); // fail

v.number

const assert = v.assert(v.number);
assert(9); // pass
assert("str"); // fail

v.finite

const assert = v.assert(v.finite);
assert(9); // pass
assert("str"); // fail
assert(NaN); // fail

v.plainArray

const assert = v.assert(v.plainArray);
assert([]); // pass
assert({}); // fail

v.plainObject

const assert = v.assert(v.plainObject);
assert({}); // pass
assert(new Map()); // fail

v.func

const assert = v.assert(v.func);
assert('foo'); // fail
assert({}); // fail
assert(() => {}); // pass

v.date

Passes if input is a Date that is valid (input.toString() !== 'Invalid Date').

const assert = v.assert(v.date);
assert('foo'); // fail
assert(new Date('2019-99-99')); // fail
assert(new Date()); // pass
assert(new Date('2019-10-04')); // pass

v.string

const assert = v.assert(v.string);
assert("str"); // pass
assert(0x0); // fail

v.nonEmptyString

const assert = v.assert(v.nonEmptyString);
assert("str"); // pass
assert(""); // fail
assert(7); // fail

Higher-Order Validators

Higher-Order Validators are functions that accept another validator or a value as their parameter and return a new validator.

v.shape(validatorObj)

Takes an object of validators and returns a validator that passes if the input shape matches.

const assert = v.assert(
  v.shape({
    name: v.required(v.string),
    contact: v.number
  })
);

// pass
assert({
  name: "john",
  contact: 8130325777
});

// fail
assert({
  name: "john",
  contact: "8130325777"
});

v.strictShape(validatorObj)

The same as v.shape, but rejects the object if it contains any properties that are not defined in the schema.

const assert = v.assert(
  v.strictShape({
    name: v.required(v.string),
    contact: v.number
  })
);

// passes, just like v.shape
assert({
  name: "john",
  contact: 8130325777
});

// fails, just like v.shape
assert({
  name: "john",
  contact: "8130325777"
});

// fails where v.shape would pass, because birthday is not defined in the schema
assert({
  name: "john",
  birthday: '06/06/66'
});

v.objectOf(validator)

Takes a validator as an argument and returns a validator that passes if and only if every value in the input object passess the validator.

const assert = v.assert(
  v.objectOf({ name: v.required(v.string) })
);

// pass
assert({
  a: { name: 'foo' },
  b: { name: 'bar' }
});

// fail
assert({
  a: { name: 'foo' },
  b: 77
});

v.arrayOf(validator)

Takes a validator as an argument and returns a validator that passes if and only if every item of the input array passes the validator.

const assert = v.assert(v.arrayOf(v.number));
assert([90, 10]); // pass
assert([90, "10"]); // fail
assert(90); // fail

v.tuple(...validators)

Takes multiple validators that correspond to items in the input array and returns a validator that passes if and only if every item of the input array passes the corresponding validator.

A "tuple" is an array with a fixed number of items, each item with its own specific type. One common example of a tuple is coordinates described by a two-item array, [longitude, latitude].

const assert = v.assert(v.tuple(v.range(-180, 180), v.range(-90, 90)));
assert([90, 10]); // pass
assert([90, "10"]); // fail
assert([90, 200]); // fail
assert(90); // fail

v.required(validator)

Returns a strict validator which rejects null/undefined along with the validator.

const assert = v.assert(v.arrayOf(v.required(v.number)));
assert([90, 10]); // pass
assert([90, 10, null]); // fail
assert([90, 10, undefined]); // fail

v.oneOfType(...validators)

Takes multiple validators and returns a validator that passes if one or more of them pass.

const assert = v.assert(v.oneOfType(v.string, v.number));
assert(90); // pass
assert("90"); // pass

v.equal(value)

Returns a validator that does a === comparison between value and input.

const assert = v.assert(v.equal(985));
assert(985); // pass
assert(986); // fail

v.oneOf(...values)

Returns a validator that passes if input matches (===) with any one of the values.

const assert = v.assert(v.oneOf(3.14, "3.1415", 3.1415));
assert(3.14); // pass
assert(986); // fail

v.range([valueA, valueB])

Returns a validator that passes if input inclusively lies between valueA & valueB.

const assert = v.assert(v.range([-10, 10]));
assert(4); // pass
assert(-100); // fail

v.instanceOf(Class)

Returns a validator that passes if input is an instance of the provided Class, as determined by JavaScript's instanceof operator.

class Foo {}
class Bar {}
class Baz extends Bar {}

const assert = v.assert(v.instanceOf(Bar))
assert(new Bar()); // pass
assert(new Baz()); // pass
assert(new Foo()); // fail

Custom validators

One of the primary goals of Fusspot is to be customizable out of the box. There are multiple ways to which one can create a custom validator. After creating a custom validator you can simply use it just like a regular validator i.e. pass it to v.assert() or use it with Higher-Order Validators.

Simple

A simple custom validator is a function which accepts the input value and returns a string if and only if the input value doesn't pass the test. This string should be a noun phrase describing the expected value type, which would be inserted into the error message like this value must be a(n) <returned_string>. Below is an example of a path validator for node environment.

const path = require('path');

function validateAbsolutePaths(value) {
  if (typeof value !== 'string' || !path.isAbsolute(value)) {
    return 'absolute path';
  }
}

const assert = v.assert(validateAbsolutePaths);
assert('../Users'); // fail
// Error: value must be an absolute path.
assert('/Users'); // pass

**For more examples look at the [src code](https://github.com/mapbox/fusspot/blob/master/lib/index.js#L238).**

Customizing the entire error message

If you need more control over the error message, your validator can return a function ({path}) => '<my_custom_error_message>' for custom messages, where path is an array containing the path (property name for objects and index for arrays) needed to traverse the input object to reach the value. The example below help illustrate this feature.

function validateHexColour(value) {
  if (typeof value !== "string" || !/^#[0-9A-F]{6}$/i.test(value)) {
    return ({ path }) =>
      `The input value '${value}' at ${path.join(".")} is not a valid hex colour.`;
  }
}

const assert = v.assert(
  v.shape({
    colours: v.arrayOf(validateHexColour)
  })
);

assert({ colours: ["#dedede", "#eoz"] }); // fail
// Error: The input value '#eoz' at colours.1 is not a valid hex colour.
assert({ colours: ["#abcdef"] }); // pass

fusspot's People

Contributors

adamfrey avatar davidtheclark avatar kepta avatar pathmapper avatar t3h2mas avatar thibaudlopez avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fusspot's Issues

Implement and/or document extension mechanism

One of the goals of the library is to make it easy to integrate custom validators. Right now that feature is undocumented. Let's make sure it is in fact easy and then document the method.

Maybe remove date and coordinates validators, which seem specific to mapbox-sdk-js

The coordinates validator seems specific to mapbox-sdk-js.

And although "date" sounds like it might be generic enough, it's actually kind of specific to how we decide to type that for mapbox-sdk-js: anything that can be converted to a Date via new Date(x). I'm not sure that's generically useful, since it implies implementation details behind the scenes.

@kepta do you think we should keep either of these?

New name

@kepta I'm thinking we should give this repo/library a less generic name. How about "formalist" or "fusspot"?

Non-empty string assertion

I very often find myself wanting to verify not just that a value is a string but also that that string is not empty. It's very easy to end up with a '' through automatic coercion in JS when a value is essentially undefined or null.

@kepta what do you think about adding a v.nonEmptyString?

v.number should reject NaN

Although typeof NaN === 'number', I'd expect that you always want to reject NaN if you're expecting a real number.

Add Date object validator

I think every single time I use this project I also want a Date object validator.

Here's a quick implementation:

function validateDate(value) {
  if (!(value instanceof Date) || value.toString() === 'Invalid Date') {
    return 'date';
  }
};

@kepta any objections to getting this into the main library?

Using fusspot for more than validation?

TLDR: Certain functions in node.js application could benefit from fusspot's parameter validation. This could be used as a sort of react-props for node.js which validates only when running the tests.

A lot of times a simple typo could silently sneak in and cause unexpected behaviour. In the example below, emphasise would remain falsy since foo gets the wrong parameter property.

const foo = ({ abc, xyz, tgif, emphasise }) => {
   if (emphasise) {
      return chalk.bold('I am emphasised');
   }
}

foo({ abc, xyz, tgif, emphasize }); // a simple American to British glitch

Also, if a fellow developer decides to change foo's parameter schema, she/he needs to dig into all the call sites of foo to fix it. These problems aren't anything new and can be carefully fixed with unit testing.

My proposal is that we use fusspot along with unit testing to write less test cases and catch bugs early on. A function could be wrapped around an assertion function (v.assertParams?) and the code would only validate when running tests i.e. NODE_ENV=test else it would simply be a passthrough (for performance reasons).

One of the drawbacks of this approach would be that we would need to write the parameters at two places and keep them in sync. Also, it is a non standard solution and might throw some people off.

Is coordinates validator too strict with longitude?

There are valid reasons for providing longitudes that exceed +/-180, so I worry that our validator is being too strict.

Should we only enforce that latitude is +/-90, since I don't know of any reasons to exceed those limits? Or should we instead remove this validator and just check that we have an array of two numbers?

cc @mapbox/frontend-platform

Shortcut to set apiName option — possibly rename that option

I want to start trying to use this library frequently to check function arguments that are not necessary objects. In order to get nice clear error message, then, I need to set that apiName option. I was thinking it might be more ergonomic if we could say "If the second argument to assert is a string, we'll consider it the equivalent of apiName." — e.g. v.assert(x, 'something').

I'm also wondering if we might want to rename the apiName option property. I don't really understand the api part of that. How about description?

@kepta what do you think?

Unnecessary space between items in list

Error: The following keys of value did not pass validation:

      - files: files is required.

      - settings: settings is required.

Seems like there are too many empty lines involved.

0.7.1 is a breaking change

@kepta I think we should revert 0.7.1 and the change for #39.

The misunderstanding there is that if a property is actually required, that should be made explicit with fusspot.required. In @kellyoung's example where f.assert(f.strictShape({name: f.string}))({}) passes, that's fine name is not required. If name were required, the schema should be changed to f.stripeShape({ name: f.required(f.string)) }).

Enable NODE_ENV performance optimisation

It would be a great feature, especially for frontend applications, if we could provide an assertion function which doesn't run any validation code when NODE_ENV=production. We can write code for this function in such a way that helps code bundler strip the validation code.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.