Schema
This document serves as an introduction to the schema concept of the Cerny.js library. A Cerny Schema allows us to validate a JavaScript object. If an object passes the validation, we can be sure that certain statements are true for the object, if it fails validation, the validation report will inform us, which statements do not hold. Schemas allow us to write simpler code, because certain facts about an object are given and do not have to be checked. This will also result in more readable and thus better maintainable JavaScript code. On the other hand if we do not use schemas, the assumptions that we make on the data structures we are working on are scattered in the code and the same assumption might checked more than once.
A Simple Schema
A Cerny schema is itself a JavaScript object. For the definition of a simple schema, we do not need any prerequisites. Let's define our first very simple schema.
Firefox = {
vendor: "Firefox"
};
This code defines a schema Firefox. For an object to
conform to that schema, it must have a member vendor
and the value of that member must be "Firefox". In other words,
the firefox schema is composed of one constraint
with the name vendor and the value
is the literal string "Firefox".
Performing validation
To perform validation our page requires CERNY.schema. This script provides the
functions CERNY.schema.validate
and CERNY.schema.isValid. The
code for validating the current browser against the Firefox schema
is as follows.
var report = CERNY.schema.validate(window.navigator, Firefox);
var isFirefox = CERNY.schema.isValid(report);
The first line performs the actual validation of the
window.navigator object against the
Firefox schema. The result of the validation is stored
in the variable report. The second line inspects the
result and returns true if no validation error occurred, and
otherwise false. The result of this inspection is stored in
isFirefox.
A Schema with several constraints
The Firefox schema is composed of one constraint. Let's take a look at a schema with more than one constraint. The following code defines a schema called Person.
Person = {
firstName: nonEmptyString,
middleName: optional(nonEmptyString),
lastName: nonEmptyString,
dateOfBirth: optional(pastIsoDate)
};
The Person schema does not only have more than one constraint, it
also defines it's constraints to be functions rather than a
literal string, as we know it from the Firefox schema. In the case
of firstName constraint the function is called
nonEmptyString. On validating an object, the
firstName member will be passed as a parameter to
nonEmptyString, which will return true
or an appropriate error message.
Now let us look at the following code to see the
Person schema at work.
var csp = {
firstName: "Charles",
middleName: "Sanders",
lastName: "Peirce",
dateOfBirth: "1839-09-10",
};
var report = CERNY.schema.validate(csp, Person);
Besides the nonEmptyString function we notice a
function called pastIsoDate which restricts the
possible values of dateOfBirth to a date of format
yyyy-mm-dd which lies in the past. Expressed in
JavaScript code:
pastIsoDate = function(value) {
var dateValue = Date._parse(value, CERNY.text.DateFormat.ISO);
if (dateValue && dateValue.getTime() < new Date().getTime()) {
return true;
}
return CERNY.schema.printValue(value) +
" must be an ISO date string (yyyy-mm-dd) for a past date.";
}
This code requires some functions which are provided by the Cerny.js lib, but the declaration of these dependencies is omitted for the sake of instructional clarity. The names of the functions will give the intermediate programmer a hint on what they do.
The pastIsoDate function takes a single parameter
value which will be bound during validation to the
value of the csp.dateOfBirth
("1839-09-10"). It returns true, if
value is a string referring to a date in the past in
a certain format and an error message otherwise. In the example
above pastIsoDate returns true. Returning the error
message in case of failure helps us to generate a more
understandable report. As another option we could just return
false, which would produce a generic error message.
This would force the programmer to lookup the function's
documentation or source code to gain an understanding of what is
wrong.
Validation reports
Sofar all example objects were confirming to their schema. Let's have a look at the following object:
var notAPerson = {
firstName: "",
middleName: "Sanders",
lastName: "Peirce",
dateOfBirth: "1839-9-10"
};
var report = CERNY.schema.validate(notAPerson, Person);
The object notAPerson does not fulfill all constraints of
the Person schema. It fails to have a non empty
firstName and the dateOfBirth value is
not a string in ISO date notation. The error report that is
returned by the validate call will look like this:
{
firstName: "'' (string) must be a non empty string.",
dateOfBirth: "'1839-9-10' (string) must be an ISO date string (yyyy-mm-dd) for a past date."
}
As we can easily see, it needs a human being to interpret the error message and act accordingly. But it tries to make this as easy as possible. The validation report follows the same same structure as the schema.
Defining constraints
We saw that the value of a constraint can either be a literal value or a function. There is two more options for specifying a constraint value. It can also be a regular expression or a schema. This leaves us all together with the following options:
- a literal value,
- a function,
- a regular expression and
- a schema.
If we look back at the Person schema, we also notice
that the middleName constraint is optional. The function
optional is included in the schema.js script of the
CERNY.js lib. It is a supporting function which makes it easier to
work with Schemas. There are some supporting functions already
included in the current version and more will be added in the
future when feedback becomes available.
A composite Schema
As mentioned above a constraint can also be defined as a schema. Let's look at the following example to see how that works and what it means.
arrayOf = CERNY.schema.arrayOf;
Family = {
mother: Person,
father: Person,
children: arrayOf(Person, 1)
};
The Family schema is composed of three constraints named
mother, father and
children, the first two being defined as
Person and the latter an array of items of type
Person, with at least one element.
The following family conforms to the Family schema.
familyOfCsp = {
mother: {
firstName: "Sarah",
middleName: "Hunt",
lastName: "Mills"
},
father: {
firstName: "Benjamin",
lastName: "Peirce"
},
children: [csp]
}
Using this in schemas
Within an object you can use the this keyword to
formulate constraints, that refer to more than one member of an
object. Let's consider the following Event schema
that checks whether the end date is after the start date.
Event = {
startDate: isoDate,
endDate: function (value) {
var endDate = Date._parse(value,
CERNY.text.DateFormat.ISO);
if (endDate === null) {
return "Must be a string of format (YYYY-MM-DD)";
}
var startDate = Date._parse(this.startDate,
CERNY.text.DateFormat.ISO);
// No need to check start for not null because of startDate
// constraint
if (endDate.before(startDate)) {
return "Must be after start date";
}
return true;
}
}
Here is an example of an instance that conforms to this schema:
lifeOfCsp = {
startDate: "1839-09-10",
endDate: "1914-04-19"
}
Crossing object boundaries
In the last section we learned how to validate statements that refer to other members of the same object. This works as well for statements that refer to a member of a sibling. Look at the following schema which ensures that the date of birth of a person is not before its mother's.
Child = {
firstName: nonEmptyString,
lastName: nonEmptyString,
dateOfBirth: function(value) {
// Date check of value omitted ...
var dobMother = Date._parse(this.mother.dateOfBirth,
CERNY.text.DateFormat.ISO);
if (dob.before(dobMother)) {
return "Must be after mother's date of birth";
}
},
mother: Person
}
Now how do we proceed, if we want to refer to a member that is
located in an object that resides in a different branch of the
object tree? For this purpose the validate function
augments the objects it is passed, with a temporary member called
_parent. With the help of this property it is
possible to access any location in the object tree.
Let's revisit our family example. Unlike in
the previous example the Person
schema does not have a mother member. But within the
family the Person should still adhere to the rule
that a child cannot be born before it's mother. This is where the
_parent property comes in handy.
Child = {
// ... just like Person
dateOfBirth: function(value) {
// Date check of value omitted ...
var dobMother = Date._parse(this._parent.mother.dateOfBirth,
CERNY.text.DateFormat.ISO);
// Compare it and print meaningful error message like in the
// previous example
}
};
Summary
The flexible, dynamic nature of JavaScript allows us to create
concise descriptions of how we picture a data structure that is
allowed to interact with our code. These descriptions can be
validated with the CERNY.schema package of the
Cerny.js library. This script offers a short and simple validation
function that creates a human readable report whether an object
conforms to our description. Constraints are composed of a name
and a value. The value can be a literal, a regular expression, a
function or a schema. The validation result is a JavaScript
object, which is empty, in case the object conforms to the
schema. Otherwise the error message is to be found at the same
position as the constraint, that is not violated.
To further improve the schema package, two functions need to be added to avoid duplication of code, one to extend Schemas, and one to chain constraint functions.

