Code Monkey home page Code Monkey logo

treacherous's Introduction

Treacherous

A modern async validation framework with a raft of features for node or the browser.

treacherous-image

Build Status Codacy Badge Codacy Coverage Npm Version Npm Downloads Join Discord Chat

Treacherous is an attempt to bring some consistency to validation in the javascript world, allowing you to write your validation rules for your models once, and re-use them on any platform with any framework.

Benefits

  • Fully async validation
  • Separation of rules and validation allowing composable rule sets
  • Supports nested complex objects/arrays
  • Outside in validation, does not augment your models in any way
  • Works in browser or server
  • Write once, use anywhere
  • Can be integrated with any view framework (i.e vue, knockout, aurelia etc)
  • Generic pipeline for localization customization

Features

Reactive Validation

Treacherous can optionally watch your model to see if any properties it is validating change and automatically re-validate those fields when changed, even in nested objects or arrays.

Composite Rules / Virtual Properties

Support for validating at property level using normal rules or for creating composite rules which are applied at the model level, allowing you to validate multiple properties within the same context.

Predicate Based Validation

You can specify predicates to only apply certain validation rules when criteria are met allowing your model validity to be contextual and flexible.

Property Alias'

This is mainly for when you are using treacherous within the browser, but you can provide alias' to properties so when errors are reported the property alias is displayed rather than the actual property name, as who wants to see hasConfirmedTermsAndConditions when you could just alias that field as T&Cs.

Full Support for Typescript

The whole of Treacherous was written in typescript and can bring some nice time saving features to typescript users like async/await and lambda style interactions.

Don't worry if you dont use/like typescript, you can still use all of treacherous' features as if it were a native javascript framework.


Installing

Via NPM

Just do an npm install @treacherous/core

In browser

As this is distributed as a commonjs module it is recommended that you consume it via your existing module loader, or in the scenario where you do not have one it is recommended that you use webpack to just package it up as a UMD module to consume with a global name, this may automatically happen when typescript 2.0 provides this functionality for UMD modules out of the box.


Simple Examples

Validating simple models

import {createRuleset, createGroup} from "@treacherous/core";

const simpleModel = {
    foo: 20,
    bar: "hello"
};

const ruleset = createRuleset()
    .forProperty("foo")
        .addRule("required")        // The property is required
        .addRule("maxValue", 20)    // The property needs a value <= 20
    .forProperty("bar")
        .addRule("maxLength", 5)     // The property neds a length <= 5
    .build();
    
const validationGroup = createGroup()
    .build(simpleModel, ruleset);

validationGroup.validate()
    .then((isValid) => {
        console.log(isValid); // should write true
    });

Ruleset Shorthand

const ruleset = createRuleset()
    .forProperty("foo")
        .required()
        .maxValue(20)
    .forProperty("bar")
        .maxLength(5)
    .build()

Validating simple arrays in models

const simpleModel = {
    foo: [10, 20, 30]
};

const ruleset = createRuleset()
    .forProperty("foo")
        .addRule("maxLength", 5)        // The array can only contain <= 5 elements
        .addRuleForEach("maxValue", 20) // Each element needs a value <= 20
    .build();
    
const validationGroup = createGroup().build(simpleModel, ruleset);

validationGroup.getModelErrors(true) // the true value indicates a full revalidation
    .then((errors) => {
        console.log(errors); // should contain { "foo[2]": "<some error about max value>" }
    });

Nested validation on the fly

const complexObject = {
    foo: {
       bar: "hello"
    }
};

const ruleset = createRuleset()
    .forProperty("foo")
    .then(fooBuilder => {
        fooBuilder.forProperty("bar")
            .required()
            .maxLength(2)
    })
    .build();

const validationGroup = createGroup().build(complexObject, ruleset);

validationGroup.getModelErrors(true)
    .then((errors) => {
        console.log(errors); // should contain { "foo.bar": "<some error about max length>" }
    });

Creating Validation Groups

The validation group is the object which manages validation state, you can find out a lot more information on this within the docs.

Here are a few simple examples to save you trawling the docs.

Check current validity

const validationGroup = createGroup()
    .build(...);

validationGroup.validate()
    .then((isValid) => {
        // true is valid, false is invalid
    ));

Get all errors

const validationGroup = createGroup()
    .build(...);

validationGroup.getModelErrors()
    .then((propertyErrors) => {...));

Subscribe validation changes

const validationGroup = createGroup()
    .build(...);

validationGroup.propertyStateChangedEvent.subscribe((propertyValidationChangedEvent) => {...));

Typescript users

As typescript users you can get some nicer features and intellisense so you can create typed rules allowing you to use lambda style property location like so:

const ruleset = createRuleset<SomeModel>()
    .addProperty(x => x.SomeProperty)
        .required()
        .matches(x => x.SomeOtherProperty)
    .build();

You can also make use of async/await for almost all async methods like so:

const modelErrors = await validationGroup.getModelErrors();
console.log(modelErrors);

Validation rules

The framework comes with built in validators for the following:

  • date - The value is expressible as a date
  • decimal - The value is expressible as a float/single
  • email - The value conforms to a valid email address
  • equal - The value is equal to another value
  • isoDate - The value conforms to a valid ISO date format
  • maxLength - The value must have a length <= value
  • maxValue - The value must have a value <= value
  • minLength - The value must have a length >= value
  • minValue - The value must have a value >= value
  • notEqual - The value is not equal to another value
  • number - The value is expressible as an integer
  • regex - The value matches the regex pattern
  • required - The value is not a null-like value
  • step - The value conforms to the numeric steps provided
  • matches - The value must match another property in the model

Creating custom rules

Creating custom rules is pretty easy, you need to:

  • Implement IValidationRule with your custom logic (JS users just match the signatures of the interface)
  • Add validation messages for supported locales
  • Register your rule with the ruleRegistry

There is a whole doc on the subject which can be found in the docs section.


Localization

There is a whole doc on the subject, but at a high level BY DEFAULT treacherous will pre-load the en-us locale for you which will be used by the library, but you can easily supplement that locale, or register and use new locales. You can also completely replace the default localization handler, but see the docs for more info on this.


Documentation

Just look in the docs folder for more documentation on certain scenarios or subject matters.

DOCS ARE HERE


Related Libraries

This library is the core treacherous framework, which purely handles the validation of models, however there are a few other libraries which build on top of this such as:


Developing

If you want to develop it further just clone the repo, npm install and gulp it should then provide you a working version to play with. If you want to minify it you can do gulp minify which will minify the output files, we don't minify by default.

You can also run gulp run-tests which will run the tests to make sure everythign works as expected.


Credits

"Mountains" Icon courtesy of The Noun Project, by Aleksandr Vector, under CC 3.0

treacherous's People

Contributors

gitter-badger avatar grofit avatar jsobell avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar

treacherous's Issues

addRule option parameters are no longer optional

.addRule("required")
is no longer a valid syntax, neither is
.addRule("required",()=>'You can't leave this blank!')
Given that a dev might pass in a function as an option, we can't identify whether a parameter is an option or a message, so I would suggest we have a .withMessage() fluent syntax to append to the most recent RuleLink.
I know this causes complications with arrays rule additions (which can presumably be addressed by storing a reference to the latest rule collection/rule added in a state-machine pattern), but it would make the syntax much cleaner.

Thoughts re handling of error tests

Some thoughts...
How to handle the fact that some fields should only be validated on blur, change, or whenever.

valtype (bitmapped field?): 0=full check, 1=propertycheck, 2=propertychanged
is passed in to any rule check based on the original reason for the validation process. It returns true if the validation is to be processed.

Default for any rule is:
applies = (valtype) => { return true; }

If the user calls validate, all returning true for applies(0) are checked
If the user calls validateProperty('X'), all returning true for applies(1) are checked
If the user (via a plugin, e.g. UI binding to model) modifies property 'X' in the model, all returning true for applies(2) are checked

But... left like this, if the user modifies property 'X' in the model through code, all with 2 are checked, so we will get the errors generated on the UI even if we're loading a record's JSON from the server.
One simple solution may be to have the concept of suspending validation (and resetting dirty checking when it is resumed).
Given that (in theory) the user should always be calling validate() anyway (which can't be suspended?), it's safe to accept that so long as the user always uses validate() before consuming the data, there won't be any issues.

A) Rule defined to apply always to any validation attempt:
applies = (valtype) => { return true; };

B) Rule defined to only apply on a full validation:
applies = (valtype) => { return (!valtype); };

C) Rule defined to only apply on a full validation or when the user requests validation of this field by name: (is this likely to be a scenario?)
applies = (valtype) => { return !valtype || (valtype & 1); };

D) Rule defined to only apply on a full validation or when the property has changed through code or binding to the UI:
applies = (valtype) => { return !valtype || (valtype & 2); };

In addition, because applies is a function, the dev can create a rule to only perform validation under chosen situations

To assign onBlur, bindings will update the model property, so the rule will only apply when the user changes data.
If the validation is to apply on onBlur even if data is unchanged, the handler for the input field must call validateProperty('X') to force evaluation of that RuleLink without going through the property watcher. By default this will only fire the validation if the link is set to A or C.

Double nesting addRulesetForEach

Is it possible to (double) rules-sets?

The order rules-set works properly, but i can't seem get the (nested) product rule-set to work.

 // product validation rules
 this.productRuleSet = createRuleset()
                               .forProperty("deliveryDate")
                                   .addRule("required")
                               .build();

 // order validation rules
 this.orderRuleSet = createRuleset()
                               .forProperty("products")
                                   .addRulesetForEach(this.productRuleSet)
                               .build();

 // invoice validation rules
 this.invoiceRuleSet = createRuleset()
                                  .forProperty("orders")
                                    .addRulesetForEach(this.orderRuleSet)
                                  .build();      

 this.invoiceValGroup = createGroup(this.invoice, this.invoiceRuleSet);

Multilingual support

I require multilingual support for my application.
I'm using Aurelia, I prefer your validation library to the official one, the API is very similar but i prefer your approach for unit testing.

I like the .withMessageKey() configuration method in the Aurelia-Validation library, did you have any thoughts on how it should work inside Treacherous?

I can submit a PR.

Support for dependent property validation

The use case is a registration form where a password and a confirmation are entered. When the password is changed the confirmation should also be validated to match the new password value

Base regex rule style for improved flexibility

Something like this makes it all much easier:

import * as Promise from "bluebird";
import {IValidationRule} from "treacherous";

export class SuperRegexValidationRule implements IValidationRule
{
  constructor(ruleName:string, expression:string, message:string|((value)=>string) ) {
    this.ruleName = ruleName;
    this.expression = expression
    this.message = (typeof message === "function") ? message : (v)=> { return (<string>message); };
  }

  public ruleName = "?";
  public message:((value)=>string);
  public expression = "?";


  public validate(value, regexPattern: RegExp): Promise<boolean>
  {
    if (value === undefined || value === null || value.length == 0)
    { return Promise.resolve(true); }

    var matchesPattern = value.toString().match(this.expression) !== null;
    return Promise.resolve(matchesPattern);
  }

  public getMessage(value, regexPattern) {
    return this.message(value);
  }
}

Called using
ruleRegistry.registerRule(new SuperRegexValidationRule('launchURL',url,value => '${value} is an Invalid launch URL'));
(needs back-ticks in the string parameter - can't escape them in Git editor)
or
ruleRegistry.registerRule(new SuperRegexValidationRule('launchURL',url,'Invalid URL'));

Refactor out dependency on bluebird

Currently promises are heavily used but there are only one or two places where Bluebird specific logic is used and in most cases that could be refactored out to just use any available Promise object.

There are tests in place so as long as all the tests pass and there is no longer a hard dependency on bluebird then it should all be ok, although as part of this there needs to be some way for typescript to know a Promise exists without it having a hard dependency on any one library.

Add support for hot swapping model being validated

It is a common scenario on front end frameworks to get data from an API or some other end point and replace the current instance of data with the instance passed back from the API. You can mitigate some of this by merging data into your existing object however for some this may not be liked.

So a new feature should be added to allow a validation group to replace the current model instance being watched, this will however require the same schema as the existing model, and should be seen as a niche use case and not a common scenario if can be avoided.

Given I have a configured validation group for model A
When I provide a model B to the validation group matching the existing schema
Then the validation group should be validating against model B
And the validation group should not be validating against model A

Custom messages for validation rules

The addRule would benefit from an optional parameter accepting either a string or a function to resolve the error message displayed if validation fails, or perhaps another fluent function.
This is particularly important for regex rules, and the functionality could double as part of the i18n.
How about
.forProperty("uRLAlias").addRule("regex", url).message("Enter a web address, you plonker")
or
.forProperty("uRLAlias").addRule("regex", url).message(resolveMsg)
?
Passing any function or a string the current value and/or rule options object might be helpful too.

Passing a string function to addRule raises an exception

.addRule("required", null, () => 'Anonymous??? ')
generates the following error in the console:

(20,19): error TS2346: Supplied parameters do not match any signature of call target.

The function still generates the message correctly, so I'm not sure where the error is generated.

modelStateChangedEvent is triggered incorrectly

The modelStateChangedEvent is triggered with isValid==true when not all properties are valid
Gist - start entering one field and isValid immediately switches to true. If you then clear that field it stays true.

Validation on group construction

Validation happens immediately in validation group constructor. Having UI validation in mind, it would be better to not validate straightaway so that just loaded empty form does not have error messages

Watcher only watches one model

When I setup two validation groups on two separate models only the changes on the second model trigger the validation event. This happens because the validation group is created with the watcher singleton. The test code

        let ruleSet1 = createRuleset().forProperty("name1").addRule("required").build();
        let ruleSet2 = createRuleset().forProperty("name2").addRule("required").build();
        let model1 = { name1: "name1" };
        let model2 = { name2: "name2" };
        let vg1 = createGroup(model1, ruleSet1);
        vg1.propertyStateChangedEvent.subscribe((...args) => { console.log("vg1", args); }, () => { return true; });
        let vg2 = createGroup(model2, ruleSet2);
        vg2.propertyStateChangedEvent.subscribe((...args) => { console.log("vg2", args) }, () => { return true; });

        model1.name1 = ""; // no log message here
        model2.name2 = ""; 

Ensuring validation rules, messages, and appliesIf can access data properly

OK, so something to consider here. Why would anyone ever want access to the model? Presumably only ever to access other properties.
So why do we resolve the property because we call the rule? To make it simpler to implement a rule because value is the thing to check (and it seems obvious at first glance).
So what happens if a rule needs to access another property. Well, firstly we have to tell the rule the name of the property, so we have to use the options. The rule either has to bodge a check of mode[optionsOrValue] or do it properly by instantiating a new PropertyResolver.
OK, so if we need this data when calling validate(), we also need it for withMessage and appliesIf, because we have no idea what they might want to access.

So if we have to hack in options and PRs, why not pass in the PR and the name of the property instead of value? Is there ever a situation where value is not the contents of a property?
I suppose it's a choice between model and PR. My question is, if Rules only ever need access to properties by name, why would be pass the model object itself?
Of course this means that as it stands, to use the PR you need to have both the model and the property string to resolve. Is it better to pass in a ModelResolver which includes the model reference?

We keep coming back to the old FieldsMatchRule example

    public validate(mr:ModelResolver, prop:string, optionsOrValue): Promise<boolean>
    {
        var result;
        var comparison = mr.get(optionsOrValue);
        var weakEquality = optionsOrValue.weakEquality || false;

        if(TypeHelper.isDateType(comparison))
        { result = ComparerHelper.dateTimeCompararer(mr.get(prop), comparison); }
        else
        { result = ComparerHelper.simpleTypeComparer(mr.get(prop), comparison, weakEquality); }

        return Promise.resolve(result);
    }

    public getMessage(value, optionsOrValue) {
        return `This field is ${mr.get(prop)} but should be equal to ${mr.get(optionsOrValue)}`;
    }

The only place this might not work is if the model contains functions, but to be honest I think that's a seriously edge-case, and there's nothing to stop us having mr.model available to cover that.

Creating a ValidationGroup against an undefined object throws errors

If the user does not have a model assigned at the time of setting rules, an exception is thrown.
A workaround is to define var user = {}, but it would be convenient if the validation was skipped (and marked as invalid?) if a model was not present.
This is particularly applicable in Typescript, where initial values must either be set to a new instance of the object or cast to the appropriate type:
private user:UserDTO = new UserDTO(); or private user:UserDTO = <UserDTO>{};
Not difficult, but unintuitive and a bit messy.

Wallaby timeouts... All promise returning calls require catch(done)

The reason errors in Wallaby fail with 2000ms timeouts is because there are no catches on the promises in the tests.
Add .catch(done) to the end of each then() in the tests causes any error to correctly logged.
e.g.

        validationGroup.isValid()
            .then(function(isValid){
                expect(isValid).to.be.true;
                validationGroup.release();
                done();
            }).catch(done);

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.