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 thanundefined 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
forcedencouraged 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:
- Error detection.
- Error notification.
- 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:
- 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.
- 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.
- 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 valueto
. - We will also need a
name
to describe whatcompare
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 ofcounter
is. - The
to
field is'integer'
.
multi
operator
The 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
andundefined
. This is because we have two possibleto
values we accept. - We add a fourth element to the rule, the string
'oneOf'
. This addition ensures thatcompare
should conform to one of theto
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 singlecompare
and two or more accepting values, like in the example we just saw. -
'each'
: when you have manycompare
values and a single accepting value. For example, if we want to check thatstrings
is an array of strings, we write the rule:['strings', strings, 'string', 'each']
-
'eachOf'
: this variant combines both'each'
and'oneOf'
, and it compares multiplecompare
values to multipleto
values. For example, if we want to check thatinput
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.
test
operator
The 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 wantcompare
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
andnull
(all of which returnobject
usingtypeof
). - We distinguish between types of numbers:
nan
,infinity
,integer
andfloat
(all of which returnnumber
usingtypeof
).
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.
names
instead of one
Two 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
.
multi
All you need to know about 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, norundefined
-
complex value
: an array or an object -
empty value
: eitherundefined
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 exampleinput [0]
) but the expected array is not of typearray
. -
compare
references a property of an object (for exampleinput.limit
) but the expected object is not of typeobject
. -
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 eithertrue
or a function. -
prod
, an optional argument that can be set totrue
.
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
andfloat
(all of which returnnumber
intypeof
). - Distinguish between
array
,date
,null
,regex
andobject
(all of which returnobject
intypeof
).
The possible types of a value can be grouped into three:
-
Values which
typeof
detects appropriately:boolean
,string
,undefined
,function
. -
Values which
typeof
considersnumber
:nan
,infinity
,integer
,float
. -
values which
typeof
considersobject
:array
,date
,null
,regex
andobject
.
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 wrapsJSON.stringify
. -
teishi.parse
, which wrapsJSON.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 supportconsole.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 returnstrue
orfalse
. These two arguments will be, as you might imagine, thecompare
and theto
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 afinalClause
.
- a string (
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
andnull
(all of which returnobject
intypeof
). - Distinguish between types of numbers:
nan
,infinity
,integer
andfloat
(all of which returnnumber
intypeof
).
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 functionObject.prototype.toString
and assign it totype
. -
type
will now be a string of the form'[object CLASSNAME]'
, whereCLASSNAME
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 Date
; in this way, you can get the timestamp of a date other than one representing the present moment.
teishi.time = function (d) {return arguments.length ? new Date (d).getTime () : new Date ().getTime ()}
We define two local variables useful for teishi.clog
, defined below:
-
lastColor
, a variable that holds the last ansi color used, so that it is not repeated twice in a row. -
ansi
, an object with several defaults for printing color to the console.
var lastColor, ansi = {
The magic of teishi.clog
's colors is done through ANSI escape codes. We will define a local object ansi
which will contain four functions.
ansi.bold
will bold the text after it, ansi.end
will remove all format from the text after it and ansi.white
will make white the text after it. We define them to be functions instead of constants because this will enable an invocation to teishi.lno
(defined below) to turn off formatting.
end: function () {return isNode ? '\033[0m' : ''},
bold: function () {return isNode ? '\033[1m' : ''},
white: function () {return isNode ? '\033[37m' : ''},
Notice that if we are in the browser, all of these variables will contain an empty string.
We will use six colors in teishi: red, green, yellow, blue, magenta and cyan. We will skip white because it is too plain, and black because it is hard to read.
ansi.color
is the function that will return the ANSI codes for coloring text using one of the six colors above, chosen randomly. If the function receives true
as its argument, it will return a code for coloring the background of the text, which is useful for the label
.
color: function (reverse) {
If we are in the browser, we return an empty string.
if (! isNode) return '';
We set a local variable color
to the value of lastColor
. We then set it to a number between 1 and 6, until it's different from lastColor
. We then set lastColor
to color
. The purpose of this sequence is to ensure that lastColor
changes to a new value between 1 and 6.
var color = lastColor;
while (lastColor === color) color = Math.round (Math.random () * 5 + 1);
lastColor = color;
We return the corresponding ANSI codes for coloring either text or background, depending on whether reverse
is set or not.
return '\033[' + (reverse ? '4' : '3') + color + 'm';
}
}
We will now define teishi.clog
which is teishi's wrapper for console.log. The improvements are:
- Colors (on node.js, not the browser).
- Unlimited expansion of nested arrays and objects.
- Timestamp for profiling purposes.
- More compact indentation/newline rules for printing nested objects.
- Stringify functions and print their first 150 characters only.
- Defaults to
alert
for very old browsers that don't supportconsole.log
.
teishi.clog = function () {
We define output
, a string where we will append all the formatted text we want to print. We will initialize it to ansi.bold
.
var output = ansi.bold ();
We define a function inner
that we will apply recursively to the arguments
. The reason for writing a function here is that we want to recurse over complex elements, such as arrays and objects.
Notice the parenthesis before the function definition - we place it because this is a function expression, which allows for a named, recursive, and immediately executed function.
The function takes two arguments: input
, the input to be printed, and depth
, a numeric value that indicates the level of nestedness of the element being printed.
(function inner (input, depth) {
We detect the type of input
and store it in a local variable inputType
. We also initialize depth
to 0, if it is undefined. Finally, we initialize first
to true
; this variable will be useful to see if we're processing the first
element of a complex element.
var inputType = teishi.type (input), depth = depth || 0, first = true;
If input
is an arguments
pseudo-array, we will set inputType
to 'array'
.
if (inputType === 'object' && Object.prototype.toString.call (input) === '[object Arguments]') inputType = 'array';
We define a local variable indent
. If depth
is less than 2, we don't want to indent anything, so we will set it to an empty string. Otherwise, we will set it to be a newline (\n
) followed by (depth - 1) * 3 spaces (for example, if depth
is 2, then indent
will have 3 spaces).
var indent = depth < 2 ? '' : '\n' + dale.go (dale.times (depth - 1), function (v) {return ' '}).join ('');
If depth
is larger than 0, we add an opening [
or {
, depending on whether inputType
is array
or object
. Notice that this assumes that inputType
is always either of these. Because of how we invoke this function the first time, and how we invoke it recursively, this will always be the case.
if (depth > 0) {
if (inputType === 'array') output += ansi.white () + '[';
else output += ansi.white () + '{';
}
We now will iterate over the items of input
.
dale.go (input, function (v, k) {
For every item in output
, we'll note its type.
var typeV = teishi.type (v);
If a) we are in a non-recursive (initial) call to inner
, b) we are iterating the first element of input
and c) this element is either a string or an integer, we'll consider this element to be a label. Hence, we will:
- Apply a special background color to it.
- Place a trailing colon and ansi color codes to remove the background color and then set again the bold font. However, we will only put the trailing colon if
input
has more than one element, to avoidoutput
being finished with a colon followed by nothing else. - Concatenate it to
output
. - Set
first
totrue
. - Exit this iteration (with
return
) since there's nothing else to do with this element.
In this case, we will return the label.
if (depth === 0 && k === 0 && (typeV === 'string' || typeV === 'integer')) {
first = false;
return output += ansi.color (true) + v + (input.length > 1 ? ':' : '') + ansi.end () + ansi.bold ();
}
If this element is not the first one, we will concatenate either a space or a comma plus space, depending on whether depth
is 0 or more. This is to separate contiguous elements within a complex structure. In every case, we set first
to false
, to mark that we have already seen the first element.
if (! first) output += ansi.white () + (depth === 0 ? ' ' : ', ');
first = false;
If the element being iterated is a string and we are in a recursive call to inner
, we surround the element with single quotes. If we did this on the initial call to inner
, the output of teishi.clog ('Hey', 'there')
would be 'Hey', 'there'
, whereas what we want is to get Hey there
. Another way of seeing this is that we treat differently strings that are within objects or arrays.
if (typeV === 'string' && depth > 0) v = "'" + v + "'";
If the element being iterated is a function we will do a few modifications to the value before concatenating it:
if (typeV === 'function') {
We will convert the function to a string.
v = v + '';
We will define baseIndent
as the amount of space between the beginning of the last line and the last closing curly bracket of the function. We will then replace this space in the function with a single newline. Since baseIndent
already includes a newline (because \s
matches a newline as well), this will have the result of removing the base indent of the function.
var baseIndent = v.match (/\s+(?=}$)/);
if (baseIndent !== null) v = v.replace (new RegExp (baseIndent [0], 'g'), '\n');
Then, if it's length is more than 150 characters, we will slice it and then append ellipsis to it, plus a newline.
if (v.length > 150) v = v.slice (0, 150) + '...\n';
Finally, if depth
is more than 1, we will add indent
to the beginning of each line. In the case where input
is an object, however, we will add a few more whitespaces to indent
, exactly as many as the key length plus two, so that functions are alignedP
// Without extra space:
fun: function () {
// something
}
// With extra space:
fun: function () {
// something
}
Here's the code that adds indent
to each line:
if (depth > 1) v = v.replace (/\n/g, inputType === 'array' ? indent : ('\n' + (k + ': ').replace (/./g, ' ') + indent.slice (1)));
}
We now append an invocation of ansi.color
to output, to set the color that we will use for this particular element.
output += ansi.color ();
If the element being iterated is an object, we also want to print its key k
, using the format key: value
.
Now, in javascript, the key of an object can be any string, but if you use a key that contains non-alphanumeric characters, you need to surround it by quotes to be able to use it without producing a syntax error.
// invalid
{
!@#@&$*: 'wow'
}
// valid
{
'!@#@&$*': 'wow'
}
Hence, if the element is an object, we will append to output
the indent
and also the key, surrounding it with quotes if it is non-alphanumeric, and appending a colon and a space.
if (inputType === 'object') output += indent + (k.match (/^[0-9a-zA-Z_]+$/) ? k : "'" + k + "'") + ': ';
Now, if the element we're currently inspecting is also a complex entity, we will do a recursive call to inner
, taking care to increase depth
by 1.
if (typeV === 'array' || typeV === 'object') inner (v, depth + 1);
Otherwise, we add v
to output
. If we're adding an element that belongs to an object
, indent
will be already placed (along with k
) - this means we'll only need to place indent
in the case where inputType
is array
.
After this, we're done with the subelements of input
.
else output += inputType === 'object' ? v : indent + v;
});
If depth
is larger than 0
, we place the closing square or curly brackets, depending on inputType
. Notice we also place the right amount of indentation and the ansi code for white text.
if (depth > 0) {
if (inputType === 'array') output += (depth > 1 ? '\n' : '') + indent.slice (4) + ansi.white () + ']';
if (inputType === 'object') output += (depth > 1 ? '\n' : '') + indent.slice (4) + ansi.white () + '}';
}
We close inner
and we immediately execute it, passing arguments
as its first element. This means that the case where depth
equals 0 will be when we pass arguments
, which we interpret to be an array. Since inner
is only called on complex objects, this is how we assure that inputType
will always be an array or object.
Notice that we copy arguments
. The reason for doing is is that we want to be able to print circular structures without falling into an infinite loop.
}) (teishi.copy (arguments));
By now, output
will contain all the text we want to print, with proper colors and indentation. All that's left is to print it, in the following way:
- The current UTC time, printed according to the format specified in RFC 2822. If the method is not defined (which happens in old browsers, we default to
toString
instead). - The colored
output
, which comes from passingarguments
toinner
. -
ansi.end
, to avoid coloring any subsequent output in the console.
Notice we use dale.clog
instead of using console.log
directly; this allows for printing an alert
message in browsers that don't support console.log
.
var d = new Date ();
dale.clog ('(' + d [d.toISOString ? 'toISOString' : 'toString'] () + ')', output + ansi.end ());
Finally we return false
, since this allows calling functions to print an error and return false
in the same line. For example: return teishi.clog ('This is an error')
.
There's nothing else to do after this, so we close the function.
return false;
}
We add teishi.lno
, a function that by setting isNode
to false, will turn off coloring and formatting in all teishi output (and the output of other libraries that use teishi.clog
as well).
teishi.lno = function () {isNode = false}
Test functions
teishi.makeTest
is a function that will create test functions. The common elements of every test function are wired into this function. This simplifies the test functions.
The function receives two arguments, fun
and clauses
.
teishi.makeTest = function (fun, clauses) {
If fun
is not a function, we print an error and return false
.
if (teishi.type (fun) !== 'function') {
return teishi.clog ('teishi.makeTest', 'fun passed to teishi.makeTest should be a function but instead is', fun, 'with type', teishi.type (fun));
}
clauses
can be either a string (if we only specify a shouldClause
or an array (which allows us to also specify a finalClause
). To merge both cases into one, if clauses
is a string we wrap it in an array.
if (teishi.type (clauses) !== 'string') clauses = [clauses];
We check that clauses
is an array and that clauses [0]
is a string. If any of these checks fails, we print an error and return false
.
if (teishi.type (clauses) !== 'array') {
return teishi.clog ('teishi.makeTest', 'clauses argument passed to teishi.makeTest should be an array but instead is', clauses, 'with type', teishi.type (clauses));
}
if (teishi.type (clauses [0]) !== 'string') {
return teishi.clog ('teishi.makeTest', 'shouldClause passed to teishi.makeTest should be a string but instead is', clauses [0], 'with type', teishi.type (clauses [0]));
}
If clauses [1]
is not undefined
and not an array, we wrap it in an array. We then ensure that it is composed of strings or functions. If that's not the case, we print an error and return false
.
if (clauses [1] !== undefined) {
if (teishi.type (clauses [1]) !== 'array') clauses [1] = [clauses [1]];
var clausesResult = dale.stopNot (clauses [1], true, function (v) {
var type = teishi.type (v);
if (type === 'string' || type === 'function') return true;
return teishi.clog ('teishi.makeTest', 'Each finalClause passed to teishi.makeTest should be a string or a function but instead is', v, 'with type', type);
});
if (clausesResult !== true) return;
}
If we reach this point, both fun
and clauses
are valid. Below is the actual test function that will be returned by teishi.makeTest
.
This function takes four to six arguments:
-
functionName
, which is an (optional) argument that is passed toteishi.v
andteishi.stop
, used to identify the calling function. -
names
, the names of thecompare
and theto
arguments, the latter being optional. -
compare
, the compared value. -
to
, the reference value. -
eachValue
, which is the originalcompare
value in a rule that hasmulti
set to'each'
or'eachOf'
. -
ofValue
, which is the originalto
value in a rule that hasmulti
set to'oneOf'
or'eachOf'
.
return function (functionName, names, compare, to, eachValue, ofValue) {
Notice there's no validation of the arguments in this function. This is for two reasons:
- We already validated the input of teishi.makeTest above.
- This function will be invoked directly by
teishi.v
andteishi.stop
. We can trust that these two functions will pass their input in a trustworthy (error-free) way, so we don't validate inputs coming from them.
We create a local variable result
where we store the result of applying the test to compare
and to
.
var result = fun (compare, to);
If the test was valid, we return true
. If the test returned an array, it is an error. We return it, since we will let teishi.v
and teishi.stop
take care of it.
if (result === true) return true;
if (teishi.type (result) === 'array') return result;
If we are here, result
is equal to false
. We found a validation error! Below we create an array which will contain the error message.
The block below, although tedious to read, is best explained by reading the code in detail. By adding these elements in order, we will have a specific error message, built from generic blocks.
Earlier versions of the library pushed the chunks of the error message into error
. However, for performance reasons (because this function is executed every time an error is encountered and hence can be considered as part of the "inner loop" of teishi), we have opted to create an index
variable and use it to append elements to error
.
var error = [], index = 0;
if (eachValue !== undefined) error [index++] = 'each of the';
if (names [0]) error [index++] = names [0];
if (functionName) error [index++] = 'passed to', error [index++] = functionName;
error [index++] = clauses [0];
if (ofValue !== undefined) error [index++] = 'one of';
error [index++] = ofValue !== undefined ? ofValue : to;
if (names [1]) error [index++] = '(' + names [1] + ')';
error [index++] = eachValue !== undefined ? 'but one of' : 'but instead';
if (eachValue !== undefined) error [index++] = eachValue;
error [index++] = 'is';
error [index++] = compare;
We add the elements of finalClause
to the error. If any of them is a function, we invoke it passing compare
and to
as arguments, and use that result in the error message.
dale.go (clauses [1], function (v) {
error [index++] = typeof v !== 'function' ? v : v (compare, to);
});
We return the error
.
return error;
}
}
We create the object that will contain the test functions bundled with teishi.
Notice that all of these functions:
- Are created by invoking
teishi.makeTest
. - All of them receive two arguments,
compare
andto
. - All of them return either
true
,false
, or an array containing an error message.
teishi.test = {
teishi.test.type
uses a very simple fun
and defines both a shouldClause
and a finalClause
. Notice that the second finalClause
is the function teishi.type
.
type: teishi.makeTest (
function (a, b) {return teishi.type (a) === b},
['should have as type', ['with type', teishi.type]]
),
teishi.test.equal
tests for deep equality if its inputs are arrays or objects, relying on teishi.eq
.
equal: teishi.makeTest (teishi.eq, 'should be equal to'),
teishi.test.notEqual
is almost identical to teishi.test.equal
, except that we invert the result returned by teishi.eq
.
Their shouldClause
s also differ by one 'no'
.
notEqual: teishi.makeTest (function (a, b) {
return ! teishi.eq (a, b);
}, 'should not be equal to'),
teishi.test.range
will do some validation checks to its inputs, namely: b
should be an object, which can be empty or contain the following keys: min
, max
, less
and more
.
range: teishi.makeTest (function (a, b) {
if (teishi.type (b, true) !== 'object') {
return ['Range options object must be an object but instead is', b, 'with type', teishi.type (b)];
}
If there are no conditions, we return true
.
if (teishi.str (b) === '{}') return true;
We iterate through the keys of b
and validate them.
return dale.stopNot (b, true, function (v, k) {
if (k !== 'min' && k !== 'max' && k !== 'less' && k !== 'more') {
return ['Range options must be one of "min", "max", "less" and "more", but instead is', k]
}
We test the corresponding condition and return the result. We close the fun
and add the shouldClause
.
if (k === 'min') return a >= v;
if (k === 'max') return a <= v;
if (k === 'less') return a < v;
return a > v;
});
}, 'should be in range'),
}
teishi.test.match
, like the previous function, also does some validation on its inputs. We need a
to be a string, and b
to be a regex.
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)];
}
We check whether a
matches b
. We then close the fun
and add the shouldClause
.
return a.match (b) !== null;
}, 'should match')
}
Validation
To ensure that a given teishi rule is valid, we will now define teishi.validateRule
.
Although this function can be used directly in your code, you will probably never need to use it directly, since the main functions of teishi (teishi.v
and teishi.stop
) invoke this function.
teishi.validateRule
takes a single argument (rule
).
teishi.validate = function (rule) {
We store the type of rule
in a local variable ruleType
.
var ruleType = teishi.type (rule);
If ruleType
is function
or boolean
, the rule is valid, so we return true
.
if (ruleType === 'function' || ruleType === 'boolean') return true;
If ruleType
is not an array, the rule is invalid. We return an error message.
if (ruleType !== 'array') {
return ['each teishi rule must be an array or boolean or function but instead is', rule, 'with type', ruleType];
}
If we are at this point, rule
is an array.
We now need to determine whether we are dealing with a simple rule or not. If we are not dealing with a simple rule, we assume that the array is a complex rule (either a nested or a conditional rule) and return true
.
You may ask: why don't we do a deep check for the equality of a nested or conditional rule? Since teishi.v
and teishi.stop
are recursive functions, when they find a conditional or nested rule, they invoke themselves recursively. When they do so, they validate the inner content of a complex rule.
For example, if teishi.validateRule
receives an array representing invalid rule (such as [222, 333, /regex!/]
), it will return true
. However, when teishi.v
treats each of the elements of the rule as a rule and passes them again to teishi.validateRule
, we will obtain an error message saying that 222
cannot be a valid teishi rule.
By just validating rules to one level of deepness, we also make the code efficient, because we don't validate nested rules over and over, just once, when they can be considered simple rules.
To determine whether we are dealing with a simple rule, we will check if there's a valid names
field in it. Remember that names
is either a string or an array containing two strings.
Our choice of names
as the distinctive element of a teishi simple rule is straightforward: compare
and to
can have any type, and options
is not required. names
, by its possible types, is distinguishible from nested or conditional rules.
We write an intricate conditional to check that whether the rule has a names
as its first element. If that is not the case, we are dealing with a complex rule, so we return true
.
var typeFirst = teishi.type (rule [0]);
if (! (typeFirst === 'string' || (typeFirst === 'array' && rule [0].length === 2 && teishi.type (rule [0] [0]) === 'string' && teishi.type (rule [0] [1]) === 'string'))) return true;
If we are here, we are dealing with a simple rule.
If the rule has length 3, no multi
or test function is present. We've already checked the three elements of the rule, so we return true
.
if (rule.length === 3) return true;
We check that the rule has a length of three to five elements (names
, compare
, to
, plus multi
, the test function or both). If that's not the case, we return an error.
if (rule.length < 3 || rule.length > 5) {
return ['Each teishi simple rule must be an array of length between 3 and 5, but instead is', rule, 'and has length', rule.length];
}
Because we used the presence of a valid names
element to check whether this rule is a simple one, if we are here, we know that names
is valid.
We also don't need to do any checks on compare
or to
, since they can have any value.
We define two local variables test
and multi
to hold the values for the test function and the multi
parameter.
var test, multi;
We iterate through the rule, to validate its fourth and fifth elements of the rule. If any of these iterations returns a value that is not true
, the loop will be stopped and the last value returned will be stored in result
.
var result = dale.stopNot (rule, true, function (v, k) {
If we're iterating through the first three elements, we ignore them.
if (k < 3) return true;
We note the type of the element.
var type = teishi.type (v);
If the element is a string, it has to be the multi
operator. We check that it is one of 'oneOf'
, 'each'
and 'eachOf'
.
if (type === 'string') {
if (v !== 'oneOf' && v !== 'each' && v !== 'eachOf') return ['Invalid multi parameter', v, '. Valid multi parameters are', ['oneOf', 'each', 'eachOf']];
If multi
is already defined (because we are currently on the fifth element of the rule and the fourth one already turned out to be the multi
operator) we return an error.
if (multi) return ['You can pass only one multi parameter to a teishi simple rule but instead you passed two:', rule [3], 'and', rule [4]];
We set multi
to the element.
multi = v;
}
If the element is a function
, and test
was already set, we return an error. Otherwise, we set test
to the element.
else if (type === 'function') {
if (test) return ['You can pass only one test function to a teishi simple rule but instead you passed two:', rule [3], 'and', rule [4]];
test = v;
}
If the element is neither a function
nor a string
, we return an error.
else return ['Elements #4 and #5 of a teishi simple rule must be either a string or a function, but element', '#' + (k + 1), 'is', v, 'and has type', type];
If we are here, no error were found within the current iteration. We return true
.
return true;
});
result
can be either an error (because the rule is invalid) or true
(because the rule is valid). In either case, we want to return it. After that, there's nothing else to do.
return result;
}
The main functions
In this section, we will define teishi.v
and teishi.stop
, the main functions of teishi.
We will now define a function reply
which is in charge of doing a set of actions that are performed when teishi.v
finds an error. These actions will vary depending on the value of the apres
variable passed to either teishi.v
or teishi.stop
.
var reply = function (error, apres) {
If apres
is undefined
(the default case), we want to print the error through teishi.clog
. We append 'teishi.v'
as the first element of the error, so that it's considered as the label of the error message. We return the result of calling teishi.clog
, which is always false
.
if (apres === undefined) return teishi.clog.apply (teishi.clog, ['teishi.v'].concat (error));
If apres
is defined, we need to stringify the error, in case it contains arrays, objects, or other elements that can lose data when being coerced onto a string.
We will now iterate through error
(which is an array), stringify each of its elements (through teishi.parse
if the element is an object or array, and through string coercion otherwise), and join the resulting array with single spaces. We will set error
to this string.
error = dale.go (error, function (v) {
return teishi.complex (v) ? teishi.str (v) : v + '';
}).join (' ');
If apres
is true
, we will return the error message.
if (apres === true) return error;
If we're here, it's because apres
is a function. We pass the error to it, return false
, and close the reply
function.
apres (error);
return false;
}
teishi.stop
is a very simple wrapper around teishi.v
, so most of the action will revolve around teishi.v
. Without further ado, we proceed to write this function.
teishi.v = function (first, second, third, fourth) {
The only required argument to teishi.v
is rule
, which can be either the first or the second argument. rule
will be the second argument only if functionName
is passed.
Since a rule
can never be a string, and functionName
always has to be a string, we decide that functionName
is present if the first argument of the function is a string.
We set apres
to be the argument that was passed after rule
. If no argument was passed, it will be undefined
.
We set prod
to be the argument that was passed after apres
. If no argument was passed, it will be undefined
.
A subtle point: we set functionName
to an empty string, instead of undefined
. This is because, for recursive function calls, we want to have a fixed number of arguments, so as to simplify writing the recursive calls. This also improves the performance of the function.
if (teishi.type (first) === 'string') var functionName = first, rule = second, apres = third, prod = fourth;
else var functionName = '', rule = first, apres = second, prod = third;
If neither prod
nor teishi.prod
is set, we proceed to perform validations. If, on the contrary, either prod
or teishi.prod
are set to a truthy value, the validations will be skipped and the input will be assumed to be valid.
if (! (prod || teishi.prod)) {
Because we assume that functionName
is defined only if the first argument is a string (and set its value to an empty string otherwise), functionName
will be a string, so we don't need to validate it. This is similar to what happened with the validation-through-assumption of names
we did in teishi.validateRule
.
We validate apres
: it must be either undefined
, true
, or a function. If it's not, we print an error message and return false
.
if (apres !== undefined && apres !== true && teishi.type (apres) !== 'function') return teishi.clog ('teishi.v', 'Invalid apres argument. Must be either undefined, true, or a function.');
We invoke teishi.ValidateRule
to check that rule
is valid, and store the result in a local variable validation
.
var validation = teishi.validateRule (rule);
If rule
is not well-formed, we pass the error to reply
and return
. We close the validation block.
if (validation !== true) return reply (validation, apres);
}
We store the type
of rule
in a local variable ruleType
.
var ruleType = teishi.type (rule);
Boolean rules: if rule
is a boolean, we return the rule itself.
if (ruleType === 'boolean') return rule;
Function guards: if rule
is a function, we invoke the rule
and pass it recursively to teishi.v
, taking care to also pass functionName
, apres
and prod
.
if (ruleType === 'function') return teishi.v (functionName, rule (), apres, prod);
If we are here, rule
must be an array.
If it has length zero, there are no rules to validate, hence there can't be any validation errors! We return true
.
if (rule.length === 0) return true;
We store the type
of rule [0]
in a local variable ruleFirstType
.
var ruleFirstType = teishi.type (rule [0]);
Conditional rules: if the first element of rule
is a boolean, if rule
has length 2, and if the second element of rule
is an array, we treat rule
as a conditional one.
if (ruleFirstType === 'boolean' && rule.length === 2 && teishi.type (rule [1]) === 'array') {
If the boolean of the conditional is false
, the second rule doesn't apply. Hence, we return true
.
if (rule [0] === false) return true;
If we are here, the second rule within rule
applies, so we pass it recursively to teishi.v
.
else return teishi.v (functionName, rule [1], apres, prod);
}
We use again the intricate conditional from teishi.validateRule
to determine whether this rule is not a simple one.
if (! (ruleFirstType === 'string' || (ruleFirstType === 'array' && rule [0].length === 2 && teishi.type (rule [0] [0]) === 'string' && teishi.type (rule [0] [1]) === 'string'))) {
Nested rule: we iterate the rule
and pass each of its elements to recursive calls to teishi.v
.
If any of these calls returns false
, the loop is stopped and false
is returned. If all of these calls returned true
, we return true
as well.
return dale.stopNot (rule, true, function (rule) {
return teishi.v (functionName, rule, apres, prod);
});
}
We define the local variable typeFourth
and typeFifth
to hold the types of the fourth and fifth elements of the rule.
var typeFourth = teishi.type (rule [3]), typeFifth = teishi.type (rule [4]);
We define a local variable test
, to store the test operator. If either the fourth or fifth elements of the array are a function, we will set test
to that. Otherwise, the default value of test
is teishi.test.type
.
var test = typeFourth === 'function' ? rule [3] : (typeFifth === 'function' ? rule [4] : teishi.test.type);
We define a local variable multi
, to store the multi operator. If either the fourth or fifth elements of the array are a string, we will set multi
to that. Otherwise, the default value of test
is undefined
.
var multi = typeFourth === 'string' ? rule [3] : (typeFifth === 'string' ? rule [4] : undefined);
We set a local variable result
to hold the result of the validation.
var result;
We set a local variable names
to hold the names
field of rule
.
If names
is a string, we wrap it in an array, since the test functions will be expecting names
to be an array. For evidence of this, check that the returned function from teishi.makeTest
(which comprises the body of every test function) directly invokes names [0]
and names [1]
.
var names = ruleFirstType === 'array' ? rule [0] : [rule [0]];
We note the types of compare
and to
.
var typeCompare = teishi.type (rule [1], true), typeTo = teishi.type (rule [2], true);
We deal with a special case of multi
: if multi
is either each
or eachOf
, and the compare
field is either undefined
, an empty array or an empty object, we deem that there are no elements inside compare
.
In the absence of elements to validate, we consider rule
to be fulfilled, and return true
.
if ((multi === 'each' || multi === 'eachOf') && ((typeCompare === 'array' && rule [1].length === 0) || (typeCompare === 'object' && dale.keys (rule [1]).length === 0) || rule [1] === undefined)) {
return true;
}
We deal with the other special case of multi
: if multi
is either oneOf
or eachOf
, and the to
field is either undefined
, an empty array or an empty object, we deem that there are no elements inside to
.
In the absence of elements to compare to, we consider rule
to be impossible to be fulfilled. Hence, we set result
to an error.
if ((multi === 'oneOf' || multi === 'eachOf') && ((typeTo === 'array' && rule [2].length === 0) || (typeTo === 'object' && dale.keys (rule [2]).length === 0) || rule [2] === undefined)) {
result = ['To field of teishi rule is', rule.to, 'but multi attribute', multi, 'requires it to be non-empty, at teishi step', rule];
}
We now consider one of the "normal" cases of multi
: when it is undefined
.
We invoke the test function with functionName
, names
, rule [1]
and rule [2]
as arguments, and store the result in result
.
Notice that the arguments we pass (and their order) match those of the inner function in teishi.makeTest
. That function receives six arguments, functionName
, names
, compare
, to
, eachValue
and ofValue
.
As you can see, the first two match each other. rule [1]
corresponds to compare
, rule [2]
corresponds to to
. In this particular case, eachValue
and ofValue
are left undefined
.
Also notice that we use else if
instead of if
, because we are computing result
according to the possible values of multi
.
else if (multi === undefined) {
result = test (functionName, names, rule [1], rule [2]);
}
We now consider multi === 'each'
.
else if (multi === 'each') {
We iterate through compare
(rule [1]
) and for each of its elements we invoke the test function.
Notice that, for any given element, the compare
value received by the test function is the specific element of the compare
from the rule (named v
in the function). Notice also that we pass compare
as the eachValue
argument.
The purpose of eachValue
is to provide the test function with the original value of compare
(before being decomposed into subparts), so that the compare
value in an error message will match that of rule
.
If any of these tests returns something other than true
, we stop the iteration and set result to that value. If every test returns true
, we set result to true
.
result = dale.stopNot (rule [1], true, function (v) {
return test (functionName, names, v, rule [2], rule [1]);
});
}
We now consider multi === 'oneOf'
.
else if (multi === 'oneOf') {
We iterate through to
(rule [2]
) and for each of its elements, we invoke the test function.
The main structural difference between this block and the one above is that it will stop when it finds a test that returns true
. This is because oneOf
is valid as long as one of the to
values matches the compare
value. In contrast, each
demands that every compare
value match the to
value.
Notice also that we pass undefined
as the eachValue
and to
as the ofValue
.
We store the result of this loop into result
.
result = dale.stop (rule [2], true, function (v) {
return test (functionName, names, rule [1], v, undefined, rule [2]);
});
}
If we are here, multi
equals eachOf
. This is the last case.
else {
We iterate first through the elements of compare
(like we did in each
above), and stop whenever we find something that is not true
. For each of the elements of compare
, we iterate through the elements of to
, and stop if any of the comparisons returns true
.
result = dale.stopNot (rule [1], true, function (v) {
return dale.stop (rule [2], true, function (v2) {
We invoke the test function passing all six arguments, including the eachValue
and ofValue
.
return test (functionName, names, v, v2, rule [1], rule [2]);
});
});
}
If result
is true
, rule
is valid. We return true
.
if (result === true) return true;
If result
is not true
, the rule
is not fulfilled, hence result
contains an error. We pass it to reply
. We have nothing else to do, so we close the function.
else return reply (result, apres);
}
We now define teishi.stop
. The purpose of this function is to invoke teishi.v
and return true
if an error is found and false
otherwise.
Notice that since we apply arguments
onto teishi.v
, you can pass an apres
parameter. If the apres
parameter is a function, you can still do something meaningful with the error message.
teishi.stop = function () {
return teishi.v.apply (teishi.v, arguments) === true ? false : true;
}
We close the module.
}) ();
License
teishi is written by Federico Pereiro (fpereiro@gmail.com) and released into the public domain.