Giter VIP home page Giter VIP logo

vest's Introduction

Vest - Declarative validations framework

Vest

Vest Documentation

Join Discord Github Stars Next Tag Version Downloads bundlephobia Status


Vest is a declarative validations framework designed to simplify the process of writing and maintaining form validations for your web application. Inspired by popular unit testing libraries such as Mocha and Jest, Vest allows developers to describe their validation requirements using a suite-like syntax, separating validation logic from feature logic to create more maintainable and readable code.

Vest's framework-agnostic approach means that it can be used with any UI framework, or without any framework at all. With Vest, you can reduce code bloat, improve feature readability and maintainability, and enhance the user experience of your web application.

test('username', 'Username is required', () => {
  enforce(data.username).isNotBlank();
});

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

test('username', 'Username already taken', async () => {
  await doesUserExist(data.username);
});

Installation

npm i vest

Motivation

Building web applications often involves writing complex forms that require validation. As the complexity of these forms increases, so does the complexity of the validation logic required to ensure data is accurate and complete.

At this point, developers may start to experience issues with code bloat, poor maintainability, and difficulty in managing validation logic across different features of an application. This can lead to bugs, errors, and a poor user experience.

Vest was designed to address these issues by providing a simple, intuitive way to write form validation that is easy to learn, scalable, and extensible. By separating validation logic from feature logic, Vest helps developers create maintainable code that is easy to update, debug, and refactor.

With Vest, developers can reduce the complexity and increase the readability of their code, leading to more efficient development cycles, fewer bugs, and a better user experience overall.

Why Vest?

Writing form validations can be time-consuming and complex, especially as your web application grows and evolves over time. Vest simplifies the process by providing a set of powerful tools that take care of the annoying parts for you, such as managing validation state and handling async validations.

Vest's declarative syntax is also designed to be easy to learn, especially for developers who are already familiar with unit testing frameworks. With Vest, you can leverage your existing knowledge to write effective form validations quickly and easily.

💡 Easy to Learn

Vest adopts the syntax and style of unit testing frameworks, so you can leverage the knowledge you already have to write your form validations.

🎨 Framework Agnostic

Vest is framework-agnostic, which means you can use it with any UI framework out there.

🧠 Takes Care of the Annoying Parts

Vest manages its validation state, handles async validations, and much more, so you don't have to.

🧩 Extendable

You can easily add new kinds of validations to Vest according to your needs.

♻️ Reusable Validation Logic

Validation logic in Vest can be shared across multiple features in your app, making it easy to maintain and refactor your codebase.

🧬 Supports Declarative Syntax

Vest's declarative syntax makes it easy to describe your form or feature structure and write clear, concise validations.

🧪 Promotes Testing and Debugging

By separating validation logic from feature logic, Vest makes it easier to test and debug your code, which can save you time and reduce errors.

Getting Started

Vest Documentation

Here are some code sandboxes to get you started:

Contribute

Information describing how to contribute can be found here:

https://github.com/ealush/vest/blob/latest/CONTRIBUTING.md

vest's People

Contributors

adife avatar almoghaimo avatar barryp avatar coobaha avatar dependabot-preview[bot] avatar dependabot[bot] avatar ealush avatar elliot-alexander avatar eyalcohen4 avatar galitpauz avatar ganeshpatil0101 avatar gaspoute avatar gaweki avatar gerardo-rodriguez avatar happytiptoe avatar kapalex avatar lirantal avatar lpizzinidev avatar mentalgear avatar moses3301 avatar norbertluszkiewicz avatar omrilugasi avatar pascalmh avatar roblevintennis avatar ronen-e avatar snyk-bot avatar syncush avatar vligas avatar vonagam avatar vuchl 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  avatar

Watchers

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

vest's Issues

Add enforce.any() rule

At the moment, when chained, all rules have an AND relationship.

enforce(4).isNumeric().isNumber().largerThan(2);
/*
enforce that 4
is numeric AND
is of type number AND
is larger than 2
*/

It can be useful to also have OR relationships between enforcement rules such as

OR RELATIONSHIP
At least one rule must be truthy

enforce(value).any(
  enforce.isEmpty(),
  enforce.greaterThan(10)
);

This used to be difficult to achieve, but with the recent change #479 that allows for lazy evaluated enforcements (enforce.isArray()([])) it should be pretty straightforward.

Instructions:

To get this working, you need to add a new rule, you can call it any or anyOf, and have it take multiple functions. It should pass each of these functions the enforced value.

What we have at the moment is this:

enforce.isNumber() // -> returns a function that takes a value
enforce.isNumber()(5) // -> returns true
enforce.isNumber()('not_a_number') // -> returns false

The assumption is that the consumer will do this:

enforce(5).any(
  enforce.isNumber(),
  enforce.isUndefined()
);

and the any rule, will call each of the functions with 5.

See Shape for reference.

[N4S] RULE isNull

Add a new enforce rule:

enforce(5).isNull(null) // ✅
enforce(5).isNull(1) // 🚨
enforce(5).isNull(undefined) // 🚨

[N4S] RULE isUndefined

Add a new enforce rule:

enforce(5).isUndefined(undefined) // ✅
enforce(5).isUndefined() // ✅
enforce(5).isUndefined(false) // 🚨

Add a cross field validation page in the docs

Based on this issue, #475, I realized there's a need to describe the different ways to have a cross field validation. The ways are briefly described in the issue, but they need to be turned into a documentation page.

The main scenarios:
Either/Or validations: At least one field needs to be validated
And validations: A field can only be validated if another field has been validated

Please submit a PR with the docs if you want to take it.

isNotEmpty() on date objects

 test('date', 'Just testing a date object.', () => {
    enforce(new Date())
      .isNotEmpty()
  });

This returns a validation error although new Date() is not empty. Seems like the isNotEmpty() method only works on strings, arrays and objects. How would you check if a date object exists or even is properly formatted?

Awesome project! Keep up the great work.

No contributing guidelines available

The PR template references a CONTRIBUTING.md file for contribution guidelines but the file is unavailable in the repository.

PR template text:

<!--
Before creating a pull request, please read our contributing guidelines:

CONTRIBUTING.md

Remember: Unless it is an urgent bugfix, please use `next` as the base for your PR

Please fill the following form (leave what's relevant)
-->

vest/schema problems with typescript

Hi,

I was trying to write some code to test the experimental vest/schema updates we discussed a couple of weeks ago (finally got to it at work). Not sure why but my Typescript project refuses to use vest/schema because it simply says it is an interface type. The error I get is:

'schema' only refers to a type, but is being used as a value here.

I cannot get typescript to import the file in vest/schema.js for the life of me. Any ideas?

FEAT: add `get` and `reset` interfaces to suite directly

At the moment you can use vest.get(suiteName) to get the suite's state, and vest.reset(suiteName) to erase it.

It could be nice to be able to access those without having to hold a reference to the suite name:

// validate.js

import vest from 'vest';

export default vest.create('suite-name', () => {
 /* ... */
});
// app.js

import v from './validate.js';

const res = v.get();

v.reset();

Can probably be added here:
https://github.com/ealush/vest/blob/latest/packages/vest/src/core/createSuite/index.js#L44-L58

React example 2 - password has wrong validation

Steps to reproduce:

  1. Type in a password with more than 1 char.
  2. Focus out of the field

Expected:
Field should pass validation
Actual:
Field fails on validation

I did not check yet wether the issue is due to the example code or the actual library.

add enforce.loose()

enforce.shape() supports defining an object schema, but it requires an exact match of an object. If the object contains extra keys, it will throw an error.

enforce.loose should implement the same behaviour of enforce.shape, but also allow extra keys to be present in the data object.

See Shape for reference.

[N4S] Rule isNegative | isPositive

Add a new enforce rule:

enforce(-5).isNegative() // ✅
enforce(5).isNegative() // 🚨
enforce(5).isPositive() // ✅
enforce(-5).isPositive() // 🚨

add enforce.allOf compound for AND style requirements

Similar to anyOf, we need an AND relationship rule: allOf.

In the following example value must both be a string and longer than 10.

enforce(value).allOf(
  enforce.longerThan(10),
  enforce.isString,
);

This can be even more useful when combined with templates and shape validations:

const User = enforce.template(
  enforce.loose({
    id: enforce.isNumber()
    name: enforce.shape({
      first: enforce.isString(),
      last: enforce.isString(),
      middle: enforce.optional(enforce.isString()),
    }),
  })
);

const DisabledAccount = enforce.template(
  enforce.loose({
    disabled: enforce.equals(true)
  })
)

enforce(value).allOf(
  User,
  DisabledAccount
);

Developer guidelines

  1. Create a new file called allOf right next to anyOf. It should be pretty much the same, implementing the rules.some logic with some other counter mechanism.

  2. To actually have it used by enforce, add it to this compounds export

  3. Add a new test file here and test different scenarios

  4. Add suitable types here and here (basically duplicate anyOf).

  5. If you feel like it, also add suitable docs for this feature here

IEnforceRules types does not exist when use enforceExtended in TypeScript

I'm not very good at English, so I'm sorry if my sentences aren't right.

I get the following error when I try to use from TypeScript:

import vest, { test } from 'vest'
import enforce from 'vest/enforceExtended'

const validate = vest.create('form', (data: { message: string }) => {
  test('message', 'message is Required', () => {
    enforce(data.message).isNotEmpty()
  })
})

export default validate
# yarn run tsc
src/validate.ts:6:27 - error TS2339: Property 'isNotEmpty' does not exist on type 'EnforceExtendMap<ExtendedRules>'.

6     enforce(data.message).isNotEmpty()
                            ~~~~~~~~~~

This is just a suggestion, but we'll fix vest.d.ts:

type EnforceExtendMap<T> = IEnforceRules<T> & {
  [K in keyof T]: (...args: any[]) => EnforceExtendMap<T>;
};

And replace this code:

IEnforceRules<T> & EnforceExtendMap<T>EnforceExtendMap<T>

If it's OK, I'll create a pull-request.

Uppercase regex does not work

Hello.

I'm using vest with React, and can't get some tests to work.
Could you please take a look?

  const validationSuite = vest.create((data = {}) => {
    // works
    test('password', 'Password must be at least 8 chars', () => {
      enforce(data.password).longerThanOrEquals(8);
    });
    //works
    test('password', 'Password must contain a digit', () => {
      enforce(data.password).matches(/[0-9]/);
    });

    // does not work
    test('password', 'Password must contain an uppercase symbol', () => {
      enforce(data.password).matches(/[^A-Z]/);
    });

    // does not work
    test('password', 'Password must contain an lowercase symbol', () => {
      enforce(data.password).matches(/[^a-z]/);
    });

    // does not work
    test('password', 'Password must contain a special character', () => {
      enforce(data.password.replace(/[^a-zA-Z0-9]/g, '')).lengthNotEquals(0);
    });
  });

  // react-hook-form
  const signUpForm = useForm({
    reValidateMode: 'onChange',
    resolver: vestResolver(validationSuite),
  });

Optimize library for size

Vest is still a relatively small library compared to its feature-set and also compared to other libraries in that space, but I believe its size could be reduced further down if we better structure the project.

This issue is here so we remember to prioritize for size.

Publish vest module on npm

The version published in npm contains only the minified code intended for browsers, so the sample code in the docs doesn't work in node.

import vest, { test, enforce } from 'vest'
               ^^^^
SyntaxError: The requested module 'vest' does not provide an export named 'test'

Nested fields ?

Hi,
Thanks for your work and this package.

I would like to validate objects with a lot of nested structure, like this:

{
	foos: [{ bar: '...' }, { bar: '...' }, { bar: '...' }, ...]
}

Do you recommend a particular way to write the validation tests with vest?

I tought about something basic like this, but maybe is there a better way?

const validate = vest.create('form', (formData) => {
	formData.foos.forEach((fooObject) => {
		test('bar', 'Must be between 2 and 10 chars', () => {
	        enforce(fooObject.bar).longerThanOrEquals(2).shorterThan(10);
	    });
	})
});

Thanks,
Mathieu.

Add suite method shorthands

At the moment we can access all suite methods via suite.get(). Alternatively, it would be very simple to add all the suite methods directly on top of suite:

Functions should be added here: https://github.com/ealush/vest/blob/latest/packages/vest/src/core/suite/createSuite.js#L60
They can all rely internally on suite.get().

Types should be added here: https://github.com/ealush/vest/blob/latest/packages/vest/src/typings/vest.d.ts#L168

Type summary:

hasErrors: (fieldName?: string) => boolean;

hasWarnings: (fieldName?: string) => boolean;

getErrors(): { [fieldName: string]: string[] };
getErrors(fieldName: string): string[];

getWarnings(): { [fieldName: string]: string[] };
getWarnings(fieldName: string): string[];

hasErrorsByGroup(groupName: string): boolean;
hasErrorsByGroup(groupName: string, fieldName: string): boolean;

hasWarningsByGroup(groupName: string): boolean;
hasWarningsByGroup(groupName: string, fieldName: string): boolean;

getErrorsByGroup(groupName: string): { [fieldName: string]: string[] };
getErrorsByGroup(groupName: string, fieldName: string): string[];

getWarningsByGroup(groupName: string): { [fieldName: string]: string[] };
getWarningsByGroup(groupName: string, fieldName: string): string[];

[N4S] - Add support for enforce.anyOf()

Will allow an at least one relationship between enforce assertions.

enforce(value).anyOf(enforce.longerThan(5), enforce.isEmpty())

This requires quite a big change to the way enforce currently works.

Add enforce.arrayOf() rule

This rule can be quite useful when a future schema validation rule will be added to enforce.

The general idea is quite similar to the enforce.any rule:

enforce(value).isArrayOf(
  enforce.isString(),
  enforce.isNumber(),
 enforce.isArrayOf(...) // Yeah, why not? A recursive call should be pretty simple here
)

The idea is that at least one needs to be truthy in order for the condition to be met - alternatively, an empty array should probably be valid as well unless explicitly mentioned:

enforce(value).isArrayOf(
  enforce.isString()
).isNotEmpty()

See Shape for reference.

[N4S] Add jest style enforce rule failures

At the moment, validation rules are simple functions that return a boolean. If the rule's return value is false, enforce will throw an Error.

Rule example:

function greaterThan(value, arg1) {
return isNumeric(value) && isNumeric(arg1) && Number(value) > Number(arg1);
}

And where it throws:

if (rule(value, ...args) !== true) {
throw new Error(`[Enforce]: invalid ${typeof value} value`);
}

This means that the thrown error is always a generic one, not indicating the rule thrown, or the expected behavior of the rule.

Jest has a nicer interface for constructing matchers that also includes a custom failure message for that rule:
https://jestjs.io/docs/en/expect#expectextendmatchers

This can be very beneficial for Vest DEV_MODE that's being planned. A debug dump could log all the failed rules and their failure messages.

I would like to preserve the current behavior of Vest rules to prevent a breaking change, but also support the more verbose failure style of:

{
	message: (ruleName, enforceValue, ...args) => ``, // someErrorMessage
    pass: true/false
}

Restructure state directory

The idea is to move all the modules that directly relate to a suite and put them in the same directory: packages/vest/src/core/suite.

  • create core/suite directory
    • move createSuite to suite/create
    • move validate to suite/validate
    • move registerSuite to suite/register
    • move reset to suite/reset
    • move remove to suite/remove
    • move getSuiteState to suite/getState
    • move patch to suite/patch
    • move cleanupCompletedSuite to suite/cleanupCompleted
    • move getSuite to suite/getSuite (maybe just get? need to see how it feels to actually use it)
    • move setSuite to suite/setSuite (maybe just set? need to see how it feels to actually use it)
    • Move hasRemainingTests to suite/hasRemainingTests

[N4S] RULE startsWith

Add a new enforce rule:

enforce('hello').startsWith('hell') // ✅
enforce('Hi').startsWith('t') // 🚨

add enforce.oneOf compound for mutually exclusive validations

Similar to anyOf, we need a mutual exclusion rule: oneOf.

In the following example value must either be shorter than five OR longer than 10, but never both.

enforce(value).oneOf(
  enforce.longerThan(10),
  enforce.shorterThan(5),
);

This can be even more useful when combined with templates and shape validations:

const User = enforce.template(
  enforce.shape({
    id: enforce.isNumber(),
    name: enforce.shape({
      first: enforce.isString(),
      last: enforce.isString(),
      middle: enforce.optional(enforce.isString()),
    }),
  })
);
const Group = enforce.template(
  enforce.shape({
    id: enforce.isNumber(),
    name: enforce.isString(),
  })
);

enforce(value).oneOf(
  User,
  Group
);

serial and parallel test support

The idea is to have the option to run the tests in serially to force those tests not to run parallely.
The idea is from ava framework(https://openbase.io/js/ava-tf/documentation)
By adding .serial after the tests you can enforce it

import vest, { test } from 'vest';
import enforce from 'vest/enforceExtended';

export default vest.create('user_form', (data = {}, currentField) => {
  vest.only(currentField);

  test.serial('username', 'Username is required', () => {
    enforce(data.username).isNotEmpty();
  });

  test.serial('username', 'Username is too short', () => {
    enforce(data.username).longerThanOrEquals(3);
  });

  test.serial('password', 'Password is required', () => {
    enforce(data.password).isNotEmpty();
  });

  test.serial('password', 'Password already used', async () => {
    const response = await axios.post('http://localhost:3000/auth')
    enforce(response.code).matches(/200/);
  });

  if (data.password) {
    test.serial('confirm_password', 'Passwords do not match', () => {
      enforce(data.confirm_password).equals(data.password);
    });
  }

  test.serial('email', 'Email Address is not valid', () => {
    enforce(data.email).isEmail();
  });

  test.serial('tos', () => {
    enforce(data.tos).isTruthy();
  });
});

What do you think?

Thanks,
Ziv

[N4S] RULE endsWith

Add a new enforce rule:

enforce('hello').endsWith('lo') // ✅
enforce('Hi').endsWith('H') // 🚨

enforceExtended and promisify not found

Hi and thanks for the a great library!

I have trouble importing enforceExtended for some reason. Is the file missing or am I missing something?

Things I've tried:

// nope
import enforceExtended from 'vest/enforceExtented'; 

// installed n4s separately and tried this without success
import enforce from 'n4s/enforceExtended';

https://ealush.com/n4s/#/./business_rules

Is the documentation lagging behind? Should I install and use validator.js to solve email validation?

https://ealush.com/vest/#/./n4s/external

Same thing when trying to import promisify looks like.

import promisify from 'vest/promisify';
// Package "vest" exists but package.json "exports" does not include entry for "./promisify".

I am using Snowpack as the bundler btw, but my gut feeling it's package related and not bundler specific.

Update: I can import promisify from vest/promisify so it might be bundler specific after all 🤔

Also, this in Snowpack.

// fails
import { test, enforce, create } from 'vest';
// [chrome]: The requested module '../../web_modules/vest.js' does not provide an export named 'test'
// [firefox]: Uncaught SyntaxError: import not found: enforce

// this works in both Chrome and FF
import vest from 'vest';
const { create, enforce, test } = vest;

Very strange. Any ideas? Is this maybe affected by how the final package is generated? I am just brainstorming here.

Export validator rules individually

Currently enforceExtended is 29kb due to the validator.js rules. If we export each rule individually and let consumers mix and match it will make it way smaller.

import { enforce } from 'vest';
import isEmail from 'vest/enforce/isEmail;

enforce.extend({ isEmail })

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.