Coder Social home page Coder Social logo

fiverr / passable Goto Github PK

View Code? Open in Web Editor NEW
99.0 62.0 11.0 3.98 MB

Declarative data validations.

Home Page: https://fiverr.github.io/passable/

License: MIT License

JavaScript 98.60% Shell 1.40%
data-validation validations isomorphic isomorphic-validations

passable's Introduction

Passable

Declarative data validations.

npm version Build Status

What is Passable?

Passable is a library for JS applications for writing validations in a way that's structured and declarative.

Inspired by the syntax of modern unit testing framework, passable validations are written as a spec or a contract, that reflects your form structure. Your validations run in production code, and you can use them in any framework (or without any framework at all).

The idea behind passable is that you can easily adopt its very familiar syntax, and transfer your knowledge from the world of testing to your form validations.

Much like most testing frameworks, Passable comes with its own assertion function, enforce, all error based assertion libraries are supported.

Key features

  1. Non failing tests.
  2. Conditionally running tests.
  3. Async validations.
  4. Test callbacks.

Syntactic differences from testing frameworks

Since Passable is running in production environment, and accommodates different needs, some changes to the basic unit test syntax have been made, to cover the main ones quickly:

  • Your test function is not available globally, it is an argument passed to your suite's callback.
  • Each test has two string values before its callback, one for the field name, and one for the error returned to the user.
  • Your suite accepts another argument after the callback - name (or array of names) of a field. This is so you can run tests selectively only for changed fields.
// validation.js
import passable, { enforce } from 'passable';

const validation = (data) => passable('NewUserForm', (test) => {

    test('username', 'Must be at least 3 chars', () => {
        enforce(data.username).longerThanOrEquals(3);
    });

    test('email', 'Is not a valid email address', () => {
        enforce(data.email)
            .isNotEmpty()
            .matches(/[^@]+@[^\.]+\..+/g);
    });
});

export default validation;
// myFeature.js
import validation from './validation.js';

const res = validation({
    username: 'example',
    email: '[email protected]'
});

res.hasErrors() // returns whether the form has errors
res.hasErrors('username') // returns whether the 'username' field has errors
res.getErrors() // returns an object with an array of errors per field
res.getErrors('username') // returns an array of errors for the `username` field

"BUT HEY! I ALREADY USE X VALIDATION LIBRARY! CAN IT WORK WITH PASSABLE?"

As a general rule, Passable works similarly to unit tests in term that if your test throws an exception, it is considered to be failing. Otherwise, it is considered to be passing.

There are a few more ways to handle failures in order to ease migration, and in most cases, you can move your validation logic directly to into Passable with only a few adjustments.

For example, if you use a different assertion libraries such as chai (expect) or v8n, you can simply use it instead of enforce, and it should work straight out of the box.

passable's People

Contributors

a1vy avatar adife avatar chellik avatar costellosean avatar dependabot-preview[bot] avatar dependabot[bot] avatar ealush avatar fiverrbot avatar greenkeeper[bot] avatar michael5r avatar michasherman avatar netanelben avatar omrilotan avatar raphaelboukara avatar ronen-e avatar sahariko avatar

Stargazers

 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  avatar  avatar  avatar  avatar  avatar  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

passable's Issues

Add `someOf` runner for "compound enforces"

Carrying on from #20.

Instead of abusing the anyOf runner in order to support compound enforce, we can instead allocate a new one, with its own syntax.

As described in #20: Support a way to group .anyOf() conditions

Currently we support these three main enforce conditionals:
noneOf(), allOf(), anyOf().

With anyOf we can easily determine whether a single condition is true, so we can do things like:

  • may either be a string or a number
enforce(value).anyOf({
    isString: true,
    isNumber: true
});
  • may be empty or larger than 55
enforce(value).anyOf({
    isEmpty: true,
    largerThan: 55
});

But there is no straightforward way to test against groups of conditionals, for example:

  • May be an array or a number smaller than 25

Multiple ideas were suggested (thanks @ronen-e), but none of the suggestion solutions could provide both ease of use and readability/style. I now believe it is time to support a fourth runner: someOf

Why someOf

Similar to anyOf, someOf hints that we're looking for at least one of an unspecified amount of items to pass the test.

What would the syntax be?

Currently, the most fitting syntax in my opinion would be per-argument-evaluation, meaning:

enforce(data.value).someOf({
    isArray: true,
    isEmpty: false
}, {
    isNumber: true,
    largerThan: 55
});

In the example above, we allow data.value to either be a non-empty-array or a number larger than 55.

Functional Enforce

Hi @ealush,
I looked that the implementation of passable Enforce was using "class" and "this" keywords and tried to re-implement that in fully functional way here https://github.com/Attrash-Islam/passable-functional-enforce with flat rules folder.

I'd like to have your notes regards the way it was implemented? and if you think that we can change the way it implemented in the original "Passable" package here.

Thank you

Proxy rules from the enforce function itself

Allow further simplification of the syntax by allowing to chain rules directly to enforce, just like other runners.

Instead of:

enforce(age).allOf({largerThan: 25});

Allow:

enforce(age).largerThan(25);

This simple change can reduce clutter in simpler input validations that do not use many rules.

enforce(email).isEmpty(false).matches(/regexp/).noneOf({...})

Add rule `sizeBetween`

Add sizeBetween rule that returns true for values that have size in an expected range.

enforce(44).sizeBetween(10, 100);

Is simply short for:

enforce(44).largerThan(10).smallerThan(100);

Turn enforce into its own constructor

Enforce will not be consumed from within passable anymore - the old API for consuming enforce and extending with custom rules should be deprecated.

passable('name', 'specific', (pass,enforce) => {}, custom)

Instead, there will be two ways of consuming enforce:

Enforce class constructor

Use this if you need custom rules in your project.

import { Enforce } from 'passable';

// customRulesObject = 
// {
//    isValidEmail: emailValidationFunction
//}

const enforce = new Enforce(customRulesObject)

passable('formName', specific, (pass) => {
   pass('field', 'error string', () => {
      enforce(value).isValidEmail();
   });
});

Instance import from Passable

Use this if you need to use enforce as is, no added functionality:

import { enforce } from 'passable';

passable('formName', specific, (pass) => {
   pass('field', 'error string', () => {
      enforce(value).isNumber();
   });
});

Why?

Currently, the heaviest action in the passable lifecycle is enforce initialization. Whenever enforce gets called, it iterates all the rules, runners and custom rules and wraps them with a function that passes down the supplied value from the context.

This naive proxying happens each time we call enforce. The ideal is to do it only once in the lifetime of the whole app, this is by initializing an instance of enforce with all the functions when first importing it and use a Proxy to call the correct function with the supplied value.

Add rules `isTruthy()` and `isFalsy()`

Similar to Jest's .toBeFalsy() and .toBeTruthy().

In JavaScript, there are six falsy values: false, 0, '', null, undefined, and NaN. Everything else is truthy.

Use when you want to enforce that a value is true or false in a boolean context.

isTruthy(2) // true
isTruthy("hi") // true
isTruthy("") // false
isTruthy(false) // false
isFalsy(2) // false
isFalsy("hi") // false
isFalsy("") // true
isFalsy(false) // true

Index Passable errors using categories

Assigning each error category a prefix would make adding errors and debugging nicer in the future, for example, all argument-related errors are between 100-200.

Also adding the error number/id to the error message ([Passable #371] for example) would be more constructive and will make error tracking or FAQs more comfortable, even more so when we'll have numerous error messages.

Add getter methods to the passable response object

Add getter methods for properties in the response object to simplify accessing the errors and warnings.

const validity = passable(...);

validity.getErrors('fieldName'); // ['error string']
validity.getErrors(); // {fieldName: ['error string']}
validity.getWarnings('fieldName'); // ['warning string']
validity.getWarnings(); // {fieldName: ['warning string']}

[RULE] Add `isNumeric`

add isNumeric rule that determines if a value is of numeric value, unlike isNumber which checks the type of the value.

Change `specific` field filter: Introduce 'only' and 'not' | Make mandatory

Add specific the ability to accept an object. Make it mandatory.

Making it a mandatory argument will reduce bloat from the very complex passableArgs function, and help shipping passable with type definitions which is hard to do with optional arguments.

/**
 * Initiates passable validation process
* @param {String} name
* @param {String | Array<String> | {not: String | Array<String>, only: String | Array<String>}} specific
* @param {Function} passes
* @param {Object} custom
 * 
*/
function passable(name, specific, passes, custom) {...}
// not example
passable('formName', {not: 'field_1'} , (pass) => {});
passable('formName', {not: ['field_1', 'field_2']} , (pass) => {});
// only example
passable('formName', {only: 'field_1'} , (pass) => {});
passable('formName', {only: ['field_1', 'field_2']} , (pass) => {});

// old API still supported as `only`:
passable('formName', ['field_1', 'field_2'] , (pass) => {});
passable('formName', 'field_2' , (pass) => {});

Either document or deprecate the undocumented .fin() api

.fin() is a function that can be chained to an enforce function to get a boolean with the result of that enforce. It is currently undocumented in the docs. We should either deprecate it, as it is not really useful, or document and maintain it.

Add rule `closeTo`

Similar to Chai's closeTo:

Asserts that the target is a number that’s within a given +/- delta range of the given number expected.

enforce(1.5).closeTo(3, 1);

Support a way to group .anyOf() conditions

Currently we support these three main enforce conditionals:
noneOf(), allOf(), anyOf().

With anyOf we can easily determine whether a single condition is true, so we can do things like:

  • may either be a string or a number
enforce(value).anyOf({
    isString: true,
    isNumber: true
});
  • may be empty or larger than 55
enforce(value).anyOf({
    isEmpty: true,
    largerThan: 55
});

But there is no straightforward way to test against groups of conditionals, for example:

  • May be an array or a number smaller than 25

Initially I thought of the following syntax

enforce(value).anyOf({
    isArray: true
}, {
    isNumber: true,
    smallerThan: 25
});

But I think it would be too confusing, as it is not inherently different than the normal way of doing it:
Do the internal conditionals (in each object) also go according to anyOf? Does it mean that it is actually going to run as: an array, or a number, or smaller than 25? or each of the objects work internally with allOf?

We could, of course, support this syntax, only by changing its name:

enforce(value).anyGroup({
    isArray: true
}, {
    isNumber: true,
    smallerThan: 25
});

But it is not as self explanatory as the others.


I am currently leaning towards:

enforce(value).anyOf([{
    isArray: true
}, {
    isNumber: true,
    smallerThan: 25
}]);

as it does add another layer on top of anyOf, hinting that the normal mode of operation does not apply here anymore.

Scripts are not running in PR's Checks

We need to run the scripts in PR's Checks and block PR unless all the checks pass.

image

Currently, PR don't have such a checks!

image

.. And even when running lint locally there's a bunch of errors that we could prevented by blocking PRs:

image

Importing passable into a TypeScript file causes compilation errors.

Using [email protected].

Importing passable at the top of a .ts file as follows:

import passable, {enforce} from 'passable';

causes TypeScript to generate the following errors upon compilation:

node_modules/passable/index.d.ts:1:9 - error TS2614: Module '"passable"' has no exported member 'IEnforceInstance'. Did you mean to use 'import IEnforceInstance from "passable"' instead?

1 import {IEnforceInstance} from "passable";
          ~~~~~~~~~~~~~~~~

node_modules/passable/index.d.ts:13:5 - error TS2666: Exports and export assignments are not permitted in module augmentations.

13     export default passable;
       ~~~~~~

node_modules/passable/index.d.ts:14:5 - error TS2666: Exports and export assignments are not permitted in module augmentations.

14     export { enforce, Enforce, validate, WARN, FAIL, VERSION };

No errors are displayed in my IDE (Visual Studio Code) while coding and TypeScript-based auto-completion for passable seems to work. The errors only appear upon compilation.

When I remove the import {IEnforceInstance} from "passable"; statement from index.d.ts in the passable project root, all compilation errors seem to be resolved.

My tsconfig.json is as follows:

{
    "compilerOptions": {
        "allowJs"         : true,
        "lib"             : ["ES2017", "DOM"],
        "target"          : "ES2017",
        "module"          : "CommonJS",
        "outDir"          : "dist",
        "rootDir"         : "src",
        "sourceMap"       : true,
        "incremental"     : true,
        "esModuleInterop" : true
    },
    "include": ["src/**/*"]
}

Deprecate `fail` severity, rename to `error`

Fail should be renamed to error to comply with Pasaable's output object.

Since it is a breaking change, a deprecation error should be logged to the console. error should be accepted as well.

In the next major, fail should be removed completely.

Add a simplified `validate` function

Add a simplified validate function that does not require the whole passable() boilerplate. This function will be more suited for single-field validation.

For example:

import {validate, enforce} from 'passable';

const validity = validate('error message', () => {
    enforce(...).allOf(...);
});
// validity:
// {
//    valid: false,
//    message: 'error message'
// } 

Add index.js.flow file in dist folder

Add index.js.flow in dist for easy consumption.

This by itself is pretty easy, but it requires major changes in the API:

  • Make specific mandatory (#70)
  • Remove the fail/warn argument from pass (or make it mandatory as well)

Fix documentation mistakes and inconsistencies.

* In code samples, use only lowercase passable() (and not Passable())
* Update all ways to fail a test to reflect there are only two. Give enforce as a throw example.

  • Make sure all examples and explanations are up-to date with the latest API
  • Use forms only as examples. Generally refer to data models and not be refer to forms as the sole intention of the library.

Related to @raphaelboukara's comments in #75

Skipped field name appears multiple times if multiple passes exist

If multiple passes exist for the same field, when the field is skipped (using 'specific'), the result object will list the skipped field multiple times correlating the the amount of existing tests.

skipped: ['field_2', 'field_3', 'field_5', 'field_5']

It is not a big deal, but can be annoying. I believe it should be fixed in the next major.

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.