1.0.5 • Published 6 years ago

class-propper v1.0.5

Weekly downloads
3
License
MIT
Repository
github
Last release
6 years ago

This is a utility for creating class properties with optional validation, type criteria, regex filters, etc. It is middleware for Object.createProperty on an es6 Class prototype.

field validation in OOP is a bit tedious; you have to define rules for a field, define an overall validator for the instance and collect errors from fields. The code to do this varies very little and there's no reason to keep pounding it out so I collected it here in a series of meta-methods.

Defining Properties onto classes

The basic workflow is this:

  1. Define a class

Your class (es6) can have any sort of constructor, properties, methods, etc.

class UserRecord {
  constructor(props) {
    for (let prop in props) {
      this[prop] = props[prop];
    }
  }
  
  get name() {
   return this._name;
  }
  
  set name(value) {
    this._name = value;
  }
}

You could go on and stamp all sorts of fields on this class... address, phone, fax, email.... but the boilerplate gets huge without a whole lot of useful value.

instead we do this:

const userPropper = propper(UserRecord);

userPropper.addString('address')
.addString('phone', {required: true})
.addString('email', {regex: /^(.*@.*\.[\w]+$/, regexMessage: '#name# must be a proper email value'})
.addProp('age', {filter: 'integer'})
.addProp('birthday', {filter: 'date'});

Now, instances of UserRecord will have formal field definitions and criteria:

let user = new UserRecord({name: 'Bob', phone: '111-222-3333', 
email: 'bob@gmail.com', birthday: new Date(1966, 11,2)});

And if you try to set bad data to the user record it will choke:

try {
  user.birthday = 11;
} catch (err) {
  console.log('error: ', err);
}

The amount of boilerplate this requires in long-form JS is many times this with no real added value (and room for errors.)

addProp(propName, options = {}) and addString(propName, options={})

addProp adds a property to the prototype of the class you are wrapping. The value of options will be used in the Object.defineProperty(name, options) call except for the Propper-specific values:

These properties define the validation requirement of the field, and how invalid data is handled: They are all optional.

  • failsWhen see validation, below
  • errorMessage
  • onBadData (function) triggered when invalid data is assigned to a field. if absent, throws
  • required (boolean)

These do other things:

  • defaultValue
  • localName (string) name of the local prop the data is stored in; defaults to '_' + name

addString(name, options) is the same as addProp but adds a string validator as well as a few more optional validations specific to strings. These are used in addString a variant of addProp

  • regex (regex)
  • regexErrorMessage (string)
  • min (number) a length criteria for the value
  • max (number) " "

These methods are chainable.

A note on default values

There is no validation done on default values. The assumption here is that it is the class designer's responsibility to either (a) set a default that is valid or (b) not actually care about the validity of the initial value until it is set.

Validation

Validation is at the core of this library. Each property that has tests is assigned a validator instance. Validators have three properties:

  • type: a string (name of is method) that enforces a type check for the field. ('string', 'integer', 'array'...)
  • failsWhen: a function, validator OR an array of same.
  • defaultError: a string that is emitted when the failsWhen succeeds
  • errors (optional): an optional hash of responses to specific emissions from failsWhen

Eventually you'll want to execute multiple tests on the same property. There are two ways to do this:

  1. Create a failsWhen that has multiple tests inside it and emits keys that have analogs in the errors property
  2. create a validator whose failsWhen is an array of single validators.

Example:

Say you want a property to be a date but one that is not in the future.

You could do this in two ways as mentoned above First, the compound validator

let v = new Validator((d) => {
  if (!(d instanceof Date)) { return 'nondate'}
  if (d.getTime() > Date.now())  { return 'future'}
},
'bad #name#',
{nodate: 'value must be a javascript date', future: 'value must be in the past'})

Or through compounding:

let v = new Validator([
    new Validator('date', 'value must be a javascript date'),
    new Validator((d) => d.getTime() > Date.now(), 'value must be in the past')
])

// exactly equal to 

let v = Validator.compound(
    new Validator('date', 'value must be a javascript date'),
    new Validator((d) => d.getTime() > Date.now(), 'value must be in the past'));

the effect will be the same from a "black box" point ov view.

A few things to note:

  • instead of a function you can put a key that evaluates to a method of the is node module.
  • the tests are run sequentially, so in a second compound test you can assume that the first one has not been triggered
  • There are a few tokens you can use to emboss your error messages:
    • #name# represents the name of the field. Since this is only known externally to the validator itself, it won't be replaced inside the Validator itself.
    • #value# represents the failing value.

Validators in practice

addProp's options parameter has two properties for setting the validation criteria of a property: failsWhen and errorMessage.

  • if failsWhen is a validator, errorMessage is ignored.
  • if it is a function or an array (of functions or validators) a validator is created using failsWhen and errorMessage as the arguments to the new Validator.

"Magic" props with implicit validation

An experimental variant of propper is EasyPropper. It uses Proxy which is not avialable on every platform so use with caution. What it does do is let you define tests "Magically".

const {easyPropper} = require('propper');

class BaseClass {
  
}

const bcPropper = easyPropper(BaseClass)
.addDate('created')
.addString('name', {required: true})
.addInteger('age');

let instance = new BaseClass();

instance.created = new Date();
instance.age = 'one'; // fails because not an integer.

these methods are "Magic" -- for those that hate magic, you can skip this. For those that do, the addname has an analog to the properties of the is module.

for instance, the above is exactly equal to

const {propper} = require('propper');

class BaseClass {
  
}

const bcPropper = propper(BaseClass)
.addProp('created', {type: 'date'})
.addProp('name', {type: 'string', required: true})
.addProp('age', {type: 'integer'});

let instance = new BaseClass();

instance.created = new Date();
instance.age = 'one'; // fails because not an integer.

or ...

const {propper} = require('propper');

class BaseClass {
  
}

const bcPropper = propper(BaseClass)
.addProp('created', {failsWhen: (value) => !is.date(value)})
.addProp('name', {failsWhen: (value) => !is.string(value), required: true})
.addProp('age', {failsWhen: (value) => !is.integer(value)});

let instance = new BaseClass();

instance.created = new Date();
instance.age = 'one'; // fails because not an integer.

... but its quicker and more semantic. You can add any options you want to "magic" methods, even further validators, which will execute after the magic validator implicit in the method.

Reflection: isValid and propErrors

You can also poll the condition of the class as a whole and get errors just as you can with Ruby activeRecord instances.

when you prepare your propper with propper(BaseClass).addIsValid(), it adds two methods, propErrors and isValid. They are properties, not methods/functions.

isValid

isValid returns true if every field that has validation criteria's current values are good. if one or more of them aren't, it returns false.

propErrors

PropErrors is an array of {prop: [name of field: string], error: [error message: string]} objects that tell you which specific fields are bad (and why). If none are, it returns null.

Note - addIsValid() must be called BEFORE addProps or you will lose track of some of the validators.

Preloading Validator and onBadData

If you have a series of properties with identical validators you can set them at the Propper level (ha!); note, you have to clear them at the end or they carry through.

class UserRecord {}
const userPropper = propper(UserRecord);

userPropper
.withValidator(new Validator((n) => (Number.isNumber(n) && n >= 0).reverseTest()))
.addInteger('age')
.addInteger('children')
.addInteger('income')
.clearValidator()
addString('address')
.addString('phone', {required: true})
.addString('email', {regex: /^(.*@.*\.[\w]+$/,
 regexMessage: '#name# must be a proper email value'})
.addProp('age', {filter: 'integer'})
.addProp('birthday', {filter: 'date'});

Handling validation errors

There may be reasons not to throw an error; if you want to handle bad data in a custom way, add an onBadData method; it will receive the name of the field, the value attempted, and the error message. The field value will not be changed, unless your custom onBadData returns true. In the absence of a custom onBadData hook, any attempt to set a field to a bad value (validation/type failure) will throw an error.

If you don't throw on validation errors you will probably want to use addIsValid() to get the status of an instance.

Dependencies

This class depends on the is module for tests. You don't need to use the is methods for your validators - you can always write your own failsWhen functions longhand.

1.0.5

6 years ago

1.0.4

6 years ago

1.0.3

6 years ago

1.0.2

6 years ago

1.0.1

6 years ago

1.0.0

6 years ago