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 its constraints to be functions rather than a literal string, as we know it from the Firefox schema. In the case of the 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 returns 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 look up the function's documentation or source code to gain an understanding of what is wrong.

Validation reports

So far all example objects confirmed 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 structure as the schema.

Defining constraints

We have seen that the value of a constraint can either be a literal value or a function. There are two more options for specifying a constraint value. It can also be a regular expression or a schema. Altogether this leaves us 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 as 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 have 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 its 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.

API Documentation for version