Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

AVRO-485. JavaScript: Add validator. Contributed by Quinn Slack.

git-svn-id: https://svn.apache.org/repos/asf/avro/trunk@1395811 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information...
commit 1dbb07ac2f068dbc6b0bea963bf2abacab7cc434 1 parent 01f774f
@cutting cutting authored
View
1  BUILD.txt
@@ -10,6 +10,7 @@ The following packages must be installed before Avro can be built:
- C: gcc, cmake, asciidoc, source-highlight
- C++: cmake 2.8.4 or greater, g++, flex, bison, libboost-dev
- C#: mono-devel mono-gmcs nunit
+ - JavaScript: nodejs, npm
- Ruby: ruby 1.86 or greater, ruby-dev, gem, rake, echoe, yajl-ruby
- Apache Ant 1.7
- Apache Forrest 0.8 (for documentation)
View
2  CHANGES.txt
@@ -4,6 +4,8 @@ Trunk (not yet released)
NEW FEATURES
+ AVRO-485. JavaScript: Add validator. (Quinn Slack via cutting)
+
IMPROVEMENTS
AVRO-1169. Java: Reduce memory footprint of resolver.
View
5 build.sh
@@ -45,6 +45,7 @@ case "$target" in
(cd lang/c; ./build.sh test)
(cd lang/c++; ./build.sh test)
(cd lang/csharp; ./build.sh test)
+ (cd lang/js; ./build.sh test)
(cd lang/ruby; rake test)
(cd lang/php; ./build.sh test)
@@ -106,6 +107,8 @@ case "$target" in
(cd lang/csharp; ./build.sh dist)
+ (cd lang/js; ./build.sh dist)
+
(cd lang/ruby; rake dist)
(cd lang/php; ./build.sh dist)
@@ -152,6 +155,8 @@ case "$target" in
(cd lang/csharp; ./build.sh clean)
+ (cd lang/js; ./build.sh clean)
+
(cd lang/ruby; rake clean)
(cd lang/php; ./build.sh clean)
View
20 lang/js/README
@@ -0,0 +1,20 @@
+Avro Javascript
+===============================================================================
+
+Usage
+-------------------------------------------------------------------------------
+
+* *With node.js*: Install this avro-js npm package and then
+ use ```require('avro-js')``` in your program.
+
+* *Outside of node.js (e.g., browser)*: Include the validator.js file and the
+ [underscore.js library](http://underscorejs.org/).
+
+
+Running tests
+-------------------------------------------------------------------------------
+
+To run the included test suite using node.js:
+
+1. Install the npm dependencies by running ```npm install``` in the "js" dir.
+2. Run ```node_modules/grunt/bin/grunt test```.
View
40 lang/js/build.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e
+
+cd `dirname "$0"`
+
+case "$1" in
+ test)
+ npm install
+ node_modules/grunt/bin/grunt test
+ ;;
+
+ dist)
+ ;;
+
+ clean)
+ ;;
+
+ *)
+ echo "Usage: $0 {test|dist|clean}"
+ exit 1
+
+esac
+
+exit 0
View
52 lang/js/grunt.js
@@ -0,0 +1,52 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ test: {
+ files: ['test/**/*.js']
+ },
+ lint: {
+ files: ['grunt.js', 'lib/**/*.js', 'test/**/*.js']
+ },
+ watch: {
+ files: '<config:lint.files>',
+ tasks: 'lint:files test:files'
+ },
+ jshint: {
+ options: {
+ curly: true,
+ eqeqeq: true,
+ immed: true,
+ latedef: true,
+ newcap: true,
+ noarg: true,
+ sub: true,
+ undef: true,
+ boss: true,
+ eqnull: true,
+ node: true
+ },
+ globals: {
+ exports: true
+ }
+ }
+ });
+
+ grunt.registerTask('default', 'lint test');
+
+};
View
448 lang/js/lib/validator.js
@@ -0,0 +1,448 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+if (typeof require !== 'undefined') {
+ var _ = require("underscore");
+}
+
+var AvroSpec = {
+ PrimitiveTypes: ['null', 'boolean', 'int', 'long', 'float', 'double', 'bytes', 'string'],
+ ComplexTypes: ['record', 'enum', 'array', 'map', 'union', 'fixed']
+};
+AvroSpec.Types = AvroSpec.PrimitiveTypes.concat(AvroSpec.ComplexTypes);
+
+var InvalidSchemaError = function(msg) { return new Error('InvalidSchemaError: ' + msg); };
+var InvalidProtocolError = function(msg) { return new Error('InvalidProtocolError: ' + msg); };
+var ValidationError = function(msg) { return new Error('ValidationError: ' + msg); };
+var ProtocolValidationError = function(msg) { return new Error('ProtocolValidationError: ' + msg); };
+
+
+function Record(name, namespace, fields) {
+ function validateArgs(name, namespace, fields) {
+ if (!_.isString(name)) {
+ throw new InvalidSchemaError('Record name must be string');
+ }
+
+ if (!_.isNull(namespace) && !_.isUndefined(namespace) && !_.isString(namespace)) {
+ throw new InvalidSchemaError('Record namespace must be string or null');
+ }
+
+ if (!_.isArray(fields)) {
+ throw new InvalidSchemaError('Record name must be string');
+ }
+ }
+
+ validateArgs(name, namespace, fields);
+
+ this.name = name;
+ this.namespace = namespace;
+ this.fields = fields;
+}
+
+function makeFullyQualifiedTypeName(schema, namespace) {
+ var typeName = null;
+ if (_.isString(schema)) {
+ typeName = schema;
+ } else if (_.isObject(schema)) {
+ if (_.isString(schema.namespace)) {
+ namespace = schema.namespace;
+ }
+ if (_.isString(schema.name)) {
+ typeName = schema.name;
+ } else if (_.isString(schema.type)) {
+ typeName = schema.type;
+ }
+ } else {
+ throw new InvalidSchemaError('unable to determine fully qualified type name from schema ' + JSON.stringify(schema) + ' in namespace ' + namespace);
+ }
+
+ if (!_.isString(typeName)) {
+ throw new InvalidSchemaError('unable to determine type name from schema ' + JSON.stringify(schema) + ' in namespace ' + namespace);
+ }
+
+ if (typeName.indexOf('.') !== -1) {
+ return typeName;
+ } else if (_.contains(AvroSpec.PrimitiveTypes, typeName)) {
+ return typeName;
+ } else if (_.isString(namespace)) {
+ return namespace + '.' + typeName;
+ } else {
+ return typeName;
+ }
+}
+
+function Union(typeSchemas, namespace) {
+ this.branchNames = function() {
+ return _.map(typeSchemas, function(typeSchema) { return makeFullyQualifiedTypeName(typeSchema, namespace); });
+ };
+
+ function validateArgs(typeSchemas) {
+ if (!_.isArray(typeSchemas) || _.isEmpty(typeSchemas)) {
+ throw new InvalidSchemaError('Union must have at least 1 branch');
+ }
+ }
+
+ validateArgs(typeSchemas);
+
+ this.typeSchemas = typeSchemas;
+ this.namespace = namespace;
+}
+
+function Enum(symbols) {
+
+ function validateArgs(symbols) {
+ if (!_.isArray(symbols)) {
+ throw new InvalidSchemaError('Enum must have array of symbols, got ' + JSON.stringify(symbols));
+ }
+ if (!_.all(symbols, function(symbol) { return _.isString(symbol); })) {
+ throw new InvalidSchemaError('Enum symbols must be strings, got ' + JSON.stringify(symbols));
+ }
+ }
+
+ validateArgs(symbols);
+
+ this.symbols = symbols;
+}
+
+function AvroArray(itemSchema) {
+
+ function validateArgs(itemSchema) {
+ if (_.isNull(itemSchema) || _.isUndefined(itemSchema)) {
+ throw new InvalidSchemaError('Array "items" schema should not be null or undefined');
+ }
+ }
+
+ validateArgs(itemSchema);
+
+ this.itemSchema = itemSchema;
+}
+
+function Map(valueSchema) {
+
+ function validateArgs(valueSchema) {
+ if (_.isNull(valueSchema) || _.isUndefined(valueSchema)) {
+ throw new InvalidSchemaError('Map "values" schema should not be null or undefined');
+ }
+ }
+
+ validateArgs(valueSchema);
+
+ this.valueSchema = valueSchema;
+}
+
+function Field(name, schema) {
+ function validateArgs(name, schema) {
+ if (!_.isString(name)) {
+ throw new InvalidSchemaError('Field name must be string');
+ }
+ }
+
+ this.name = name;
+ this.schema = schema;
+}
+
+function Primitive(type) {
+ function validateArgs(type) {
+ if (!_.isString(type)) {
+ throw new InvalidSchemaError('Primitive type name must be a string');
+ }
+
+ if (!_.contains(AvroSpec.PrimitiveTypes, type)) {
+ throw new InvalidSchemaError('Primitive type must be one of: ' + JSON.stringify(AvroSpec.PrimitiveTypes) + '; got ' + type);
+ }
+ }
+
+ validateArgs(type);
+
+ this.type = type;
+}
+
+function Validator(schema, namespace, namedTypes) {
+ this.validate = function(obj) {
+ return _validate(this.schema, obj);
+ };
+
+ var _validate = function(schema, obj) {
+ if (schema instanceof Record) {
+ return _validateRecord(schema, obj);
+ } else if (schema instanceof Union) {
+ return _validateUnion(schema, obj);
+ } else if (schema instanceof Enum) {
+ return _validateEnum(schema, obj);
+ } else if (schema instanceof AvroArray) {
+ return _validateArray(schema, obj);
+ } else if (schema instanceof Map) {
+ return _validateMap(schema, obj);
+ } else if (schema instanceof Primitive) {
+ return _validatePrimitive(schema, obj);
+ } else {
+ throw new InvalidSchemaError('validation not yet implemented: ' + JSON.stringify(schema));
+ }
+ };
+
+ var _validateRecord = function(schema, obj) {
+ if (!_.isObject(obj) || _.isArray(obj)) {
+ throw new ValidationError('Expected record Javascript type to be non-array object, got ' + JSON.stringify(obj));
+ }
+
+ var schemaFieldNames = _.pluck(schema.fields, 'name').sort();
+ var objFieldNames = _.keys(obj).sort();
+ if (!_.isEqual(schemaFieldNames, objFieldNames)) {
+ throw new ValidationError('Expected record fields ' + JSON.stringify(schemaFieldNames) + '; got ' + JSON.stringify(objFieldNames));
+ }
+
+ return _.all(schema.fields, function(field) {
+ return _validate(field.schema, obj[field.name]);
+ });
+ };
+
+ var _validateUnion = function(schema, obj) {
+ if (_.isObject(obj)) {
+ if (_.isArray(obj)) {
+ throw new ValidationError('Expected union Javascript type to be non-array object (or null), got ' + JSON.stringify(obj));
+ } else if (_.size(obj) !== 1) {
+ throw new ValidationError('Expected union Javascript object to be object with exactly 1 key (or null), got ' + JSON.stringify(obj));
+ } else {
+ var unionBranch = _.keys(obj)[0];
+ if (unionBranch === "") {
+ throw new ValidationError('Expected union Javascript object to contain non-empty string branch, got ' + JSON.stringify(obj));
+ }
+ if (_.contains(schema.branchNames(), unionBranch)) {
+ return true;
+ } else {
+ throw new ValidationError('Expected union branch to be one of ' + JSON.stringify(schema.branchNames()) + '; got ' + JSON.stringify(unionBranch));
+ }
+ }
+ } else if (_.isNull(obj)) {
+ if (_.contains(schema.branchNames(), 'null')) {
+ return true;
+ } else {
+ throw new ValidationError('Expected union branch to be one of ' + JSON.stringify(schema.branchNames()) + '; got ' + JSON.stringify(obj));
+ }
+ } else {
+ throw new ValidationError('Expected union Javascript object to be non-array object of size 1 or null, got ' + JSON.stringify(obj));
+ }
+ };
+
+ var _validateEnum = function(schema, obj) {
+ if (_.isString(obj)) {
+ if (_.contains(schema.symbols, obj)) {
+ return true;
+ } else {
+ throw new ValidationError('Expected enum value to be one of ' + JSON.stringify(schema.symbols) + '; got ' + JSON.stringify(obj));
+ }
+ } else {
+ throw new ValidationError('Expected enum Javascript object to be string, got ' + JSON.stringify(obj));
+ }
+ };
+
+ var _validateArray = function(schema, obj) {
+ if (_.isArray(obj)) {
+ return _.all(obj, function(member) { return _validate(schema.itemSchema, member); });
+ } else {
+ throw new ValidationError('Expected array Javascript object to be array, got ' + JSON.stringify(obj));
+ }
+ };
+
+ var _validateMap = function(schema, obj) {
+ if (_.isObject(obj) && !_.isArray(obj)) {
+ return _.all(obj, function(value) { return _validate(schema.valueSchema, value); });
+ } else if (_.isArray(obj)) {
+ throw new ValidationError('Expected map Javascript object to be non-array object, got array ' + JSON.stringify(obj));
+ } else {
+ throw new ValidationError('Expected map Javascript object to be non-array object, got ' + JSON.stringify(obj));
+ }
+ };
+
+ var _validatePrimitive = function(schema, obj) {
+ switch (schema.type) {
+ case 'null':
+ if (_.isNull(obj) || _.isUndefined(obj)) {
+ return true;
+ } else {
+ throw new ValidationError('Expected Javascript null or undefined for Avro null, got ' + JSON.stringify(obj));
+ }
+ break;
+ case 'boolean':
+ if (_.isBoolean(obj)) {
+ return true;
+ } else {
+ throw new ValidationError('Expected Javascript boolean for Avro boolean, got ' + JSON.stringify(obj));
+ }
+ break;
+ case 'int':
+ if (_.isNumber(obj) && Math.floor(obj) === obj && Math.abs(obj) <= Math.pow(2, 31)) {
+ return true;
+ } else {
+ throw new ValidationError('Expected Javascript int32 number for Avro int, got ' + JSON.stringify(obj));
+ }
+ break;
+ case 'long':
+ if (_.isNumber(obj) && Math.floor(obj) === obj && Math.abs(obj) <= Math.pow(2, 63)) {
+ return true;
+ } else {
+ throw new ValidationError('Expected Javascript int64 number for Avro long, got ' + JSON.stringify(obj));
+ }
+ break;
+ case 'float':
+ if (_.isNumber(obj)) { // TODO: handle NaN?
+ return true;
+ } else {
+ throw new ValidationError('Expected Javascript float number for Avro float, got ' + JSON.stringify(obj));
+ }
+ break;
+ case 'double':
+ if (_.isNumber(obj)) { // TODO: handle NaN?
+ return true;
+ } else {
+ throw new ValidationError('Expected Javascript double number for Avro double, got ' + JSON.stringify(obj));
+ }
+ break;
+ case 'bytes':
+ throw new InvalidSchemaError('not yet implemented: ' + schema.type);
+ case 'string':
+ if (_.isString(obj)) { // TODO: handle NaN?
+ return true;
+ } else {
+ throw new ValidationError('Expected Javascript string for Avro string, got ' + JSON.stringify(obj));
+ }
+ break;
+ default:
+ throw new InvalidSchemaError('unrecognized primitive type: ' + schema.type);
+ }
+ };
+
+ // TODO: namespace handling is rudimentary. multiple namespaces within a certain nested schema definition
+ // are probably buggy.
+ var _namedTypes = namedTypes || {};
+ var _saveNamedType = function(fullyQualifiedTypeName, schema) {
+ if (_.has(_namedTypes, fullyQualifiedTypeName)) {
+ if (!_.isEqual(_namedTypes[fullyQualifiedTypeName], schema)) {
+ throw new InvalidSchemaError('conflicting definitions for type ' + fullyQualifiedTypeName + ': ' + JSON.stringify(_namedTypes[fullyQualifiedTypeName]) + ' and ' + JSON.stringify(schema));
+ }
+ } else {
+ _namedTypes[fullyQualifiedTypeName] = schema;
+ }
+ };
+
+ var _lookupTypeByFullyQualifiedName = function(fullyQualifiedTypeName) {
+ if (_.has(_namedTypes, fullyQualifiedTypeName)) {
+ return _namedTypes[fullyQualifiedTypeName];
+ } else {
+ return null;
+ }
+ };
+
+ var _parseNamedType = function(schema, namespace) {
+ if (_.contains(AvroSpec.PrimitiveTypes, schema)) {
+ return new Primitive(schema);
+ } else if (!_.isNull(_lookupTypeByFullyQualifiedName(makeFullyQualifiedTypeName(schema, namespace)))) {
+ return _lookupTypeByFullyQualifiedName(makeFullyQualifiedTypeName(schema, namespace));
+ } else {
+ throw new InvalidSchemaError('unknown type name: ' + JSON.stringify(schema) + '; known type names are ' + JSON.stringify(_.keys(_namedTypes)));
+ }
+ };
+
+ var _parseSchema = function(schema, parentSchema, namespace) {
+ if (_.isNull(schema) || _.isUndefined(schema)) {
+ throw new InvalidSchemaError('schema is null, in parentSchema: ' + JSON.stringify(parentSchema));
+ } else if (_.isString(schema)) {
+ return _parseNamedType(schema, namespace);
+ } else if (_.isObject(schema) && !_.isArray(schema)) {
+ if (schema.type === 'record') {
+ var newRecord = new Record(schema.name, schema.namespace, _.map(schema.fields, function(field) {
+ return new Field(field.name, _parseSchema(field.type, schema, schema.namespace || namespace));
+ }));
+ _saveNamedType(makeFullyQualifiedTypeName(schema, namespace), newRecord);
+ return newRecord;
+ } else if (schema.type === 'enum') {
+ if (_.has(schema, 'symbols')) {
+ var newEnum = new Enum(schema.symbols);
+ _saveNamedType(makeFullyQualifiedTypeName(schema, namespace), newEnum);
+ return newEnum;
+ } else {
+ throw new InvalidSchemaError('enum must specify symbols, got ' + JSON.stringify(schema));
+ }
+ } else if (schema.type === 'array') {
+ if (_.has(schema, 'items')) {
+ return new AvroArray(_parseSchema(schema.items, schema, namespace));
+ } else {
+ throw new InvalidSchemaError('array must specify "items" schema, got ' + JSON.stringify(schema));
+ }
+ } else if (schema.type === 'map') {
+ if (_.has(schema, 'values')) {
+ return new Map(_parseSchema(schema.values, schema, namespace));
+ } else {
+ throw new InvalidSchemaError('map must specify "values" schema, got ' + JSON.stringify(schema));
+ }
+ } else if (_.has(schema, 'type') && _.contains(AvroSpec.PrimitiveTypes, schema.type)) {
+ return _parseNamedType(schema.type, namespace);
+ } else {
+ throw new InvalidSchemaError('not yet implemented: ' + schema.type);
+ }
+ } else if (_.isArray(schema)) {
+ if (_.isEmpty(schema)) {
+ throw new InvalidSchemaError('unions must have at least 1 branch');
+ }
+ var branchTypes = _.map(schema, function(branchType) { return _parseSchema(branchType, schema, namespace); });
+ return new Union(branchTypes, namespace);
+ } else {
+ throw new InvalidSchemaError('unexpected Javascript type for schema: ' + (typeof schema));
+ }
+ };
+
+ this.rawSchema = schema;
+ this.schema = _parseSchema(schema, null, namespace);
+}
+
+Validator.validate = function(schema, obj) {
+ return (new Validator(schema)).validate(obj);
+};
+
+function ProtocolValidator(protocol) {
+ this.validate = function(typeName, obj) {
+ var fullyQualifiedTypeName = makeFullyQualifiedTypeName(typeName, protocol.namespace);
+ if (!_.has(_typeSchemaValidators, fullyQualifiedTypeName)) {
+ throw new ProtocolValidationError('Protocol does not contain definition for type ' + JSON.stringify(fullyQualifiedTypeName) + ' (fully qualified from input "' + typeName + '"); known types are ' + JSON.stringify(_.keys(_typeSchemaValidators)));
+ }
+ return _typeSchemaValidators[fullyQualifiedTypeName].validate(obj);
+ };
+
+ var _typeSchemaValidators = {};
+ var _initSchemaValidators = function(protocol) {
+ var namedTypes = {};
+ if (!_.has(protocol, 'protocol') || !_.isString(protocol.protocol)) {
+ throw new InvalidProtocolError('Protocol must contain a "protocol" attribute with a string value');
+ }
+ if (_.isArray(protocol.types)) {
+ _.each(protocol.types, function(typeSchema) {
+ var schemaValidator = new Validator(typeSchema, protocol.namespace, namedTypes);
+ var fullyQualifiedTypeName = makeFullyQualifiedTypeName(typeSchema, protocol.namespace);
+ _typeSchemaValidators[fullyQualifiedTypeName] = schemaValidator;
+ });
+ }
+ };
+
+ _initSchemaValidators(protocol);
+}
+
+ProtocolValidator.validate = function(protocol, typeName, obj) {
+ return (new ProtocolValidator(protocol)).validate(typeName, obj);
+};
+
+if (typeof exports !== 'undefined') {
+ exports['Validator'] = Validator;
+ exports['ProtocolValidator'] = ProtocolValidator;
+}
View
34 lang/js/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "avro-js",
+ "version": "0.0.1",
+ "author": "Avro Developers <dev@avro.apache.org>",
+ "description": "Avro validator for Javascript",
+ "contributors": [
+ {
+ "name": "Quinn Slack",
+ "email": "sqs@cs.stanford.edu"
+ }
+ ],
+ "scripts": {
+ "test": "grunt test"
+ },
+ "repository": {
+ "type": "svn",
+ "url": "http://svn.apache.org/repos/asf/avro/trunk/lang/js/"
+ },
+ "keywords": [
+ "avro",
+ "json"
+ ],
+ "dependencies" : {
+ "underscore" : "*"
+ },
+ "devDependencies" : {
+ "grunt" : "*"
+ },
+ "noAnalyze": true,
+ "license": "Apache",
+ "engine": {
+ "node": ">=0.4"
+ }
+}
View
525 lang/js/test/validator.js
@@ -0,0 +1,525 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+var validator = require('../lib/validator.js');
+var Validator = validator.Validator;
+var ProtocolValidator = validator.ProtocolValidator;
+
+exports['test'] = {
+ setUp: function(done) {
+ done();
+ },
+ 'nonexistent/null/undefined': function(test) {
+ test.throws(function() { return new Validator(); });
+ test.throws(function() { return new Validator(null); });
+ test.throws(function() { return new Validator(undefined); });
+ test.done();
+ },
+ 'unrecognized primitive type name': function(test) {
+ test.throws(function() { return new Validator('badtype'); });
+ test.done();
+ },
+ 'invalid schema javascript type': function(test) {
+ test.throws(function() { return new Validator(123); });
+ test.throws(function() { return new Validator(function() { }); });
+ test.done();
+ },
+
+ // Primitive types
+ 'null': function(test) {
+ test.ok(Validator.validate('null', null));
+ test.ok(Validator.validate('null', undefined));
+ test.throws(function() { Validator.validate('null', 1); });
+ test.throws(function() { Validator.validate('null', 'a'); });
+ test.done();
+ },
+ 'boolean': function(test) {
+ test.ok(Validator.validate('boolean', true));
+ test.ok(Validator.validate('boolean', false));
+ test.throws(function() { Validator.validate('boolean', null); });
+ test.throws(function() { Validator.validate('boolean', 1); });
+ test.throws(function() { Validator.validate('boolean', 'a'); });
+ test.done();
+ },
+ 'int': function(test) {
+ test.ok(Validator.validate('int', 1));
+ test.ok(Validator.validate('long', Math.pow(2, 31) - 1));
+ test.throws(function() { Validator.validate('int', 1.5); });
+ test.throws(function() { Validator.validate('int', Math.pow(2, 40)); });
+ test.throws(function() { Validator.validate('int', null); });
+ test.throws(function() { Validator.validate('int', 'a'); });
+ test.done();
+ },
+ 'long': function(test) {
+ test.ok(Validator.validate('long', 1));
+ test.ok(Validator.validate('long', Math.pow(2, 63) - 1));
+ test.throws(function() { Validator.validate('long', 1.5); });
+ test.throws(function() { Validator.validate('long', Math.pow(2, 70)); });
+ test.throws(function() { Validator.validate('long', null); });
+ test.throws(function() { Validator.validate('long', 'a'); });
+ test.done();
+ },
+ 'float': function(test) {
+ test.ok(Validator.validate('float', 1));
+ test.ok(Validator.validate('float', 1.5));
+ test.throws(function() { Validator.validate('float', 'a'); });
+ test.throws(function() { Validator.validate('float', null); });
+ test.done();
+ },
+ 'double': function(test) {
+ test.ok(Validator.validate('double', 1));
+ test.ok(Validator.validate('double', 1.5));
+ test.throws(function() { Validator.validate('double', 'a'); });
+ test.throws(function() { Validator.validate('double', null); });
+ test.done();
+ },
+ 'bytes': function(test) {
+ // not implemented yet
+ test.throws(function() { Validator.validate('bytes', 1); });
+ test.done();
+ },
+ 'string': function(test) {
+ test.ok(Validator.validate('string', 'a'));
+ test.throws(function() { Validator.validate('string', 1); });
+ test.throws(function() { Validator.validate('string', null); });
+ test.done();
+ },
+
+ // Records
+ 'empty-record': function(test) {
+ var schema = {type: 'record', name: 'EmptyRecord', fields: []};
+ test.ok(Validator.validate(schema, {}));
+ test.throws(function() { Validator.validate(schema, 1); });
+ test.throws(function() { Validator.validate(schema, null); });
+ test.throws(function() { Validator.validate(schema, 'a'); });
+ test.done();
+ },
+ 'record-with-string': function(test) {
+ var schema = {type: 'record', name: 'EmptyRecord', fields: [{name: 'stringField', type: 'string'}]};
+ test.ok(Validator.validate(schema, {stringField: 'a'}));
+ test.throws(function() { Validator.validate(schema, {}); });
+ test.throws(function() { Validator.validate(schema, {stringField: 1}); });
+ test.throws(function() { Validator.validate(schema, {stringField: []}); });
+ test.throws(function() { Validator.validate(schema, {stringField: {}}); });
+ test.throws(function() { Validator.validate(schema, {stringField: null}); });
+ test.throws(function() { Validator.validate(schema, {stringField: 'a', unexpectedField: 'a'}); });
+ test.done();
+ },
+ 'record-with-string-and-number': function(test) {
+ var schema = {type: 'record', name: 'EmptyRecord', fields: [{name: 'stringField', type: 'string'}, {name: 'intField', type: 'int'}]};
+ test.ok(Validator.validate(schema, {stringField: 'a', intField: 1}));
+ test.throws(function() { Validator.validate(schema, {}); });
+ test.throws(function() { Validator.validate(schema, {stringField: 'a'}); });
+ test.throws(function() { Validator.validate(schema, {intField: 1}); });
+ test.throws(function() { Validator.validate(schema, {stringField: 'a', intField: 1, unexpectedField: 'a'}); });
+ test.done();
+ },
+ 'nested-record-with-namespace-relative': function(test) {
+ var schema = {type: 'record', namespace: 'x.y.z', name: 'RecordA', fields: [{name: 'recordBField1', type: ['null', {type: 'record', name: 'RecordB', fields: []}]}, {name: 'recordBField2', type: 'RecordB'}]};
+ test.ok(Validator.validate(schema, {recordBField1: null, recordBField2: {}}));
+ test.ok(Validator.validate(schema, {recordBField1: {'x.y.z.RecordB': {}}, recordBField2: {}}));
+ test.throws(function() { Validator.validate(schema, {}); });
+ test.throws(function() { Validator.validate(schema, {recordBField1: null}); });
+ test.throws(function() { Validator.validate(schema, {recordBField2: {}}); });
+ test.throws(function() { Validator.validate(schema, {recordBField1: {'RecordB': {}}, recordBField2: {}}); });
+ test.done();
+ },
+ 'nested-record-with-namespace-absolute': function(test) {
+ var schema = {type: 'record', namespace: 'x.y.z', name: 'RecordA', fields: [{name: 'recordBField1', type: ['null', {type: 'record', name: 'RecordB', fields: []}]}, {name: 'recordBField2', type: 'x.y.z.RecordB'}]};
+ test.ok(Validator.validate(schema, {recordBField1: null, recordBField2: {}}));
+ test.ok(Validator.validate(schema, {recordBField1: {'x.y.z.RecordB': {}}, recordBField2: {}}));
+ test.throws(function() { Validator.validate(schema, {}); });
+ test.throws(function() { Validator.validate(schema, {recordBField1: null}); });
+ test.throws(function() { Validator.validate(schema, {recordBField2: {}}); });
+ test.throws(function() { Validator.validate(schema, {recordBField1: {'RecordB': {}}, recordBField2: {}}); });
+ test.done();
+ },
+
+
+ // Enums
+ 'enum': function(test) {
+ var schema = {type: 'enum', name: 'Colors', symbols: ['Red', 'Blue']};
+ test.ok(Validator.validate(schema, 'Red'));
+ test.ok(Validator.validate(schema, 'Blue'));
+ test.throws(function() { Validator.validate(schema, null); });
+ test.throws(function() { Validator.validate(schema, undefined); });
+ test.throws(function() { Validator.validate(schema, 'NotAColor'); });
+ test.throws(function() { Validator.validate(schema, ''); });
+ test.throws(function() { Validator.validate(schema, {}); });
+ test.throws(function() { Validator.validate(schema, []); });
+ test.throws(function() { Validator.validate(schema, 1); });
+ test.done();
+ },
+
+ // Unions
+ 'union': function(test) {
+ var schema = ['string', 'int'];
+ test.ok(Validator.validate(schema, {string: 'a'}));
+ test.ok(Validator.validate(schema, {int: 1}));
+ test.throws(function() { Validator.validate(schema, null); });
+ test.throws(function() { Validator.validate(schema, undefined); });
+ test.throws(function() { Validator.validate(schema, 'a'); });
+ test.throws(function() { Validator.validate(schema, 1); });
+ test.throws(function() { Validator.validate(schema, {string: 'a', int: 1}); });
+ test.throws(function() { Validator.validate(schema, []); });
+ test.done();
+ },
+
+ 'union with null': function(test) {
+ var schema = ['string', 'null'];
+ test.ok(Validator.validate(schema, {string: 'a'}));
+ test.ok(Validator.validate(schema, null));
+ test.throws(function() { Validator.validate(schema, undefined); });
+ test.done();
+ },
+
+ 'nested union': function(test) {
+ var schema = ['string', {type: 'int'}];
+ test.ok(Validator.validate(schema, {string: 'a'}));
+ test.ok(Validator.validate(schema, {int: 1}));
+ test.throws(function() { Validator.validate(schema, null); });
+ test.throws(function() { Validator.validate(schema, undefined); });
+ test.throws(function() { Validator.validate(schema, 'a'); });
+ test.throws(function() { Validator.validate(schema, 1); });
+ test.throws(function() { Validator.validate(schema, {string: 'a', int: 1}); });
+ test.throws(function() { Validator.validate(schema, []); });
+ test.done();
+ },
+
+ // Arrays
+ 'array': function(test) {
+ var schema = {type: "array", items: "string"};
+ test.ok(Validator.validate(schema, []));
+ test.ok(Validator.validate(schema, ["a"]));
+ test.ok(Validator.validate(schema, ["a", "b", "a"]));
+ test.throws(function() { Validator.validate(schema, null); });
+ test.throws(function() { Validator.validate(schema, undefined); });
+ test.throws(function() { Validator.validate(schema, 'a'); });
+ test.throws(function() { Validator.validate(schema, 1); });
+ test.throws(function() { Validator.validate(schema, {}); });
+ test.throws(function() { Validator.validate(schema, {"1": "a"}); });
+ test.throws(function() { Validator.validate(schema, {1: "a"}); });
+ test.throws(function() { Validator.validate(schema, {1: "a", "b": undefined}); });
+ test.throws(function() { var a = {}; a[0] = "a"; Validator.validate(schema, a); });
+ test.throws(function() { Validator.validate(schema, [1]); });
+ test.throws(function() { Validator.validate(schema, [1, "a"]); });
+ test.throws(function() { Validator.validate(schema, ["a", 1]); });
+ test.throws(function() { Validator.validate(schema, [null, 1]); });
+ test.done();
+ },
+
+ // Maps
+ 'map': function(test) {
+ var schema = {type: "map", values: "string"};
+ test.ok(Validator.validate(schema, {}));
+ test.ok(Validator.validate(schema, {"a": "b"}));
+ test.ok(Validator.validate(schema, {"a": "b", "c": "d"}));
+ test.throws(function() { Validator.validate(schema, null); });
+ test.throws(function() { Validator.validate(schema, undefined); });
+ test.throws(function() { Validator.validate(schema, 'a'); });
+ test.throws(function() { Validator.validate(schema, 1); });
+ test.throws(function() { Validator.validate(schema, [1]); });
+ test.throws(function() { Validator.validate(schema, {"a": 1}); });
+ test.throws(function() { Validator.validate(schema, {"a": "b", "c": 1}); });
+ test.done();
+ },
+
+ // Protocols
+ 'protocol': function(test) {
+ var protocol = {protocol: "Protocol1", namespace: "x.y.z", types: [
+ {type: "record", name: "RecordA", fields: []},
+ {type: "record", name: "RecordB", fields: [{name: "recordAField", type: "RecordA"}]}
+ ]};
+ test.ok(ProtocolValidator.validate(protocol, 'RecordA', {}));
+ test.ok(ProtocolValidator.validate(protocol, 'x.y.z.RecordA', {}));
+ test.ok(ProtocolValidator.validate(protocol, 'RecordB', {recordAField: {}}));
+ test.ok(ProtocolValidator.validate(protocol, 'x.y.z.RecordB', {recordAField: {}}));
+ test.throws(function() { ProtocolValidator.validate(protocol, 'RecordDoesNotExist', {}); });
+ test.throws(function() { ProtocolValidator.validate(protocol, 'RecordDoesNotExist', null); });
+ test.throws(function() { ProtocolValidator.validate(protocol, 'RecordB', {}); });
+ test.throws(function() { ProtocolValidator.validate(protocol, null, {}); });
+ test.throws(function() { ProtocolValidator.validate(protocol, '', {}); });
+ test.throws(function() { ProtocolValidator.validate(protocol, {}, {}); });
+ test.done();
+ },
+
+ // Samples
+ 'link': function(test) {
+ var schema = {
+ "type" : "record",
+ "name" : "Bundle",
+ "namespace" : "aa.bb.cc",
+ "fields" : [ {
+ "name" : "id",
+ "type" : "string"
+ }, {
+ "name" : "type",
+ "type" : "string"
+ }, {
+ "name" : "data_",
+ "type" : [ "null", {
+ "type" : "record",
+ "name" : "LinkData",
+ "fields" : [ {
+ "name" : "address",
+ "type" : "string"
+ }, {
+ "name" : "title",
+ "type" : [ "null", "string" ],
+ "default" : null
+ }, {
+ "name" : "excerpt",
+ "type" : [ "null", "string" ],
+ "default" : null
+ }, {
+ "name" : "image",
+ "type" : [ "null", {
+ "type" : "record",
+ "name" : "Image",
+ "fields" : [ {
+ "name" : "url",
+ "type" : "string"
+ }, {
+ "name" : "width",
+ "type" : "int"
+ }, {
+ "name" : "height",
+ "type" : "int"
+ } ]
+ } ],
+ "default" : null
+ }, {
+ "name" : "meta",
+ "type" : {
+ "type" : "map",
+ "values" : "string"
+ },
+ "default" : {
+ }
+ } ]
+ } ],
+ "default" : null
+ }, {
+ "name" : "atoms_",
+ "type" : {
+ "type" : "map",
+ "values" : {
+ "type" : "map",
+ "values" : {
+ "type" : "record",
+ "name" : "Atom",
+ "fields" : [ {
+ "name" : "index_",
+ "type" : {
+ "type" : "record",
+ "name" : "AtomIndex",
+ "fields" : [ {
+ "name" : "type_",
+ "type" : "string"
+ }, {
+ "name" : "id",
+ "type" : "string"
+ } ]
+ }
+ }, {
+ "name" : "data_",
+ "type" : [ "LinkData" ]
+ } ]
+ }
+ }
+ },
+ "default" : {
+ }
+ }, {
+ "name" : "meta_",
+ "type" : {
+ "type" : "record",
+ "name" : "BundleMetadata",
+ "fields" : [ {
+ "name" : "date",
+ "type" : "long",
+ "default" : 0
+ }, {
+ "name" : "members",
+ "type" : {
+ "type" : "map",
+ "values" : "string"
+ },
+ "default" : {
+ }
+ }, {
+ "name" : "tags",
+ "type" : {
+ "type" : "map",
+ "values" : "string"
+ },
+ "default" : {
+ }
+ }, {
+ "name" : "meta",
+ "type" : {
+ "type" : "map",
+ "values" : "string"
+ },
+ "default" : {
+ }
+ }, {
+ "name" : "votes",
+ "type" : {
+ "type" : "map",
+ "values" : {
+ "type" : "record",
+ "name" : "VoteData",
+ "fields" : [ {
+ "name" : "date",
+ "type" : "long"
+ }, {
+ "name" : "userName",
+ "type" : [ "null", "string" ],
+ "default" : null
+ }, {
+ "name" : "direction",
+ "type" : {
+ "type" : "enum",
+ "name" : "VoteDirection",
+ "symbols" : [ "Up", "Down", "None" ]
+ }
+ } ]
+ }
+ },
+ "default" : {
+ }
+ }, {
+ "name" : "views",
+ "type" : {
+ "type" : "map",
+ "values" : {
+ "type" : "record",
+ "name" : "ViewData",
+ "fields" : [ {
+ "name" : "userName",
+ "type" : "string"
+ }, {
+ "name" : "count",
+ "type" : "int"
+ } ]
+ }
+ },
+ "default" : {
+ }
+ }, {
+ "name" : "relevance",
+ "type" : {
+ "type" : "map",
+ "values" : "string"
+ },
+ "default" : {
+ }
+ }, {
+ "name" : "clicks",
+ "type" : {
+ "type" : "map",
+ "values" : "string"
+ },
+ "default" : {
+ }
+ } ]
+ }
+ } ]
+ };
+ var okObj = {
+ "id": "https://github.com/sqs/akka-kryo-serialization/subscription",
+ "type": "link",
+ "data_": {
+ "aa.bb.cc.LinkData": {
+ "address": "https://github.com/sqs/akka-kryo-serialization/subscription",
+ "title": {
+ "string": "Sign in · GitHub"
+ },
+ "excerpt": {
+ "string": "Signup and Pricing Explore GitHub Features Blog Sign in Sign in (Pricing and Signup) Username or Email Password (forgot password) GitHub Links GitHub About Blog Feat"
+ },
+ "image": {
+ "aa.bb.cc.Image": {
+ "url": "https://a248.e.akamai.net/assets.github.com/images/modules/header/logov7@4x.png?1340659561",
+ "width": 280,
+ "height": 120
+ }
+ },
+ "meta": {}
+ }
+ },
+ "atoms_": {
+ "link": {
+ "https://github.com/sqs/akka-kryo-serialization/subscription": {
+ "index_": {
+ "type_": "link",
+ "id": "https://github.com/sqs/akka-kryo-serialization/subscription"
+ },
+ "data_": {
+ "aa.bb.cc.LinkData": {
+ "address": "https://github.com/sqs/akka-kryo-serialization/subscription",
+ "title": {
+ "string": "Sign in · GitHub"
+ },
+ "excerpt": {
+ "string": "Signup and Pricing Explore GitHub Features Blog Sign in Sign in (Pricing and Signup) Username or Email Password (forgot password) GitHub Links GitHub About Blog Feat"
+ },
+ "image": {
+ "aa.bb.cc.Image": {
+ "url": "https://a248.e.akamai.net/assets.github.com/images/modules/header/logov7@4x.png?1340659561",
+ "width": 280,
+ "height": 120
+ }
+ },
+ "meta": {}
+ }
+ }
+ }
+ }
+ },
+ "meta_": {
+ "date": 1345537530000,
+ "members": {
+ "a@a.com": "1"
+ },
+ "tags": {
+ "blue": "1"
+ },
+ "meta": {},
+ "votes": {},
+ "views": {
+ "a@a.com": {
+ "userName": "John Smith",
+ "count": 100
+ }
+ },
+ "relevance": {
+ "a@a.com": "1",
+ "b@b.com": "2"
+ },
+ "clicks": {}
+ }
+ };
+
+ test.ok(Validator.validate(schema, okObj));
+
+ var badObj = okObj; // no deep copy since we won't reuse okObj
+ badObj.meta_.clicks['a@a.com'] = 123;
+ test.throws(function() { Validator.validate(schema, badObj); });
+
+ test.done();
+ }
+};
Please sign in to comment.
Something went wrong with that request. Please try again.