5.1.0 • Published 2 years ago

teishi v5.1.0

Weekly downloads
44
License
-
Repository
github
Last release
2 years ago

teishi

"A string is a string is a string." --Gertrude Stein

teishi is a tool for validating the input of functions.

teishi means "stop" in Japanese. The inspiration for the library comes from the concept of "auto-activation", which is one of the two main pillars of the Toyota Production System (according to its creator, Taiichi Ohno).

Current status of the project

The current version of teishi, v5.1.0, is considered to be stable and complete. Suggestions and patches are welcome. Besides bug fixes, there are no future changes planned.

teishi is part of the ustack, a set of libraries to build web applications which aims to be fully understandable by those who use it.

Usage examples

Validate the input of a function that receives two arguments, an integer (counter) and a function (callback). The second argument is optional.

function example1 (counter, callback) {
   if (teishi.stop ('example1', [
      ['counter', counter, 'integer'],
      ['callback', callback, ['function', 'undefined'], 'oneOf']
   ])) return false;

   // If we are here, the tests passed and we can trust the input.

Validate the input of a function that receives two arguments, a string (action) which can have four possible values ('create', 'read', 'update' and 'delete'), and an integer between 0 and 100 (limit).

function example2 (action, limit) {
   if (teishi.stop ('example2', [
      ['action', action, ['create', 'read', 'update', 'delete'], 'oneOf', teishi.test.equal],
      ['limit', limit, 'integer'],
      [['limit', 'page size'], limit, {min: 0, max: 100}, teishi.test.range]
   ])) return false;

   // If we are here, the tests passed and we can trust the input.

Validate the input of a function that receives an object with two keys, action and limit. The applicable rules are the same than those in function example2 above.

function example3 (input) {
   if (teishi.stop ('example3', [
      ['input', input, 'object'],
      ['keys of input', dale.keys (input), ['action', 'limit'], 'eachOf', teishi.test.equal],
      function () {return [
         ['input.action', input.action, ['create', 'read', 'update', 'delete'], 'oneOf', teishi.test.equal],
         ['input.limit', input.limit, 'integer'],
         [['input.limit', 'page size'], input.limit, {min: 0, max: 100}, teishi.test.range]
      ]}
   ])) return false;

   // If we are here, the tests passed and we can trust the input.

Auto-activation

Auto-activation means that a machine or process stops immediately when an error is found, instead of going on until the faults in the process make it break down completely. Let's restate this: an auto-activated machine stops on its own when it detects an error.

The purpose of auto-activation is twofold:

  • No defective products are made, because every machine involved in the process checks the state of the product and will stop the whole process if any abnormality is found.
  • By stopping the process on any error, the system sharply distinguishes normal from abnormal operation. The process cannot start again until that error is solved. Thus, errors are not ignored or dismissed, but rather brought into the light so that its root causes can be determined and eliminated.

Auto-activation in functional programming

This idea can fruitfully be applied to code. More specifically, here's how I think it can be applied to a (mostly) functional style of programming:

  • Every function is a machine. The "product" or "throughput" is the data flow that enters the function and then exits it.
  • Every function checks its input according to a set of rules.
  • If the function deems its input valid, it has the responsability to return valid output.
  • If the function deems its input invalid, it must do three things:
    • Notify the user of the error, in the most precise terms possible.
    • Stop its own execution.
    • Return false.

I propose returning false instead of throwing an exception for the following reasons:

  • An uncaught exception stops the whole program. If we want every error to stop the entire program, this approach is the most direct.
  • However, if we want our code to deal with an error on its own (and allow it to take correcting measures), each call to a function that might fail must be wrapped in a try/catch block. This is extremely cumbersome.
  • In contrast, using false to indicate invalid input is comparatively elegant and conforms to the structured data flow of a functional program.
  • In practice, the overloading of a valid return value (false) to indicate an exception has not exerted either restriction or confusion in other libraries currently based on teishi.

Let's summarize the advantages of auto-activation, applied to code:

  • Cleaner exception handling: functions can return a false value on receiving a false input, so you can check for a false value instead of placing a try/catch block.
  • Exception-less code: if your program is correct, your program will never throw an exception or crash, no matter how wrong its input is. Although this doesn't reduce at all the difficulty of writing correct code, it at least offers the guarantee that no malformed input can break a properly written program.
  • Errors can't run far: in a non-auto-activated program, an invalid input can go through many functions before making one of them crash and burn. Hence, the source of an error can be far from the exception that it provokes later. If every function checks its input, the detection of the error will be much closer to its source.
  • Meaningful error messages: by specifying the difference between expected and actual input, error messages can convey more information. argument [1] to myFunction must be a function explains much more than undefined is not a function. If you forgot to pass a function as a second argument to another function, it is easier to just be told that you missed it, instead of debugging the first (failed) usage of that argument.
  • Raise your own bar: most importantly, you are forced encouraged to clearly specify the inputs of each function. This leads to clearer code and fewer bugs.

Auto-activation boilerplate

When I started happily applying this concept to my code (as you can see in old versions of lith), I found myself writing this kind of code block, over and over.

if (type (input) !== 'array' && type (input) !== 'undefined') {
   console.log ('Input to myFunction must be either an array or undefined, but instead is', input);
   return false;
}

if (type (input) === 'array') {
   if (input.length !== 3) {
      console.log ('Input to myFunction must be an array of length 3, but instead has length', input.length, 'and is', JSON.stringify (input));
      return false;
   }
   for (var item in input) {
      if (type (input [item]) !== 'string') {
         console.log ('Each item of the input to myFunction must be a string, but instead is', input [item]);
         return false;
      }
   }
}

These code blocks have three parts in common:

  1. Error detection.
  2. Error notification.
  3. Return immediately from the function, so that no further execution is performed and a false value is returned.

The repetitive parts of the three actions above are:

  1. Multiple comparisons:
    • When you have a single input and many accepting values (as in the first example).
    • When you have to iterate the input to see if it matches an accepting value (as in the second example).
    • When you have to do both at the same time.
  2. Error notification:
    • Writing the error message every time, such as "Input can be of type array or object but instead is".
    • Having to stringify objects and arrays when printing them for error notification purposes.
  3. Returning early from the function requires to write one return clause per possible validation error.

teishi simplifies the first two parts and allows you to return false just once, at the end of all the checks you want to perform. Using teishi, the example above can be rewritten like this:

if (teishi.stop ('myFunction', [
   ['input', input, ['array', 'undefined'], 'oneOf'],
   [teishi.type (input) === 'array', [
      function () {
         return ['input.length', input.length, 3, teishi.test.equal]
      },
      ['items of input', input, 'string', 'each']
   ]]
])) return false;

Auto-activated validations using teishi are 50-75% smaller (counting either lines or tokens) than the boilerplate they replace.

More importantly, teishi allows you to express rules succintly and regularly, which makes rules easier to both read and write. Its core purpose is to facilitate as much as possible the exacting task of defining precisely the input of your functions.

Installation

teishi depends on dale

teishi is written in Javascript. You can use it in the browser by sourcing dale and the main file:

<script src="dale.js"></script>
<script src="teishi.js"></script>

Or you can use these links to the latest version - courtesy of jsDelivr.

<script src="https://cdn.jsdelivr.net/gh/fpereiro/dale@3199cebc19ec639abf242fd8788481b65c7dc3a3/dale.js"></script>
<script src="https://cdn.jsdelivr.net/gh/fpereiro/teishi@31a9cf552dbaee79fb1c2b7d12c6fad20f987983/teishi.js"></script>

And you also can use it in node.js. To install: npm install teishi

teishi should work in any version of node.js (tested in v0.8.0 and above). Browser compatibility has been tested in the following browsers:

  • Google Chrome 15 and above.
  • Mozilla Firefox 3 and above.
  • Safari 4 and above.
  • Internet Explorer 6 and above.
  • Microsoft Edge 14 and above.
  • Opera 10.6 and above.
  • Yandex 14.12 and above.

The author wishes to thank Browserstack for providing tools to test cross-browser compatibility.

Simple rules

The fundamental pattern of teishi is to compare a variable to an expected value and see if the variable conforms or not to our expectations. Each of these expectations conforms a rule. Within a rule:

  • We will call the variable compare, and call the expected value to.
  • We will also need a name to describe what compare stands for.

The most basic teishi rules have this form: [name, compare, to].

The simplest rule

Take this rule from example1 above:

['counter', counter, 'integer']

This rule enforces that counter will be of type integer. If this rule is not fulfulled, teishi will return the following error message: counter should have as type "integer" but instead has value VALUE and type TYPE. (with VALUE and TYPE being the value and the type of counter, respectively).

Things to notice here:

  • The whole rule is an array with three elements.
  • The name field is 'counter'.
  • The compare field is whatever the value of counter is.
  • The to field is 'integer'.

The multi operator

Let's take a slightly more complex rule, also from example1:

['callback', callback, ['function', 'undefined'], 'oneOf']

This rule enforces that callback will be either of type function or undefined.

Things to notice here:

  • The to field is an array with two strings, function and undefined. This is because we have two possible to values we accept.
  • We add a fourth element to the rule, the string 'oneOf'. This addition ensures that compare should conform to one of the to values.

'oneOf' is an instance of the multi operator, which allows you to do one-to-many comparisons ('oneOf'), many-to-one comparisons ('each') and many-to-many comparisons ('eachOf'). These are the three possible values for the multi operator:

  • 'oneOf': when you have a single compare and two or more accepting values, like in the example we just saw.
  • 'each': when you have many compare values and a single accepting value. For example, if we want to check that strings is an array of strings, we write the rule: ['strings', strings, 'string', 'each']
  • 'eachOf': this variant combines both 'each' and 'oneOf', and it compares multiple compare values to multiple to values. For example, if we want to check that input is an array made of strings or integers, we write the rule: ['input', input, ['string', 'integer'], 'eachOf'].

If no multi operator is present, the rule just compares a single compare value to a single to value.

The test operator

Notice that so far, we've only checked the type of compare. Let's see how we can check, for example, its actual value.

Let's take this rule from example2 above:

['action', action, ['create', 'read', 'update', 'delete'], 'oneOf', teishi.test.equal]

In this rule, we state that action should be either 'create', 'read', 'update' or 'delete'.

Things to notice here:

  • As in the previous example, to is an array of values.
  • Also as in the previous example, the multi operator is 'oneOf', since we want compare to be equal to one of 'create', 'read', 'update' and 'delete'.
  • In contrast to the previous example, we set the test function to teishi.test.equal, which is one of the five test functions bundled with teishi.

In any rule, you can add a multi operator, a test function, or both of them, in any order you prefer, as long as they are after the three mandatory elements of the rule (name, compare and to).

Test functions

teishi comes bundled with five test functions.

teishi.test.type

teishi.test.type is the default test function (in these days of dynamic typing, it is remarkable how much we want our inputs to conform to certain types). This function will check the type of any input and return a string with one of the following values:

  • 'integer'
  • 'float'
  • 'nan'
  • 'infinity'
  • 'object'
  • 'array'
  • 'regex'
  • 'date'
  • 'null'
  • 'function'
  • 'undefined'
  • 'string'

Type detection is different to the native typeof operator in two ways:

  • We distinguish between object, array, regex, date and null (all of which return object using typeof).
  • We distinguish between types of numbers: nan, infinity, integer and float (all of which return number using typeof).

Please remember that you cannot use 'number' as a type since teishi requires more specificity. Instead use one of 'integer', 'float', 'nan' or 'infinity'.

teishi.test.equal

teishi.test.equal checks that two elements are equal, using the === operator.

In javascript, when you compare complex objects (arrays and objects), the language does not compare that the value of the two objects is the same, but rather that both objects are the same object.

What this means is that if you compare (for example) two arrays with the same values, you will get a false value.

[1, 2, 3] === [1, 2, 3] // returns false

Whereas if you do:

var array = [1, 2, 3];
array === array // returns true

In practice, most often you will want to compare whether the values of two arrays or objects are equal.

As a result, teishi.test.equal will compare the values of two objects or arrays and will return true if their values are equal (and false if they are not). This kind of comparison is often named deep equality.

['input', [1, 2, 3], [1, 2, 3], teishi.test.equal] // this will return true

Historical note: in a previous version of teishi, teishi.test.equal was the default test function, until after dutifully writing teishi.test.type in my teishi rules a few hundred times I realized that type checking was 5-10 times more prevalent than equality checks.

teishi.test.notEqual

teishi.test.notEqual is just like teishi.test.equal, but will return true if two things are different (and false otherwise).

One very important caveat: if you use the oneOf or eachOf operator with this test function, you will probably not get the results you want. Take the following example:

  ['not a stooge', name, ['moe', 'larry', 'curly'], 'oneOf', teishi.test.notEqual] // this will return true even if name is `moe`, `larry` or `curly`.

Even if name is 'moe', this will still be true because 'moe' is not equal to 'larry' or 'curly'. To achieve what you want, you can rewrite this rule as follows:

  ['not a stooge', ['moe', 'larry', 'curly'], name, 'each', teishi.test.notEqual] // this won't let `moe` through

However, this is but a workaround, since the error message will look a bit strange. Because how multi operators work, negations against a set cannot be expressed directly. If you're experiencing a problem with this, please open an issue.

teishi.test.range

teishi.test.range checks that compare is in a certain range. This function is useful for testing the range of numbers.

We've already used this function in example2 above:

[['limit', 'page size'], limit, {min: 0, max: 100}, teishi.test.range]

Here, we ensure that limit can be 0, 100 or any number in between.

min and max allow the compare value to be equal to them. In mathematical terms, they determine a closed line segment.

If we want limit to be between 0 and 100, but we don't want it to be 0 or 100, we write:

[['limit', 'page size'], limit, {more: 0, less: 100}, teishi.test.range]

more and less don't allow the compare value to be equal to them. In mathematical terms, they determine an open line segment.

If you are using teishi.test.range, a valid to value is an object with one or more of the following keys: min, max, less, more.

Notice that you can mix open and closed operators. For example:

[['limit', 'page size'], limit, {min: 0, less: 100}, teishi.test.range]

This rule will allow limit to be 0 but it won't allow it to be 100.

teishi.test.match

teishi.test.match checks that compare is a string that matches the regex specified in the to field.

For example, imagine we want a certain identifier to be a string of at least one character composed only of letters and numbers. We can determine this by using the following rule:

[['identifier', 'alphanumeric string'], identifier, /^[0-9a-zA-Z]+$/, teishi.test.match]

If compare is not a string and to is not a regex, a proper error message will be displayed.

Writing your own test functions

Although the five functions above will take you surprisingly far, you may need to write your own test functions. While this is certainly possible and encouraged, it is an advanced topic that deserves its own section.

Two names instead of one

Notice this rule, which we've already seen before:

[['limit', 'page size'], limit, {min: 0, max: 100}, teishi.test.range]

Here, name (the first element of the rule) is not a string, but rather an array with two strings. The purpose of the second string is to provide a verbal description of the to field.

In this case, if limit was out of range, you would get the following error message:

limit should be in range {min: 0, max: 100} (page size) but instead is LIMIT, where LIMIT is the actual value of limit.

Why didn't we do this for every rule? In practice, the to field is usually self-explanatory. When this is not the case, use two names instead of one. The second name will give additional information about the to value.

From now on we will refer to name as names.

All you need to know about multi

There are a few things we haven't explained about the multi operator.

Let's first state a few definitions:

  • simple value: anything that is neither an array, nor an object, nor undefined
  • complex value: an array or an object
  • empty value: either undefined or an empty complex array or object

What happens if compare or to are objects instead of arrays?

If you use an object instead of an array and multi goes through each of its elements, teishi will ignore the keys of the object and only take into account its values. For example, these two rules are equivalent:

['length of input', input.length, [1, 2, 3], 'oneOf', teishi.test.equal]
['length of input', input.length, {cant: 1, touch: 2, this: 3}, 'oneOf', teishi.test.equal]

What happens if compare is a simple value and you set multi to 'each' or 'eachOf'?

If multi is set to 'each' or 'eachOf', this is the same as setting compare to an array with a single element. For example, these two rules will be the same.

['input', 1, 'integer', 'each']
['input', [1], 'integer', 'each']

What happens if to is a simple value and you set multi to 'oneOf' or 'eachOf'?

Same than above, to will be treated as an array with one element. For example, these two rules are equivalent:

['input', input, 'integer', 'oneOf']
['input', input, ['integer'], 'oneOf']

What happens if compare is an empty value and you set multi to 'each' or 'eachOf'?

If compare is empty and multi is either 'each' or 'eachOf', teishi assumes that there are no values to compare, so there cannot be possibly a source of error. Hence, it will always return true. Here are examples of rules that will always return true:

['input', undefined, 'integer', 'each']
['input', [], 'integer', 'each']
['input', {}, 'integer', 'each']

What happens if to is an empty value and you set multi to 'oneOf' or 'eachOf'?

If to is empty and multi is either 'oneOf' or 'eachOf', teishi assumes that there are no values that compare can match, so there cannot be any possible way to pass the test. Hence, it will always return false, plus an error message. Here are examples of rules that will always return false:

['input', input, undefined, 'oneOf']
['input', input, [], 'oneOf']
['input', input, {}, 'oneOf']

Summary: teishi simple rules expressed as teishi rules

To sum up what a teishi simple rule is, let's express it in terms of teishi simple rules!

A teishi simple rule is an array.

['teishi simple rule', rule, 'array']

The rule can have three to five elements.

['length of teishi simple rule', rule.length, {min: 3, max: 5}, teishi.test.range],

The names of the rule must be either a string or an array.

['rule name', rule [0], ['string', 'array'], 'oneOf']

If names is an array, it must have length 2 and only contain strings.

['rule name', rule [0].length, 2, teishi.test.equal]
['rule name', rule [0], 'string', 'each']

compare and to can be anything, so we don't have to write any validation rules for them!

The fourth element of the rule can be either the multi operator or a test function. Also, it can be undefined.

['rule options', rule [3], ['string', 'function', 'undefined'], 'oneOf']

Same thing with the fifth element.

['rule options', rule [4], ['string', 'function', 'undefined'], 'oneOf']

If the fourth element of the rule is a string, it needs to be a valid multi operator.

['multi operator', rule [3], ['each', 'oneOf', 'eachOf'], 'oneOf', teishi.test.equal]

Same with the fifth element of the rule, in case it is a string.

['multi operator', rule [4], ['each', 'oneOf', 'eachOf'], 'oneOf', teishi.test.equal]

If both the fourth and the fifth element are defined, they have to be of different types (one being a string, the other a function).

[['type of multi operator', 'type of test function'], teishi.type (rule [3]), teishi.type (rule [4]), teishi.test.notEqual],

Complex rules

There are four kinds of complex rules in teishi. Let's take a look at them.

Nested rules

A nested teishi rule is an array containing teishi rules. If a, b and c are teishi rules, all of these are valid teishi rules:

[a, b, c]
[[a, b, c]]
[[a], b, c]

(any other concoction of arrays and a, b, c that you can imagine)

If you check example1 through example3 above, you will notice that the rule passed to the main teishi functions receive an array enclosing many rules. That enclosing array is a nested rule.

A nested rule is just a sequence of rules. When teishi finds a complex rule, it will first test the first rule inside it. If the rule is valid, it will then proceed to the next rule. Otherwise, it will return false and display the proper error message.

Boolean rules

If teishi finds a boolean (true or false) in place of a rule, it will interpret this result as either a valid or an invalid result. This is useful when you want to reuse your validation functions.

For example, imagine you have a function validateWidget that returns true if a widget is valid and false otherwise.

If you want to validate a widget as part of the validations on a certain function, you can write the following:

function validateSomething (widget, ...) {
   if (teishi.stop ([
      // a rule here...
      // another rule here...
      validateWidget (widget),
      // more rules here...
   ])) return false;

When validateSomething is invoked, validateWidget (widget) will be evaluated to either true or false, so that when teishi encounters the rule, it will be a simple boolean which can make it either stop or proceed.

Function guards

Imagine that you want to validate a certain array. You want array to be an array, and you want it to have a length of 3. So you write the following rules:

['array', array, 'array']
['array length', array.length, 3, teishi.test.equal]

However, if array is not an array, instead of getting a nice teishi error, you will get an exception! For example, if array is null, you will get an exception that says something like: TypeError: Cannot read property 'length' of null.

Because of how javascript works, as soon as teishi receives those two rules, javascript replaces array by null in the first rule and array by null.length in the second rule. Of course, this last replacement yields an exception, because null has no method length.

To prevent this, we want javascript to evaluate the elements of a rule only when teishi is about to use that rule. To do this, we wrap the potentially dangerous rules in a function. The function effectively guards the rules to be evaluated before we know it is safe to do so.

When teishi finds a function, it executes it and then considers the result as a rule. So, we can express these two rules as follows:

['array', array, 'array']
function () {
   return ['array length', array.length, 3, teishi.test.equal]
}

If array is null, when teishi is evaluating the first rule, it will find the type discrepancy, return false and report the problem. The second rule, wrapped in a function, will never be evaluated, and in this way no exceptions will be generated.

Now, how can we know which are potentially dangerous rules? All dangerous rules can potentially raise exceptions because of a mismatch between the expected and the actual type of the compare field. More specifically, exceptions can be raised when:

  • compare references an element of an array (for example input [0]) but the expected array is not of type array.
  • compare references a property of an object (for example input.limit) but the expected object is not of type object.
  • compare invokes a method that is supported on a certain type (for example, length, which is supported for strings and arrays) but then the expected element is not of the expected type and hence does not support the method.

Function guards make teishi rules more verbose and they are not easy to grasp at first. And, as you can notice from the examples above, they have to be employed very often.

On the flip side, you will quickly learn where to write them and also quickly you will learn to ignore them while reading a set of rules.

Conditional rules

Conditional rules allow you to enforce certain teishi rules only when a condition is met. Let's see an example:

[teishi.type (input) === 'array', [
   function () {
      return ['input.length', input.length, 3, teishi.test.equal]
   },
   ['items of input', input, 'string', 'each']
]]

teishi.type is a simple function that returns the type of a value. When teishi is invoked, the expression teishi.type (input) === 'array' will be replaced by true or false, depending on the type of input.

When teishi encounters a rule with the form [boolean, array], teishi will only use the rules contained in array only if boolean is true. If the boolean is false, teishi will skip the rules contained in array.

In the example above, if input is an array, teishi will execute the two rules contained in the array (one of them is contained in a function guard as well!). If input is of another type, these rules will be ignored.

Let's see another example:

[
   options.port !== undefined,
   ['options.port', options.port, {min: 1, max: 65536}, teishi.test.range]
]

As you can see, if options.port is not undefined, the rule that specifies that options.port should be between 1 and 65536 will be enforced. If options.port is undefined, the rule will be ignored.

To sum up, a conditional rule:

  • Is a nested rule.
  • Contains exactly two rules.
  • The first rule is something that evaluates to a boolean (usually a function call or a comparison).
  • The second rule is an array (which can be either a nested rule or a simple rule).

When you use boolean rules (because you are using another validation function as a teishi rule), you must be careful of conditional capture. Conditional capture happens when you want to express two teishi rules in succession, but teishi thinks that you are using a conditional.

Let's see an example of conditional capture:

[
   validateWidget (widget),
   ['sprocket', sprocket, 'object']
]

In your eyes, this is a nested rule containing two simple rules. However, since the nested rule is composed of two rules, the first a boolean and the second an array, teishi will interpret this a conditional. Which means that if validateWidget (widget) returns false, not only it won't return false, but it will also ignore the next rule! This is dangerous because if the first rule is not met, not only teishi won't stop, but it will also ignore the next rule.

Although validateWidget will print an error message, teishi will proceed as if no errors had been reported.

To avoid conditional capture, you need to bear in mind the following rule:

When writing a nested rule of length 2 where the first rule is a boolean, wrap the second rule in a function guard.

[
   validateWidget (widget),
   function () {return ['sprocket', sprocket, 'object']}
]

By doing this, teishi will interpret the rule as a normal nested rule.

This hack has the added benefit of further dignifying the other hack (function guards).

Summary: teishi rules expressed as teishi rules

Now that we know complex rules, we can write a teishi rule that validates teishi rules!

[
   ['teishi rule', rule, ['function', 'boolean', 'array'], 'oneOf'],
   [teishi.type (rule) === 'array', [
      function () {
         return [
            [<conditional which will be true if this is a simple rule>, [
               ['teishi simple rule', rule, 'array'],
               ['length of teishi simple rule', rule.length, {min: 3, max: 5}, teishi.test.range],
               ['rule name', rule [0], ['string', 'array'], 'oneOf'],
               [teishi.type (rule [0]) === 'array', [
                  function () {return ['rule name', rule [0].length, 2, teishi.test.equal]},
                  ['rule name', rule [0], 'string', 'each'],
               ]],
               ['rule options', rule [3], ['string', 'function', 'undefined'], 'oneOf'],
               ['rule options', rule [4], ['string', 'function', 'undefined'], 'oneOf'],
               [teishi.type (rule [3]) === 'string', ['multi operator', rule [3], ['each', 'oneOf', 'eachOf'], 'oneOf', teishi.test.equal]],
               [teishi.type (rule [4]) === 'string', ['multi operator', rule [4], ['each', 'oneOf', 'eachOf'], 'oneOf', teishi.test.equal]],
               [rule [3] !== undefined && rule [4] !== undefined, [
                  [['type of multi operator', 'type of test function'], teishi.type (rule [3]), teishi.type (rule [4]), teishi.test.notEqual],
               ]],
            ]]
         ]
      }
   ]]
]

And there it is! This teishi rule will verify that any teishi rule is indeed valid. As a matter of fact, we will use this rule in example.js to validate teishi rules. The only thing I haven't specified here is the intricate conditional which I use to distinguish a simple rule from a nested one.

I wish I could use this code in teishi proper, but we can't because we need to write teishi without teishi. Such are the demands of bootstrapping. To see how teishi actually validates its input, please refer to the annotated source below.

teishi main functions

teishi.v

teishi.v is a function that receives three arguments:

  • functionName, a string. This argument is optional.
  • rule, which is a teishi rule (simple or complex). This argument is required.
  • apres, an optional argument that can be set to either true or a function.
  • prod, an optional argument that can be set to true.

rule is a simple or complex teishi rule. We've already explained these in the previous two sections.

teishi.v will test that the rule (including any sub-rules nested within it) are enforced for the given input. If any rule returns false, of them fails, the function does two things:

  • Report an error.
  • Return false.

The canonical usage example of teishi.v is to create validation functions. For example:

function validateWidget (widget) {
   return teishi.v (['widget', widget, 'object']);
}

The function above will return true if widget is an object, and false otherwise.

The purpose of functionName is to provide the name of the calling function to the error messages, to locate errors more easily. For example:

function validateWidget (widget) {
   return teishi.v ('validateWidget', ['widget', widget, 'object']);
}

If validateWidget receives a widget that is not an object, teishi.v will print the following error:

widget passed to validateWidget should have as type object but instead is WIDGET with type WIDGETTYPE

If functionName hadn't been specified, the error message would be:

widget should have as type object but instead is WIDGET with type WIDGETTYPE

Let's explain apres. apres is a variable that determines what is to be done if teishi.v finds an error.

When apres is set to true, if teishi.v finds an error, instead of reporting it and returning false, it returns the error message itself. By doing this, you let the function calling teishi.v to capture the error message and decide if it should be printed or not to the user. Sometimes it is useful to use teishi's machinery to to find out whether a given condition is matched or not, without having to report an error. I had to use this functionality in lith, when checking whether a given input was of a certain kind or of some other kind. If it belonged to neither kind, the error message would be reported, otherwise it wouldn't.

For certain situations, you might want to do some other thing with the error message. For example, if you are validating an HTTP request, you might want to write the error into the response object. This is where you can set apres to a function that receives the error as its sole argument:

function (request, response) {
   teishi.v (['id', response.body.id, 'integer'], function (error) {
      response.end (error);
   });
}

In the case above, the error will not be printed to the console, but rather it will be written to the response.

Finally, let's cover the prod parameter. When prod is set to true, it will turn off rule validation; in other words, the rules you pass to teishi.v will be assumed to be valid. This will increase teishi's performance in production settings, but should only be done when your code is thoroughly debugged.

If you want to turn off validations globally, you can directly set teishi.prod to true. This will be equivalent as passing a truthy prod parameter to every invocation of teishi.v.

teishi.stop

teishi.stop takes the same arguments as teishi.v.

The main difference between teishi.v and this function is that when teishi.stop finds an error, it returns true and when it finds no errors, it returns false. Let's state it again: teishi.stop returns FALSE if there were NO validation errors.

teishi.stop exists because of the following pattern:

if (teishi.stop ('myFunction', [
   // here be rules
   // and more rules
])) return false;

teishi already does two things for us: a) multiple comparisons; b) automatic error messages. The final thing we need to do to have properly auto-activated code is to return false when we find an error. If we were to do this with teishi.v, you would use the following pattern:

if (teishi.v ('myFunction', [
   // here be rules
   // and more rules
]) === false) return false;

Or this other pattern, which is shorter but error-prone:

if (! teishi.v ('myFunction', [
   // here be rules
   // and more rules
])) return false;

Thanks to teishi.stop, we can get rid of the === false or the ! in the examples above. There is no way, however, to get rid of the conditional wrapping the call to teishi.stop, nor a way of omitting the return false. The above pattern is the most succint auto-activation code you can get from teishi.

If apres set to true and a validation error is found, the error will be lost, since teishi.stop only returns true or false. This is useful for when you want to check the absence of a condition, but you don't consider this absence to be an error, just a result that will control the flow of your program. Here's an example.

If apres is set to a function and a validation error is found, you can still do something meaningful with the error. For example:

function (request, response) {
   if (teishi.stop (['id', response.body.id, 'integer'], function (error) {
      response.end (error);
   })) return false;
}

Helper functions

teishi relies on eleven helper functions which can also be helpful beyond the domain of error checking. You can use these functions directly in your code.

teishi.type

teishi.type takes an argument and returns a string indicating the value of that argument.

The purpose of teishi.type is to create an improved version of typeof. The improvements are two:

  • Distinguish between types of numbers: nan, infinity, integer and float (all of which return number in typeof).
  • Distinguish between array, date, null, regex and object (all of which return object in typeof).

The possible types of a value can be grouped into three:

  • Values which typeof detects appropriately: boolean, string, undefined, function.
  • Values which typeof considers number: nan, infinity, integer, float.
  • values which typeof considers object: array, date, null, regex and object.

If you pass true as a second argument, type will distinguish between plain objects (ie: object literals) and other objects. If you pass an object that belongs to a class, type will return the lowercased class name instead.

The clearest example of this is the arguments object:

type (arguments)        // returns 'object'
type (arguments, true)  // returns 'arguments'

teishi.str and teishi.parse

Two very useful javascript functions, JSON.stringify and JSON.parse, throw an exception if they receive an invalid input.

In keeping with the principle of exception-less code, teishi provides two wrappers to these functions:

  • teishi.str, which wraps JSON.stringify.
  • teishi.parse, which wraps JSON.parse.

If they receive invalid input, these two functions will return false instead of throwing an exception. If the input is valid, they will return the output of JSON.stringify and JSON.parse, respectively.

teishi.simple and teishi.complex

teishi.simple takes an input and returns true if it's a simple object (anything but an array or an object).

teishi.complex takes an input and returns true if it's a complex object (array or object).

teishi.inc

A function that takes an array or an arguments pseudo-array as its first element and a value as its second argument. If value is contained inside the array, the function returns true; otherwise, it returns false. This is the sole teishi function that doesn't validate its arguments - this is done to save execution time in libraries that depend on teishi.

teishi.copy

teishi.copy takes any input and returns a copy of it.

This function is useful when you want to pass an array or object to a function that will modify it and you want the array or object in question to remain modified outside of the scope of that function. javascript passes objects and arrays by reference, so in this case you need to copy the array or object to avoid side effects.

If input has any circular references, teishi.copy will replace them with a string with the form '[Circular]'.

If input is (or contains) an arguments pseudo-array, it will be copied into a standard array.

teishi.eq

teishi.eq (short for teishi.equal) takes two elements and returns true if they are equal and false otherwise. This function is mostly useful for comparing whether two arrays or objects contain the same things, despite being occupying distinct locations in memory. The rules for equality are as follows:

  • If both arguments are simple, the strict equality check is used (===).
  • If both arguments are complex, 1) their types must be the same (as per teishi.type); 2) their keys must be the same; and 3) the values for each of the keys must fulfill the same equality conditions, whether they are simple or complex.

teishi.last

teishi.last takes an array as its first argument and returns its last element. If you pass an argument that is not an array, an error will be printed through teishi.clog and the function will return false. This function can also receive an arguments pseudo-array.

You can pass an integer larger than 0 as the second argument to teishi.last. This will make teishi.last to retrieve the nth element from the end instead of the last one; for example, if you pass 2 as the second argument, teishi.last will return the next-to-last element of the passed array.

teishi.time

A function that returns the current date in milliseconds. If you pass a single argument to it, the date will be constructed with that argument instead of representing the current date.

teishi.clog

teishi.clog serves the noble purpose of printing output to the console. All teishi functions print error messages through this function.

Why use teishi.clog instead of console.log?

  • Output comes in pretty colors, thanks to cutting edge 1980s technology.
  • Complex values (arrays and objects) are expanded, so you can print nested objects without having to stringify them.
  • It prints a timestamp that can be helpful when profiling or debugging code.
  • Defaults to alert for very old browsers that don't support console.log.

teishi.clog takes one or more arguments, of any type. If the first argument is a string, and there's more than one argument passed to teishi.clog, the first argument will be treated as a label, which is just some text with a different background color, followed by a colon (:).

It is important to notice that colorized output will only be present in node.js, since there's no standard way of giving format to the javascript console in browsers.

If you want to send the output of teishi.clog to a logfile, the color codes will bother you. In this case, invoke once teishi.lno (short for log with no colors), which will turn off all colorized output for any subsequent invocation to teishi.clog.

Custom test functions

What if the five test functions provided with teishi are not enough? Well, you can write your own custom test functions! Earlier I mentioned that this is an advanced topic. However, if you've made it through the readme, you are ready to do this.

To dispel your fears, here's the code of teishi.test.type, the most useful test function in teishi:

teishi.test.type = teishi.makeTest (
   function (a, b) {return teishi.type (a) === b},
   ['should have as type', ['with type', teishi.type]]
);

To create a test function, you need to invoke the function teishi.makeTest. This function takes two arguments:

  • fun, a function that takes two arguments and returns true or false. These two arguments will be, as you might imagine, the compare and the to of each rule.
  • clauses, which can be one of the following:
    • a string (shouldClause).
    • an array containing a shouldClause.
    • an array containing a shouldClause and a finalClause.

shouldClause is required, but finalClause is optional. In fact, of all five teishi test functions, only teishi.test.type uses a finalClause.

The shouldClause is the string that teishi will use to specify what kind of validation error was encountered. For example, the shouldClause of teishi.test.type is 'should have as type'. In the error message below, the shouldClause is responsible for the bolded text:

input should have as type integer but instead is INPUT with type INPUTTYPE

The finalClause is responsible for the final part of the error message. It is displayed at the end of the error, after the compare value. For example, in teishi.test.type, the finalClause is ['with type', teishi.type]. In the error message below, the finalClause is responsible for the bolded text:

input should have as type integer but instead is INPUT with type INPUTTYPE

The finalClause can be any of the following:

  • undefined
  • a string or function
  • an array containing one or more strings/functions

When you place a function in the finalClause, that function will be evaluated with compare and to as its arguments. This is why we put teishi.type in the finalClause, so that teishi.type will receive compare as argument and return its type.

Earlier I said that fun can return either true or false, depending on the result of the validation. However, what happens if fun receives invalid arguments altogether? In this case, fun can return a special error message, in the form of an array containing text.

To illustrate this, let's take at look at the slightly more intimidating teishi.test.match:

teishi.test.match = teishi.makeTest (function (a, b) {
   if (teishi.type (a) !== 'string') {
      return ['Invalid comparison string passed to teishi.test.match. Comparison string must be of type string but instead is', a, 'with type', teishi.type (a)];
   }
   if (teishi.type (b) !== 'regex') {
      return ['Invalid regex passed to teishi.test.match. Regex must be of type regex but instead is', b, 'with type', teishi.type (b)];
   }
   return a.match (b) !== null;
}, 'should match');

As you can see, if teishi.test.match receives a compare field that is not a string, or a to field that is not a regex, the fun will return custom error messages that are more illustrative than the standard one.

When teishi invokes fun, it will treat both false and a custom error message as indication that the test failed. The only difference between false and an error message is that when false is returned, the standard error message will be printed, whereas if an error is returned, the error itself will be printed.

The purpose of returning a custom error message is because this kind of error implies a programming error on the way that the teishi rules were written. If, for example, teishi.test.match is invoked with a non-string argument, this is because your function didn't check the type of the input before. For this category of errors, the default error message would be misleading, so that's why we print custom errors.

For more information, please refer to the annotated source code below, where I describe teishi.makeTest and all the test functions in detail.

Source code

The complete source code is contained in teishi.js. It is about 410 lines long.

Below is the annotated source.

/*
teishi - v5.1.0

Written by Federico Pereiro (fpereiro@gmail.com) and released into the public domain.

Please refer to readme.md to read the annotated source.
*/

Setup

We wrap the entire file in a self-executing anonymous function. This practice is commonly named the javascript module pattern. The purpose of it is to wrap our code in a closure and hence avoid making the local variables we define here to be available outside of this module. A cursory test indicates that local variables exceed the scope of a script in the browser, but not in node.js. This means that this pattern is useful only on the browser.

(function () {

Since this file must run both in the browser and in node.js, we define a variable isNode to check where we are. The exports object only exists in node.js.

   var isNode = typeof exports === 'object';

We require dale. Note that, in the browser, dale will be loaded as a global variable.

   var dale   = isNode ? require ('dale') : window.dale;

This is the most succinct form I found to export an object containing all the public members (functions and constants) of a javascript module. Note that, in the browser, we use the global variable teishi to export the library.

   if (isNode) var teishi = exports;
   else        var teishi = window.teishi = {};

indexOf polyfill

To provide compatibility with older browsers, teishi provides its own indexOf polyfill for arrays. If the method is already defined (as it will be on any ES5 compatible browser), the polyfill won't be set. You can also override it by loading your own polyfill before loading teishi.

The function takes two arguments, element and fromIndex.

   if (! Array.prototype.indexOf) Array.prototype.indexOf = function (element, fromIndex) {

Within the function, this will refer to the array on which we're applying the operation. We iterate its elements using dale.stopNot, stopping when we find the first value that is not undefined.

      var result = dale.stopNot (this, undefined, function (v, k) {

If fromIndex is present and it is larger than the index of the element currently being scanned, we ignore the element.

         if (fromIndex && k < fromIndex) return;

If we found an element in the array that is equal to element, we return the index.

         if (element === v) return k;
      });

If result is undefined, we could not find element, so we return -1. Otherwise, we return result which contains the index ofelement within the array. There's nothing else, so we close the function.

      return result === undefined ? -1 : result;
   }

Helper functions

We start by defining teishi.type, by far the most useful function of the bunch. This function is inspired on Douglas Crockford's remedial type function.

The purpose of teishi.type is to create an improved version of typeof. The improvements are two:

  • Distinguish between object, array, regex, date and null (all of which return object in typeof).
  • Distinguish between types of numbers: nan, infinity, integer and float (all of which return number in typeof).

Before we define teishi.type, we define argdetect, a local variable that will be true in most javascript engines. In Internet Explorer 8 and below, however, it is not possible to get the type of the prototype of an arguments pseudo-array, hence the definition of this variable (which will be used in type and also once more later).

The variable gets its value from a self-execution anonymous function. We need to do this since the arguments pseudo-array is only defined in the context of a function.

   var argdetect = (function () {return Object.prototype.toString.call (arguments).match ('arguments')}) ();

teishi.type takes a single argument (of any type, naturally) and returns a string which can be any of: nan, infinity, integer, float, array, object, function, string, regex, date, null and undefined.

If we pass a truthy second argument to teishi.type, and input turns out to be an object, teishi.type will return the lowercased name of the class of the object (which, for example, can be object for object literals, arguments for arguments pseudo-arrays, and other, user-created classes).

   teishi.type = function (value, objectType) {

We first apply typeof to value.

      var type = typeof value;

In Safari 5 and below, typeof returns function for regexes, so we need to perform instead a check using Object.prototype.toString.

      if (type === 'function') return Object.prototype.toString.call (value).match (/regexp/i) ? 'regex' : 'function';

Except for the exception of regexes in Safari we just saw above, teishi.type will only a result different from typeof if type is neither object nor number. If it's not the case, we return the type.

      if (type !== 'object' && type !== 'number') return type;

If value fulfills the condition below, it is an array. Hence, we return array.

      if (value instanceof Array) return 'array';

If type is number, we distinguish between nan, infinity, integer and float.

      if (type === 'number') {
         if      (isNaN (value))      return 'nan';
         else if (! isFinite (value)) return 'infinity';
         else if (value % 1 === 0)    return 'integer';
         else                         return 'float';
      }

We test whether value is null.

      if (value === null) return 'null';

If we're here, type is object, so now we want to find out which kind of object we're dealing with. We will do the following:

  • Stringify value through the function Object.prototype.toString and assign it to type.
  • type will now be a string of the form '[object CLASSNAME]', where CLASSNAME is what we're looking for.
  • We get rid of everything but the CLASSNAME, and we lowercase the result.
      type = Object.prototype.toString.call (value).replace ('[object ', '').replace (']', '').toLowerCase ();

Now, if type is array or date, we simply return the type. And if type is regexp, we return regex instead.

      if (type === 'array' || type === 'date') return type;
      if (type === 'regexp') return 'regex';

You may ask: why did we check for array, if we already covered this case before? If an array is created in a different frame, the instanceof check will fail, so the check above is a good fallback. The reason we use both checks is that the latter check is considerably slower than the instanceof check, so we use it as a failover.

Now, if the function received a truthy second argument, we want to return the exact class name of this object. In that case, we return type. Otherwise, we just return object.

Notice however that if argdetect is false (which means we're in Internet Explorer 8 and below), we will perform an extra check on value.callee - if it is of type function, we consider the object to be an arguments pseudo-array; there's no other way to check for this type, to the best of my knowledge.

      if (objectType) return argdetect ? type : (type (value.callee) === 'function' ? 'arguments' : type);

After this, there's nothing left to do, so we close the function.

      return 'object';
   }

teishi.str and teishi.parse are wrappers around JSON.stringify and JSON.parse, respectively. The only difference between these functions and their JSON counterparts is that if they receive invalid output, they will return false instead of throwing an exception.

   teishi.str = function () {
      try {return JSON.stringify.apply (JSON.stringify, arguments)}
      catch (error) {return false}
   }

   teishi.parse = function () {
      try {return JSON.parse.apply (JSON.parse, arguments)}
      catch (error) {return false}
   }

teishi.simple and teishi.complex return false/true (respectively) if their input is a complex value (array or object) and true/false otherwise.

   teishi.simple = function (input) {
      var type = teishi.type (input);
      return type !== 'array' && type !== 'object';
   }

   teishi.complex = function (input) {
      return ! teishi.simple (input);
   }

teishi.inc returns true or false depending on whether the value v is contained in the array (or arguments pseudo-array) a. Note we don't validate whether a is an array or pseudo-array.

   teishi.inc = function (a, v) {return a.indexOf (v) > -1}

teishi.copy does two things: 1) copy an input; 2) eliminate any circular references within the copied input.

The "public" interface of the function (if we allow that distinction, since in practice the user can pass extra arguments) takes a single argument, the input we want to copy. However, we define a private argument (seen) that the function will use to pass information to recursive calls.

This function is recursive. On recursive calls, input won't represent the input that the user passed to the function, but rather one of the elements that are contained within the original input.

   teishi.copy = function (input, seen) {

If input is not an array or object, we just return the input itself.

      if (teishi.simple (input)) return input;

If we're here, we know our object is complex. We detect the inputType of input. What we want to know here is if we're dealing with an array, an object, or an arguments pseudo-array - we want to treat the latter as an array.

We initialize the output variable to either an empty array or object, depending on the type of input.

      var inputType = teishi.type (input, true);
      var output    = inputType === 'array' || inputType === 'arguments' ? [] : {};

We iterate through the elements of input.

      dale.go (input, function (v, k) {

If v is neither an array nor an object, we set output [k] to v and return from this inner function.

         if (teishi.simple (v)) return output [k] = v;

If we're here, v is a complex object itself.

seen is a list of references to objects/arrays that are parents of the current element (v) we are iterating. The first time we invoke this function, seen is undefined, so we initialize it to [input]. If seen already exists, we copy it (through the concat function, which returns a copy of the array to which it is applied).

We store the seen array in a new local variable named Seen.

         var Seen = seen ? seen.concat () : [input];

If the element currently being iterated has already been seen, we found a circular reference! We set output [k] to a string of the form [Circular].

         if (teishi.inc (Seen, v)) return output [k] = '[Circular]';

If we're here, v is not circular. We push it onto seen.

         Seen.push (v);

We do a recursive call to teishi.copy, where the new input will be v itself. Note we pass a copy of path and append to it the key k. Note also we pass the seen array as well.

The result of this recursive call will be set to output [k].

This concludes the inner function.

         return output [k] = teishi.copy (v, Seen);
      });

We return the output. There's nothing else to do, so we close the function.

      return output;
   }

teishi.eq is a function that checks for deep equality between objects. It takes two arguments.

   teishi.eq = function (a, b) {

If a and b are simple, we compare them with === and return the result.

      if (teishi.simple (a) && teishi.simple (b)) return a === b;

If we are here, at least one of the arguments is complex. If their type is different, we return false, since they can't be equal.

      if (teishi.type (a, true) !== teishi.type (b, true)) return false;

If we're here, both elements are complex and have the same type. We now compare their keys and check that they are exactly the same. To do this, instead of iterating the keys, we simply take all of them, sort them, stringify them and then compare them. If this comparison is not true, we know there is one key in one object that's not present in the other, hence we return false.

      if (teishi.str (dale.keys (a).sort ()) !== teishi.str (dale.keys (b).sort ())) return false;

We loop through the elements of a.

      return dale.stop (a, false, function (v, k) {

Here v is a given element of a, and k is the key of that element (if a is an array, k will be a number, and if a is an object, k will be a string). We invoke inner recursively passing it v and b [k], the latter being the corresponding element to v in b.

If a difference is found, this function will return false. If, however, both elements are either empty arrays or objects, the invocation to dale.stop will return undefined. In this case, they must be true, since they are elements of the same type with no elements, so we return true. At this point, there's nothing else to do, so we close the function.

         return teishi.eq (v, b [k]);
      }) === false ? false : true;
   }

We define teishi.last, which will return the last element of the array which it receives as its first argument. If a second element is passed to it, the function will instead return the nth element from the end.

If its first argument is not an array, we print an error and return false. If its second argument is not undefined, it must be an integer larger than 0, otherwise we print an error and return false.

   teishi.last = function (a, n) {
      if (! teishi.inc (['array', 'arguments'], teishi.type (a, true))) return teishi.clog ('First argument passed to teishi.last must be array or arguments but instead has type ' + teishi.type (a, true));
      if (n !== undefined && (teishi.type (n) !== 'integer' || n < 1)) return teishi.clog ('Second argument passed to teishi.last must be either undefined or an integer larger than 0.');
      return a [a.length - (n || 1)];
   }

We define teishi.time, which will return the current date in milliseconds. If you pass an argument to this function, it will be passed in turn to the internal invocation of `new

5.1.0

2 years ago

5.0.3

4 years ago

5.0.2

4 years ago

5.0.1

4 years ago

5.0.0

5 years ago

4.0.0

5 years ago

3.14.1

5 years ago

3.14.0

5 years ago

3.13.2

5 years ago

3.13.1

6 years ago

3.13.0

6 years ago

3.12.0

6 years ago

3.11.1

7 years ago

3.11.0

7 years ago

3.10.0

7 years ago

3.9.0

7 years ago

3.8.0

7 years ago

3.7.0

8 years ago

3.6.0

8 years ago

3.5.0

8 years ago

3.4.0

8 years ago

3.3.0

8 years ago

3.2.1

8 years ago

3.2.0

8 years ago

3.1.5

8 years ago

3.1.4

8 years ago

3.1.3

8 years ago

3.1.2

9 years ago

3.1.1

9 years ago

3.1.0

9 years ago

3.0.4

9 years ago

3.0.3

9 years ago

3.0.2

9 years ago

3.0.1

9 years ago

3.0.0

9 years ago

2.1.10

9 years ago

2.1.9

9 years ago

2.1.8

9 years ago

2.1.7

9 years ago

2.1.6

9 years ago

2.1.5

9 years ago

2.1.4

9 years ago

2.1.3

9 years ago

2.1.2

9 years ago

2.1.1

9 years ago

2.1.0

9 years ago

2.0.0

9 years ago

1.0.12

10 years ago

1.0.11

10 years ago

1.0.10

10 years ago

1.0.9

10 years ago

1.0.8

10 years ago

1.0.7

10 years ago

1.0.6

10 years ago

1.0.5

10 years ago

1.0.4

10 years ago

1.0.3

10 years ago

1.0.2

10 years ago

1.0.1

10 years ago

1.0.0

10 years ago