You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@avro.apache.org by cu...@apache.org on 2012/10/09 01:26:08 UTC

svn commit: r1395811 - in /avro/trunk: BUILD.txt CHANGES.txt build.sh lang/js/ lang/js/README lang/js/build.sh lang/js/grunt.js lang/js/lib/ lang/js/lib/validator.js lang/js/package.json lang/js/test/ lang/js/test/validator.js

Author: cutting
Date: Mon Oct  8 23:26:07 2012
New Revision: 1395811

URL: http://svn.apache.org/viewvc?rev=1395811&view=rev
Log:
AVRO-485.  JavaScript: Add validator.  Contributed by Quinn Slack.

Added:
    avro/trunk/lang/js/   (with props)
    avro/trunk/lang/js/README   (with props)
    avro/trunk/lang/js/build.sh   (with props)
    avro/trunk/lang/js/grunt.js   (with props)
    avro/trunk/lang/js/lib/
    avro/trunk/lang/js/lib/validator.js   (with props)
    avro/trunk/lang/js/package.json
    avro/trunk/lang/js/test/
    avro/trunk/lang/js/test/validator.js   (with props)
Modified:
    avro/trunk/BUILD.txt
    avro/trunk/CHANGES.txt
    avro/trunk/build.sh

Modified: avro/trunk/BUILD.txt
URL: http://svn.apache.org/viewvc/avro/trunk/BUILD.txt?rev=1395811&r1=1395810&r2=1395811&view=diff
==============================================================================
--- avro/trunk/BUILD.txt (original)
+++ avro/trunk/BUILD.txt Mon Oct  8 23:26:07 2012
@@ -10,6 +10,7 @@ The following packages must be installed
  - 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)

Modified: avro/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/avro/trunk/CHANGES.txt?rev=1395811&r1=1395810&r2=1395811&view=diff
==============================================================================
--- avro/trunk/CHANGES.txt (original)
+++ avro/trunk/CHANGES.txt Mon Oct  8 23:26:07 2012
@@ -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.

Modified: avro/trunk/build.sh
URL: http://svn.apache.org/viewvc/avro/trunk/build.sh?rev=1395811&r1=1395810&r2=1395811&view=diff
==============================================================================
--- avro/trunk/build.sh (original)
+++ avro/trunk/build.sh Mon Oct  8 23:26:07 2012
@@ -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)

Propchange: avro/trunk/lang/js/
------------------------------------------------------------------------------
--- svn:ignore (added)
+++ svn:ignore Mon Oct  8 23:26:07 2012
@@ -0,0 +1 @@
+node_modules

Added: avro/trunk/lang/js/README
URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/README?rev=1395811&view=auto
==============================================================================
--- avro/trunk/lang/js/README (added)
+++ avro/trunk/lang/js/README Mon Oct  8 23:26:07 2012
@@ -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```.

Propchange: avro/trunk/lang/js/README
------------------------------------------------------------------------------
    svn:eol-style = native

Added: avro/trunk/lang/js/build.sh
URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/build.sh?rev=1395811&view=auto
==============================================================================
--- avro/trunk/lang/js/build.sh (added)
+++ avro/trunk/lang/js/build.sh Mon Oct  8 23:26:07 2012
@@ -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

Propchange: avro/trunk/lang/js/build.sh
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: avro/trunk/lang/js/build.sh
------------------------------------------------------------------------------
    svn:executable = *

Added: avro/trunk/lang/js/grunt.js
URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/grunt.js?rev=1395811&view=auto
==============================================================================
--- avro/trunk/lang/js/grunt.js (added)
+++ avro/trunk/lang/js/grunt.js Mon Oct  8 23:26:07 2012
@@ -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');
+
+};

Propchange: avro/trunk/lang/js/grunt.js
------------------------------------------------------------------------------
    svn:eol-style = native

Added: avro/trunk/lang/js/lib/validator.js
URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/lib/validator.js?rev=1395811&view=auto
==============================================================================
--- avro/trunk/lang/js/lib/validator.js (added)
+++ avro/trunk/lang/js/lib/validator.js Mon Oct  8 23:26:07 2012
@@ -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;
+}

Propchange: avro/trunk/lang/js/lib/validator.js
------------------------------------------------------------------------------
    svn:eol-style = native

Added: avro/trunk/lang/js/package.json
URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/package.json?rev=1395811&view=auto
==============================================================================
--- avro/trunk/lang/js/package.json (added)
+++ avro/trunk/lang/js/package.json Mon Oct  8 23:26:07 2012
@@ -0,0 +1,34 @@
+{
+  "name": "avro-js",
+  "version": "0.0.1",
+  "author": "Avro Developers <de...@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"
+  }
+}

Added: avro/trunk/lang/js/test/validator.js
URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/test/validator.js?rev=1395811&view=auto
==============================================================================
--- avro/trunk/lang/js/test/validator.js (added)
+++ avro/trunk/lang/js/test/validator.js Mon Oct  8 23:26:07 2012
@@ -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();
+  }
+};

Propchange: avro/trunk/lang/js/test/validator.js
------------------------------------------------------------------------------
    svn:eol-style = native