Added: avro/trunk/lang/js/lib/schemas.js URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/lib/schemas.js?rev=1717850&view=auto ============================================================================== --- avro/trunk/lang/js/lib/schemas.js (added) +++ avro/trunk/lang/js/lib/schemas.js Thu Dec 3 21:35:44 2015 @@ -0,0 +1,2197 @@ +/* jshint node: true */ + +/** + * 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. + * + */ + +'use strict'; + +var utils = require('./utils'), + buffer = require('buffer'), // For `SlowBuffer`. + crypto = require('crypto'), + util = require('util'); + +// Convenience imports. +var Tap = utils.Tap; +var f = util.format; + +// All Avro types. +var TYPES = { + 'array': ArrayType, + 'boolean': BooleanType, + 'bytes': BytesType, + 'double': DoubleType, + 'enum': EnumType, + 'error': RecordType, + 'fixed': FixedType, + 'float': FloatType, + 'int': IntType, + 'long': LongType, + 'map': MapType, + 'null': NullType, + 'record': RecordType, + 'string': StringType, + 'union': UnionType +}; + +// Valid (field, type, and symbol) name regex. +var NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; + +// Random generator. +var RANDOM = new utils.Lcg(); + +// Encoding tap (shared for performance). +var TAP = new Tap(new buffer.SlowBuffer(1024)); + +// Path prefix for validity checks (shared for performance). +var PATH = []; + +// Currently active logical type, used for name redirection. +var LOGICAL_TYPE = null; + + +/** + * Schema parsing entry point. + * + * It isn't exposed directly but called from `parse` inside `index.js` (node) + * or `avro.js` (browserify) which each add convenience functionality. + * + */ +function createType(attrs, opts) { + if (attrs instanceof Type) { + return attrs; + } + + opts = getOpts(attrs, opts); + + var type; + if (typeof attrs == 'string') { // Type reference. + if (opts.namespace && !~attrs.indexOf('.') && !isPrimitive(attrs)) { + attrs = opts.namespace + '.' + attrs; + } + type = opts.registry[attrs]; + if (type) { + // Type was already defined, return it. + return type; + } + if (isPrimitive(attrs)) { + // Reference to a primitive type. These are also defined names by default + // so we create the appropriate type and it to the registry for future + // reference. + return opts.registry[attrs] = createType({type: attrs}, opts); + } + throw new Error(f('undefined type name: %s', attrs)); + } + + if (opts.typeHook && (type = opts.typeHook(attrs, opts))) { + if (!(type instanceof Type)) { + throw new Error(f('invalid typehook return value: %j', type)); + } + return type; + } + + if (attrs.logicalType && !LOGICAL_TYPE) { + var DerivedType = opts.logicalTypes[attrs.logicalType]; + if (DerivedType) { + var registry = {}; + Object.keys(opts.registry).forEach(function (key) { + registry[key] = opts.registry[key]; + }); + try { + return new DerivedType(attrs, opts); + } catch (err) { + if (opts.assertLogicalType) { + // The spec mandates that we fall through to the underlying type if + // the logical type is invalid. We provide this option to ease + // debugging. + throw err; + } + LOGICAL_TYPE = null; + opts.registry = registry; // In case any names were registered. + } + } + } + + if (attrs instanceof Array) { // Union. + type = new UnionType(attrs, opts); + } else { // New type definition. + type = (function (typeName) { + var Type = TYPES[typeName]; + if (Type === undefined) { + throw new Error(f('unknown type: %j', typeName)); + } + return new Type(attrs, opts); + })(attrs.type); + } + return type; +} + +/** + * "Abstract" base Avro type class. + * + * This class' constructor will register any named types to support + * recursive schemas. + * + * All type values are represented in memory similarly to their JSON + * representation, except for `bytes` and `fixed` which are represented as + * `Buffer`s. See individual subclasses for details. + * + */ +function Type(registry) { + var name = this._name; + var type = LOGICAL_TYPE || this; + LOGICAL_TYPE = null; + + if (registry === undefined || name === undefined) { + return; + } + + var prev = registry[name]; + if (prev !== undefined) { + throw new Error(f('duplicate type name: %s', name)); + } + registry[name] = type; +} + +Type.__reset = function (size) { TAP.buf = new buffer.SlowBuffer(size); }; + +Type.prototype.createResolver = function (type, opts) { + if (!(type instanceof Type)) { + // More explicit error message than the "incompatible type" thrown + // otherwise (especially because of the overridden `toJSON` method). + throw new Error(f('not a type: %j', type)); + } + + if (type instanceof LogicalType && !(this instanceof LogicalType)) { + // Trying to read a logical type as a built-in: unwrap the logical type. + return this.createResolver(type._underlyingType, opts); + } + + opts = opts || {}; + opts.registry = opts.registry || {}; + + var resolver, key; + if (this instanceof RecordType && type instanceof RecordType) { + key = this._name + ':' + type._name; // ':' is illegal in Avro type names. + resolver = opts.registry[key]; + if (resolver) { + return resolver; + } + } + + resolver = new Resolver(this); + if (key) { // Register resolver early for recursive schemas. + opts.registry[key] = resolver; + } + + if (type instanceof UnionType) { + var resolvers = type._types.map(function (t) { + return this.createResolver(t, opts); + }, this); + resolver._read = function (tap) { + var index = tap.readLong(); + var resolver = resolvers[index]; + if (resolver === undefined) { + throw new Error(f('invalid union index: %s', index)); + } + return resolvers[index]._read(tap); + }; + } else { + this._updateResolver(resolver, type, opts); + } + + if (!resolver._read) { + throw new Error(f('cannot read %s as %s', type, this)); + } + return resolver; +}; + +Type.prototype.decode = function (buf, pos, resolver) { + var tap = new Tap(buf); + tap.pos = pos | 0; + var val = readValue(this, tap, resolver); + if (!tap.isValid()) { + return {value: undefined, offset: -1}; + } + return {value: val, offset: tap.pos}; +}; + +Type.prototype.encode = function (val, buf, pos) { + var tap = new Tap(buf); + tap.pos = pos | 0; + this._write(tap, val); + if (!tap.isValid()) { + // Don't throw as there is no way to predict this. We also return the + // number of missing bytes to ease resizing. + return buf.length - tap.pos; + } + return tap.pos; +}; + +Type.prototype.fromBuffer = function (buf, resolver, noCheck) { + var tap = new Tap(buf); + var val = readValue(this, tap, resolver, noCheck); + if (!tap.isValid()) { + throw new Error('truncated buffer'); + } + if (!noCheck && tap.pos < buf.length) { + throw new Error('trailing data'); + } + return val; +}; + +Type.prototype.toBuffer = function (val) { + TAP.pos = 0; + this._write(TAP, val); + if (!TAP.isValid()) { + Type.__reset(2 * TAP.pos); + TAP.pos = 0; + this._write(TAP, val); + } + var buf = new Buffer(TAP.pos); + TAP.buf.copy(buf, 0, 0, TAP.pos); + return buf; +}; + +Type.prototype.fromString = function (str) { + return this._copy(JSON.parse(str), {coerce: 2}); +}; + +Type.prototype.toString = function (val) { + if (val === undefined) { + // Consistent behavior with standard `toString` expectations. + return this.getSchema(true); + } + return JSON.stringify(this._copy(val, {coerce: 3})); +}; + +Type.prototype.clone = function (val, opts) { + if (opts) { + opts = { + coerce: !!opts.coerceBuffers | 0, // Coerce JSON to Buffer. + fieldHook: opts.fieldHook, + wrap: !!opts.wrapUnions | 0 // Wrap first match into union. + }; + } + return this._copy(val, opts); +}; + +Type.prototype.isValid = function (val, opts) { + while (PATH.length) { + // In case the previous `isValid` call didn't complete successfully (e.g. + // if an exception was thrown, but then caught in client code), `PATH` + // might be non-empty, we must manually clear it. + PATH.pop(); + } + return this._check(val, opts && opts.errorHook); +}; + +Type.prototype.compareBuffers = function (buf1, buf2) { + return this._match(new Tap(buf1), new Tap(buf2)); +}; + +Type.prototype.getName = function () { return this._name; }; + +Type.prototype.getSchema = function (noDeref) { + // Since JS objects are unordered, this implementation (unfortunately) + // relies on engines returning properties in the same order that they are + // inserted in. This is not in the JS spec, but can be "somewhat" safely + // assumed (more here: http://stackoverflow.com/q/5525795/1062617). + return (function (type, registry) { + return JSON.stringify(type, function (key, value) { + if (value instanceof Field) { + return {name: value._name, type: value._type}; + } else if (value && value.name) { + var name = value.name; + if (noDeref || registry[name]) { + return name; + } + registry[name] = true; + } + return value; + }); + })(this, {}); +}; + +Type.prototype.getFingerprint = function (algorithm) { + algorithm = algorithm || 'md5'; + var hash = crypto.createHash(algorithm); + hash.end(this.getSchema()); + return hash.read(); +}; + +Type.prototype.inspect = function () { + if (this instanceof PrimitiveType) { + return f('<%s>', this.constructor.name); + } else { + var obj = JSON.parse(this.getSchema(true)); // Slow, only for debugging. + if (typeof obj == 'object') { + obj.type = undefined; // Would be redundant with constructor name. + } + return f('<%s %j>', this.constructor.name, obj); + } +}; + +Type.prototype._check = utils.abstractFunction; +Type.prototype._copy = utils.abstractFunction; +Type.prototype._match = utils.abstractFunction; +Type.prototype._read = utils.abstractFunction; +Type.prototype._skip = utils.abstractFunction; +Type.prototype._updateResolver = utils.abstractFunction; +Type.prototype._write = utils.abstractFunction; +Type.prototype.compare = utils.abstractFunction; +Type.prototype.random = utils.abstractFunction; + +// Implementations. + +/** + * Base primitive Avro type. + * + * Most of the primitive types share the same cloning and resolution + * mechanisms, provided by this class. This class also lets us conveniently + * check whether a type is a primitive using `instanceof`. + * + */ +function PrimitiveType() { Type.call(this); } +util.inherits(PrimitiveType, Type); +PrimitiveType.prototype._updateResolver = function (resolver, type) { + if (type.constructor === this.constructor) { + resolver._read = this._read; + } +}; +PrimitiveType.prototype._copy = function (val) { + this._check(val, throwInvalidError); + return val; +}; +PrimitiveType.prototype.compare = utils.compare; + +/** + * Nulls. + * + */ +function NullType() { PrimitiveType.call(this); } +util.inherits(NullType, PrimitiveType); +NullType.prototype._check = function (val, cb) { + var b = val === null; + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; +NullType.prototype._read = function () { return null; }; +NullType.prototype._skip = function () {}; +NullType.prototype._write = function (tap, val) { + if (val !== null) { + throwInvalidError(null, val, this); + } +}; +NullType.prototype._match = function () { return 0; }; +NullType.prototype.compare = NullType.prototype._match; +NullType.prototype.random = NullType.prototype._read; +NullType.prototype.toJSON = function () { return 'null'; }; + +/** + * Booleans. + * + */ +function BooleanType() { PrimitiveType.call(this); } +util.inherits(BooleanType, PrimitiveType); +BooleanType.prototype._check = function (val, cb) { + var b = typeof val == 'boolean'; + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; +BooleanType.prototype._read = function (tap) { return tap.readBoolean(); }; +BooleanType.prototype._skip = function (tap) { tap.skipBoolean(); }; +BooleanType.prototype._write = function (tap, val) { + if (typeof val != 'boolean') { + throwInvalidError(null, val, this); + } + tap.writeBoolean(val); +}; +BooleanType.prototype._match = function (tap1, tap2) { + return tap1.matchBoolean(tap2); +}; +BooleanType.prototype.random = function () { return RANDOM.nextBoolean(); }; +BooleanType.prototype.toJSON = function () { return 'boolean'; }; + +/** + * Integers. + * + */ +function IntType() { PrimitiveType.call(this); } +util.inherits(IntType, PrimitiveType); +IntType.prototype._check = function (val, cb) { + var b = val === (val | 0); + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; +IntType.prototype._read = function (tap) { return tap.readInt(); }; +IntType.prototype._skip = function (tap) { tap.skipInt(); }; +IntType.prototype._write = function (tap, val) { + if (val !== (val | 0)) { + throwInvalidError(null, val, this); + } + tap.writeInt(val); +}; +IntType.prototype._match = function (tap1, tap2) { + return tap1.matchInt(tap2); +}; +IntType.prototype.random = function () { return RANDOM.nextInt(1000) | 0; }; +IntType.prototype.toJSON = function () { return 'int'; }; + +/** + * Longs. + * + * We can't capture all the range unfortunately since JavaScript represents all + * numbers internally as `double`s, so the default implementation plays safe + * and throws rather than potentially silently change the data. See `using` or + * `AbstractLongType` below for a way to implement a custom long type. + * + */ +function LongType() { PrimitiveType.call(this); } +util.inherits(LongType, PrimitiveType); +LongType.prototype._check = function (val, cb) { + var b = typeof val == 'number' && val % 1 === 0 && isSafeLong(val); + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; +LongType.prototype._read = function (tap) { + var n = tap.readLong(); + if (!isSafeLong(n)) { + throw new Error('potential precision loss'); + } + return n; +}; +LongType.prototype._skip = function (tap) { tap.skipLong(); }; +LongType.prototype._write = function (tap, val) { + if (typeof val != 'number' || val % 1 || !isSafeLong(val)) { + throwInvalidError(null, val, this); + } + tap.writeLong(val); +}; +LongType.prototype._match = function (tap1, tap2) { + return tap1.matchLong(tap2); +}; +LongType.prototype._updateResolver = function (resolver, type) { + if (type instanceof LongType || type instanceof IntType) { + resolver._read = type._read; + } +}; +LongType.prototype.random = function () { return RANDOM.nextInt(); }; +LongType.prototype.toJSON = function () { return 'long'; }; +LongType.using = function (methods, noUnpack) { + methods = methods || {}; // Will give a more helpful error message. + // We map some of the methods to a different name to be able to intercept + // their input and output (otherwise we wouldn't be able to perform any + // unpacking logic, and the type wouldn't work when nested). + var mapping = { + toBuffer: '_toBuffer', + fromBuffer: '_fromBuffer', + fromJSON: '_fromJSON', + toJSON: '_toJSON', + isValid: '_isValid', + compare: 'compare' + }; + var type = new AbstractLongType(noUnpack); + Object.keys(mapping).forEach(function (name) { + if (methods[name] === undefined) { + throw new Error(f('missing method implementation: %s', name)); + } + type[mapping[name]] = methods[name]; + }); + return type; +}; + +/** + * Floats. + * + */ +function FloatType() { PrimitiveType.call(this); } +util.inherits(FloatType, PrimitiveType); +FloatType.prototype._check = function (val, cb) { + var b = typeof val == 'number'; + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; +FloatType.prototype._read = function (tap) { return tap.readFloat(); }; +FloatType.prototype._skip = function (tap) { tap.skipFloat(); }; +FloatType.prototype._write = function (tap, val) { + if (typeof val != 'number') { + throwInvalidError(null, val, this); + } + tap.writeFloat(val); +}; +FloatType.prototype._match = function (tap1, tap2) { + return tap1.matchFloat(tap2); +}; +FloatType.prototype._updateResolver = function (resolver, type) { + if ( + type instanceof FloatType || + type instanceof LongType || + type instanceof IntType + ) { + resolver._read = type._read; + } +}; +FloatType.prototype.random = function () { return RANDOM.nextFloat(1e3); }; +FloatType.prototype.toJSON = function () { return 'float'; }; + +/** + * Doubles. + * + */ +function DoubleType() { PrimitiveType.call(this); } +util.inherits(DoubleType, PrimitiveType); +DoubleType.prototype._check = function (val, cb) { + var b = typeof val == 'number'; + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; +DoubleType.prototype._read = function (tap) { return tap.readDouble(); }; +DoubleType.prototype._skip = function (tap) { tap.skipDouble(); }; +DoubleType.prototype._write = function (tap, val) { + if (typeof val != 'number') { + throwInvalidError(null, val, this); + } + tap.writeDouble(val); +}; +DoubleType.prototype._match = function (tap1, tap2) { + return tap1.matchDouble(tap2); +}; +DoubleType.prototype._updateResolver = function (resolver, type) { + if ( + type instanceof DoubleType || + type instanceof FloatType || + type instanceof LongType || + type instanceof IntType + ) { + resolver._read = type._read; + } +}; +DoubleType.prototype.random = function () { return RANDOM.nextFloat(); }; +DoubleType.prototype.toJSON = function () { return 'double'; }; + +/** + * Strings. + * + */ +function StringType() { PrimitiveType.call(this); } +util.inherits(StringType, PrimitiveType); +StringType.prototype._check = function (val, cb) { + var b = typeof val == 'string'; + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; +StringType.prototype._read = function (tap) { return tap.readString(); }; +StringType.prototype._skip = function (tap) { tap.skipString(); }; +StringType.prototype._write = function (tap, val) { + if (typeof val != 'string') { + throwInvalidError(null, val, this); + } + tap.writeString(val); +}; +StringType.prototype._match = function (tap1, tap2) { + return tap1.matchString(tap2); +}; +StringType.prototype._updateResolver = function (resolver, type) { + if (type instanceof StringType || type instanceof BytesType) { + resolver._read = this._read; + } +}; +StringType.prototype.random = function () { + return RANDOM.nextString(RANDOM.nextInt(32)); +}; +StringType.prototype.toJSON = function () { return 'string'; }; + +/** + * Bytes. + * + * These are represented in memory as `Buffer`s rather than binary-encoded + * strings. This is more efficient (when decoding/encoding from bytes, the + * common use-case), idiomatic, and convenient. + * + * Note the coercion in `_copy`. + * + */ +function BytesType() { PrimitiveType.call(this); } +util.inherits(BytesType, PrimitiveType); +BytesType.prototype._check = function (val, cb) { + var b = Buffer.isBuffer(val); + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; +BytesType.prototype._read = function (tap) { return tap.readBytes(); }; +BytesType.prototype._skip = function (tap) { tap.skipBytes(); }; +BytesType.prototype._write = function (tap, val) { + if (!Buffer.isBuffer(val)) { + throwInvalidError(null, val, this); + } + tap.writeBytes(val); +}; +BytesType.prototype._match = function (tap1, tap2) { + return tap1.matchBytes(tap2); +}; +BytesType.prototype._updateResolver = StringType.prototype._updateResolver; +BytesType.prototype._copy = function (obj, opts) { + var buf; + switch ((opts && opts.coerce) | 0) { + case 3: // Coerce buffers to strings. + this._check(obj, throwInvalidError); + return obj.toString('binary'); + case 2: // Coerce strings to buffers. + if (typeof obj != 'string') { + throw new Error(f('cannot coerce to buffer: %j', obj)); + } + buf = new Buffer(obj, 'binary'); + this._check(buf, throwInvalidError); + return buf; + case 1: // Coerce buffer JSON representation to buffers. + if (!obj || obj.type !== 'Buffer' || !(obj.data instanceof Array)) { + throw new Error(f('cannot coerce to buffer: %j', obj)); + } + buf = new Buffer(obj.data); + this._check(buf, throwInvalidError); + return buf; + default: // Copy buffer. + this._check(obj, throwInvalidError); + return new Buffer(obj); + } +}; +BytesType.prototype.compare = Buffer.compare; +BytesType.prototype.random = function () { + return RANDOM.nextBuffer(RANDOM.nextInt(32)); +}; +BytesType.prototype.toJSON = function () { return 'bytes'; }; + +/** + * Avro unions. + * + * Unions are represented in memory similarly to their JSON representation + * (i.e. inside an object with single key the name of the contained type). + * + * This is not ideal, but is the most efficient way to unambiguously support + * all unions. Here are a few reasons why the wrapping object is necessary: + * + * + Unions with multiple number types would have undefined behavior, unless + * numbers are wrapped (either everywhere, leading to large performance and + * convenience costs; or only when necessary inside unions, making it hard to + * understand when numbers are wrapped or not). + * + Fixed types would have to be wrapped to be distinguished from bytes. + * + Using record's constructor names would work (after a slight change to use + * the fully qualified name), but would mean that generic objects could no + * longer be valid records (making it inconvenient to do simple things like + * creating new records). + * + * Lore: In the past (until d304cab), there used to be an "unwrapped union + * type" which directly exposed its values, without the wrapping object + * (similarly to Avro's python implementation). It was removed to keep all + * representations consistent and make this library simpler to understand + * (conversions, e.g. for schema evolution, between representations were + * particularly confusing). Encoding was also much slower (worst case + * complexity linear in the number of types in the union). + * + */ +function UnionType(attrs, opts) { + if (!(attrs instanceof Array)) { + throw new Error(f('non-array union schema: %j', attrs)); + } + if (!attrs.length) { + throw new Error('empty union'); + } + + opts = getOpts(attrs, opts); + Type.call(this); + this._types = attrs.map(function (obj) { return createType(obj, opts); }); + + this._indices = {}; + this._types.forEach(function (type, i) { + if (type instanceof UnionType) { + throw new Error('unions cannot be directly nested'); + } + var name = type._name || getTypeName(type); + if (this._indices[name] !== undefined) { + throw new Error(f('duplicate union name: %j', name)); + } + this._indices[name] = i; + }, this); + + this._constructors = this._types.map(function (type) { + // jshint -W054 + var name = type._name || getTypeName(type); + if (name === 'null') { + return null; + } + var body; + if (~name.indexOf('.')) { // Qualified name. + body = 'this[\'' + name + '\'] = val;'; + } else { + body = 'this.' + name + ' = val;'; + } + return new Function('val', body); + }); +} +util.inherits(UnionType, Type); + +UnionType.prototype._check = function (val, cb) { + var b = false; + if (val === null) { + // Shortcut type lookup in this case. + b = this._indices['null'] !== undefined; + } else if (typeof val == 'object') { + var keys = Object.keys(val); + if (keys.length === 1) { + // We require a single key here to ensure that writes are correct and + // efficient as soon as a record passes this check. + var name = keys[0]; + var index = this._indices[name]; + if (index !== undefined) { + PATH.push(name); + b = this._types[index]._check(val[name], cb); + PATH.pop(); + return b; + } + } + } + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; + +UnionType.prototype._read = function (tap) { + var index = tap.readLong(); + var Class = this._constructors[index]; + if (Class) { + return new Class(this._types[index]._read(tap)); + } else if (Class === null) { + return null; + } else { + throw new Error(f('invalid union index: %s', index)); + } +}; + +UnionType.prototype._skip = function (tap) { + this._types[tap.readLong()]._skip(tap); +}; + +UnionType.prototype._write = function (tap, val) { + var index, keys, name; + if (val === null) { + index = this._indices['null']; + if (index === undefined) { + throwInvalidError(null, val, this); + } + tap.writeLong(index); + } else { + keys = Object.keys(val); + if (keys.length === 1) { + name = keys[0]; + index = this._indices[name]; + } + if (index === undefined) { + throwInvalidError(null, val, this); + } + tap.writeLong(index); + this._types[index]._write(tap, val[name]); + } +}; + +UnionType.prototype._match = function (tap1, tap2) { + var n1 = tap1.readLong(); + var n2 = tap2.readLong(); + if (n1 === n2) { + return this._types[n1]._match(tap1, tap2); + } else { + return n1 < n2 ? -1 : 1; + } +}; + +UnionType.prototype._updateResolver = function (resolver, type, opts) { + // jshint -W083 + // (The loop exits after the first function is created.) + var i, l, typeResolver, Class; + for (i = 0, l = this._types.length; i < l; i++) { + try { + typeResolver = this._types[i].createResolver(type, opts); + } catch (err) { + continue; + } + Class = this._constructors[i]; + if (Class) { + resolver._read = function (tap) { + return new Class(typeResolver._read(tap)); + }; + } else { + resolver._read = function () { return null; }; + } + return; + } +}; + +UnionType.prototype._copy = function (val, opts) { + var wrap = opts && opts.wrap | 0; + if (wrap === 2) { + // Promote into first type (used for schema defaults). + if (val === null && this._constructors[0] === null) { + return null; + } + return new this._constructors[0](this._types[0]._copy(val, opts)); + } + if (val === null && this._indices['null'] !== undefined) { + return null; + } + var i, l, obj; + if (wrap === 1) { + // Promote into first match (convenience, slow). + i = 0; + l = this._types.length; + while (i < l && obj === undefined) { + try { + obj = this._types[i]._copy(val, opts); + } catch (err) { + i++; + } + } + } else if (typeof val == 'object') { + var keys = Object.keys(val); + if (keys.length === 1) { + var name = keys[0]; + i = this._indices[name]; + if (i === undefined) { + // We are a bit more flexible than in `_check` here since we have + // to deal with other serializers being less strict, so we fall + // back to looking up unqualified names. + var j, type; + for (j = 0, l = this._types.length; j < l; j++) { + type = this._types[j]; + if (type._name && name === unqualify(type._name)) { + i = j; + break; + } + } + } + if (i !== undefined) { + obj = this._types[i]._copy(val[name], opts); + } + } + } + if (obj !== undefined) { + return new this._constructors[i](obj); + } + throwInvalidError(null, val, this); +}; + +UnionType.prototype.compare = function (val1, val2) { + var name1 = val1 === null ? 'null' : Object.keys(val1)[0]; + var name2 = val2 === null ? 'null' : Object.keys(val2)[0]; + var index = this._indices[name1]; + if (name1 === name2) { + return name1 === 'null' ? + 0 : + this._types[index].compare(val1[name1], val2[name1]); + } else { + return utils.compare(index, this._indices[name2]); + } +}; + +UnionType.prototype.getTypes = function () { return this._types.slice(); }; + +UnionType.prototype.random = function () { + var index = RANDOM.nextInt(this._types.length); + var Class = this._constructors[index]; + if (!Class) { + return null; + } + return new Class(this._types[index].random()); +}; + +UnionType.prototype.toJSON = function () { return this._types; }; + +/** + * Avro enum type. + * + * Represented as strings (with allowed values from the set of symbols). Using + * integers would be a reasonable option, but the performance boost is arguably + * offset by the legibility cost and the extra deviation from the JSON encoding + * convention. + * + * An integer representation can still be used (e.g. for compatibility with + * TypeScript `enum`s) by overriding the `EnumType` with a `LongType` (e.g. via + * `parse`'s registry). + * + */ +function EnumType(attrs, opts) { + if (!(attrs.symbols instanceof Array) || !attrs.symbols.length) { + throw new Error(f('invalid %j enum symbols: %j', attrs.name, attrs)); + } + + opts = getOpts(attrs, opts); + var resolutions = resolveNames(attrs, opts.namespace); + this._name = resolutions.name; + this._symbols = attrs.symbols; + this._aliases = resolutions.aliases; + Type.call(this, opts.registry); + + this._indices = {}; + this._symbols.forEach(function (symbol, i) { + if (!NAME_PATTERN.test(symbol)) { + throw new Error(f('invalid %s symbol: %j', this, symbol)); + } + if (this._indices[symbol] !== undefined) { + throw new Error(f('duplicate %s symbol: %j', this, symbol)); + } + this._indices[symbol] = i; + }, this); +} +util.inherits(EnumType, Type); + +EnumType.prototype._check = function (val, cb) { + var b = this._indices[val] !== undefined; + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; + +EnumType.prototype._read = function (tap) { + var index = tap.readLong(); + var symbol = this._symbols[index]; + if (symbol === undefined) { + throw new Error(f('invalid %s enum index: %s', this._name, index)); + } + return symbol; +}; + +EnumType.prototype._skip = function (tap) { tap.skipLong(); }; + +EnumType.prototype._write = function (tap, val) { + var index = this._indices[val]; + if (index === undefined) { + throwInvalidError(null, val, this); + } + tap.writeLong(index); +}; + +EnumType.prototype._match = function (tap1, tap2) { + return tap1.matchLong(tap2); +}; + +EnumType.prototype.compare = function (val1, val2) { + return utils.compare(this._indices[val1], this._indices[val2]); +}; + +EnumType.prototype._updateResolver = function (resolver, type) { + var symbols = this._symbols; + if ( + type instanceof EnumType && + ~getAliases(this).indexOf(type._name) && + type._symbols.every(function (s) { return ~symbols.indexOf(s); }) + ) { + resolver._symbols = type._symbols; + resolver._read = type._read; + } +}; + +EnumType.prototype._copy = function (val) { + this._check(val, throwInvalidError); + return val; +}; + +EnumType.prototype.getAliases = function () { return this._aliases; }; + +EnumType.prototype.getSymbols = function () { return this._symbols.slice(); }; + +EnumType.prototype.random = function () { + return RANDOM.choice(this._symbols); +}; + +EnumType.prototype.toJSON = function () { + return {name: this._name, type: 'enum', symbols: this._symbols}; +}; + +/** + * Avro fixed type. + * + * Represented simply as a `Buffer`. + * + */ +function FixedType(attrs, opts) { + if (attrs.size !== (attrs.size | 0) || attrs.size < 1) { + throw new Error(f('invalid %j fixed size: %j', attrs.name, attrs.size)); + } + + opts = getOpts(attrs, opts); + var resolutions = resolveNames(attrs, opts.namespace); + this._name = resolutions.name; + this._size = attrs.size | 0; + this._aliases = resolutions.aliases; + Type.call(this, opts.registry); +} +util.inherits(FixedType, Type); + +FixedType.prototype._check = function (val, cb) { + var b = Buffer.isBuffer(val) && val.length === this._size; + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; + +FixedType.prototype._read = function (tap) { + return tap.readFixed(this._size); +}; + +FixedType.prototype._skip = function (tap) { + tap.skipFixed(this._size); +}; + +FixedType.prototype._write = function (tap, val) { + if (!Buffer.isBuffer(val) || val.length !== this._size) { + throwInvalidError(null, val, this); + } + tap.writeFixed(val, this._size); +}; + +FixedType.prototype._match = function (tap1, tap2) { + return tap1.matchFixed(tap2, this._size); +}; + +FixedType.prototype.compare = Buffer.compare; + +FixedType.prototype._updateResolver = function (resolver, type) { + if ( + type instanceof FixedType && + this._size === type._size && + ~getAliases(this).indexOf(type._name) + ) { + resolver._size = this._size; + resolver._read = this._read; + } +}; + +FixedType.prototype._copy = BytesType.prototype._copy; + +FixedType.prototype.getAliases = function () { return this._aliases; }; + +FixedType.prototype.getSize = function () { return this._size; }; + +FixedType.prototype.random = function () { + return RANDOM.nextBuffer(this._size); +}; + +FixedType.prototype.toJSON = function () { + return {name: this._name, type: 'fixed', size: this._size}; +}; + +/** + * Avro map. + * + * Represented as vanilla objects. + * + */ +function MapType(attrs, opts) { + if (!attrs.values) { + throw new Error(f('missing map values: %j', attrs)); + } + + opts = getOpts(attrs, opts); + Type.call(this); + this._values = createType(attrs.values, opts); +} +util.inherits(MapType, Type); + +MapType.prototype.getValuesType = function () { return this._values; }; + +MapType.prototype._check = function (val, cb) { + if (!val || typeof val != 'object' || val instanceof Array) { + if (cb) { + cb(PATH.slice(), val, this); + } + return false; + } + + var keys = Object.keys(val); + var b = true; + var i, l, j, key; + if (cb) { + // Slow path. + j = PATH.length; + PATH.push(''); + for (i = 0, l = keys.length; i < l; i++) { + key = PATH[j] = keys[i]; + if (!this._values._check(val[key], cb)) { + b = false; + } + } + PATH.pop(); + } else { + for (i = 0, l = keys.length; i < l; i++) { + if (!this._values._check(val[keys[i]], cb)) { + return false; + } + } + } + return b; +}; + +MapType.prototype._read = function (tap) { + var values = this._values; + var val = {}; + var n; + while ((n = readArraySize(tap))) { + while (n--) { + var key = tap.readString(); + val[key] = values._read(tap); + } + } + return val; +}; + +MapType.prototype._skip = function (tap) { + var values = this._values; + var len, n; + while ((n = tap.readLong())) { + if (n < 0) { + len = tap.readLong(); + tap.pos += len; + } else { + while (n--) { + tap.skipString(); + values._skip(tap); + } + } + } +}; + +MapType.prototype._write = function (tap, val) { + if (!val || typeof val != 'object' || val instanceof Array) { + throwInvalidError(null, val, this); + } + + var values = this._values; + var keys = Object.keys(val); + var n = keys.length; + var i, key; + if (n) { + tap.writeLong(n); + for (i = 0; i < n; i++) { + key = keys[i]; + tap.writeString(key); + values._write(tap, val[key]); + } + } + tap.writeLong(0); +}; + +MapType.prototype._match = function () { + throw new Error('maps cannot be compared'); +}; + +MapType.prototype._updateResolver = function (resolver, type, opts) { + if (type instanceof MapType) { + resolver._values = this._values.createResolver(type._values, opts); + resolver._read = this._read; + } +}; + +MapType.prototype._copy = function (val, opts) { + if (val && typeof val == 'object' && !(val instanceof Array)) { + var values = this._values; + var keys = Object.keys(val); + var i, l, key; + var copy = {}; + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + copy[key] = values._copy(val[key], opts); + } + return copy; + } + throwInvalidError(null, val, this); +}; + +MapType.prototype.compare = MapType.prototype._match; + +MapType.prototype.random = function () { + var val = {}; + var i, l; + for (i = 0, l = RANDOM.nextInt(10); i < l; i++) { + val[RANDOM.nextString(RANDOM.nextInt(20))] = this._values.random(); + } + return val; +}; + +MapType.prototype.toJSON = function () { + return {type: 'map', values: this._values}; +}; + +/** + * Avro array. + * + * Represented as vanilla arrays. + * + */ +function ArrayType(attrs, opts) { + if (!attrs.items) { + throw new Error(f('missing array items: %j', attrs)); + } + + opts = getOpts(attrs, opts); + + this._items = createType(attrs.items, opts); + Type.call(this); +} +util.inherits(ArrayType, Type); + +ArrayType.prototype._check = function (val, cb) { + if (!(val instanceof Array)) { + if (cb) { + cb(PATH.slice(), val, this); + } + return false; + } + + var b = true; + var i, l, j; + if (cb) { + // Slow path. + j = PATH.length; + PATH.push(''); + for (i = 0, l = val.length; i < l; i++) { + PATH[j] = '' + i; + if (!this._items._check(val[i], cb)) { + b = false; + } + } + PATH.pop(); + } else { + for (i = 0, l = val.length; i < l; i++) { + if (!this._items._check(val[i], cb)) { + return false; + } + } + } + return b; +}; + +ArrayType.prototype._read = function (tap) { + var items = this._items; + var val = []; + var n; + while ((n = tap.readLong())) { + if (n < 0) { + n = -n; + tap.skipLong(); // Skip size. + } + while (n--) { + val.push(items._read(tap)); + } + } + return val; +}; + +ArrayType.prototype._skip = function (tap) { + var len, n; + while ((n = tap.readLong())) { + if (n < 0) { + len = tap.readLong(); + tap.pos += len; + } else { + while (n--) { + this._items._skip(tap); + } + } + } +}; + +ArrayType.prototype._write = function (tap, val) { + if (!(val instanceof Array)) { + throwInvalidError(null, val, this); + } + + var n = val.length; + var i; + if (n) { + tap.writeLong(n); + for (i = 0; i < n; i++) { + this._items._write(tap, val[i]); + } + } + tap.writeLong(0); +}; + +ArrayType.prototype._match = function (tap1, tap2) { + var n1 = tap1.readLong(); + var n2 = tap2.readLong(); + var f; + while (n1 && n2) { + f = this._items._match(tap1, tap2); + if (f) { + return f; + } + if (!--n1) { + n1 = readArraySize(tap1); + } + if (!--n2) { + n2 = readArraySize(tap2); + } + } + return utils.compare(n1, n2); +}; + +ArrayType.prototype._updateResolver = function (resolver, type, opts) { + if (type instanceof ArrayType) { + resolver._items = this._items.createResolver(type._items, opts); + resolver._read = this._read; + } +}; + +ArrayType.prototype._copy = function (val, opts) { + if (!(val instanceof Array)) { + throwInvalidError(null, val, this); + } + var items = []; + var i, l; + for (i = 0, l = val.length; i < l; i++) { + items.push(this._items._copy(val[i], opts)); + } + return items; +}; + +ArrayType.prototype.compare = function (val1, val2) { + var n1 = val1.length; + var n2 = val2.length; + var i, l, f; + for (i = 0, l = Math.min(n1, n2); i < l; i++) { + if ((f = this._items.compare(val1[i], val2[i]))) { + return f; + } + } + return utils.compare(n1, n2); +}; + +ArrayType.prototype.getItemsType = function () { return this._items; }; + +ArrayType.prototype.random = function () { + var arr = []; + var i, l; + for (i = 0, l = RANDOM.nextInt(10); i < l; i++) { + arr.push(this._items.random()); + } + return arr; +}; + +ArrayType.prototype.toJSON = function () { + return {type: 'array', items: this._items}; +}; + +/** + * Avro record. + * + * Values are represented as instances of a programmatically generated + * constructor (similar to a "specific record"), available via the + * `getRecordConstructor` method. This "specific record class" gives + * significant speedups over using generics objects. + * + * Note that vanilla objects are still accepted as valid as long as their + * fields match (this makes it much more convenient to do simple things like + * update nested records). + * + */ +function RecordType(attrs, opts) { + opts = getOpts(attrs, opts); + + var resolutions = resolveNames(attrs, opts.namespace); + this._name = resolutions.name; + this._aliases = resolutions.aliases; + Type.call(this, opts.registry); + + if (!(attrs.fields instanceof Array)) { + throw new Error(f('non-array %s fields', this._name)); + } + this._fields = attrs.fields.map(function (f) { + return new Field(f, opts); + }); + if (utils.hasDuplicates(attrs.fields, function (f) { return f.name; })) { + throw new Error(f('duplicate %s field name', this._name)); + } + + var isError = attrs.type === 'error'; + this._constructor = this._createConstructor(isError); + this._read = this._createReader(); + this._skip = this._createSkipper(); + this._write = this._createWriter(); + this._check = this._createChecker(); +} +util.inherits(RecordType, Type); + +RecordType.prototype._createConstructor = function (isError) { + // jshint -W054 + var outerArgs = []; + var innerArgs = []; + var ds = []; // Defaults. + var innerBody = isError ? ' Error.call(this);\n' : ''; + // Not calling `Error.captureStackTrace` because this wouldn't be compatible + // with browsers other than Chrome. + var i, l, field, name, getDefault; + for (i = 0, l = this._fields.length; i < l; i++) { + field = this._fields[i]; + getDefault = field.getDefault; + name = field._name; + innerArgs.push('v' + i); + innerBody += ' '; + if (getDefault() === undefined) { + innerBody += 'this.' + name + ' = v' + i + ';\n'; + } else { + innerBody += 'if (v' + i + ' === undefined) { '; + innerBody += 'this.' + name + ' = d' + ds.length + '(); '; + innerBody += '} else { this.' + name + ' = v' + i + '; }\n'; + outerArgs.push('d' + ds.length); + ds.push(getDefault); + } + } + var outerBody = 'return function ' + unqualify(this._name) + '('; + outerBody += innerArgs.join() + ') {\n' + innerBody + '};'; + var Record = new Function(outerArgs.join(), outerBody).apply(undefined, ds); + + var self = this; + Record.getType = function () { return self; }; + Record.prototype = { + constructor: Record, + $clone: function (opts) { return self.clone(this, opts); }, + $compare: function (val) { return self.compare(this, val); }, + $getType: Record.getType, + $isValid: function (opts) { return self.isValid(this, opts); }, + $toBuffer: function () { return self.toBuffer(this); }, + $toString: function (noCheck) { return self.toString(this, noCheck); } + }; + // The names of these properties added to the prototype are prefixed with `$` + // because it is an invalid property name in Avro but not in JavaScript. + // (This way we are guaranteed not to be stepped over!) + if (isError) { + util.inherits(Record, Error); + // Not setting the name on the prototype to be consistent with how object + // fields are mapped to (only if defined in the schema as a field). + } + + return Record; +}; + +RecordType.prototype._createChecker = function () { + // jshint -W054 + var names = ['t', 'P']; + var values = [this, PATH]; + var body = 'return function check' + unqualify(this._name) + '(val, cb) {\n'; + body += ' if (val === null || typeof val != \'object\') {\n'; + body += ' if (cb) { cb(P.slice(), val, t); }\n'; + body += ' return false;\n'; + body += ' }\n'; + if (!this._fields.length) { + // Special case, empty record. We handle this directly. + body += ' return true;\n'; + } else { + for (i = 0, l = this._fields.length; i < l; i++) { + field = this._fields[i]; + names.push('t' + i); + values.push(field._type); + if (field.getDefault() !== undefined) { + body += ' var v' + i + ' = val.' + field._name + ';\n'; + } + } + body += ' if (cb) {\n'; + body += ' var b = 1;\n'; + body += ' var j = P.length;\n'; + body += ' P.push(\'\');\n'; + var i, l, field; + for (i = 0, l = this._fields.length; i < l; i++) { + field = this._fields[i]; + body += ' P[j] = \'' + field._name + '\';\n'; + if (field.getDefault() === undefined) { + body += ' b &= t' + i + '._check(val.' + field._name + ', cb);\n'; + } else { + body += ' b &= v' + i + ' === undefined || '; + body += 't' + i + '._check(v' + i + ', cb);\n'; + } + } + body += ' P.pop();\n'; + body += ' return !!b;\n'; + body += ' } else {\n return (\n '; + body += this._fields.map(function (field, i) { + if (field.getDefault() === undefined) { + return 't' + i + '._check(val.' + field._name + ')'; + } else { + return '(v' + i + ' === undefined || t' + i + '._check(v' + i + '))'; + } + }).join(' &&\n '); + body += '\n );\n }\n'; + } + body += '};'; + return new Function(names.join(), body).apply(undefined, values); +}; + +RecordType.prototype._createReader = function () { + // jshint -W054 + var uname = unqualify(this._name); + var names = []; + var values = [this._constructor]; + var i, l; + for (i = 0, l = this._fields.length; i < l; i++) { + names.push('t' + i); + values.push(this._fields[i]._type); + } + var body = 'return function read' + uname + '(tap) {\n'; + body += ' return new ' + uname + '('; + body += names.map(function (t) { return t + '._read(tap)'; }).join(); + body += ');\n};'; + names.unshift(uname); + // We can do this since the JS spec guarantees that function arguments are + // evaluated from left to right. + return new Function(names.join(), body).apply(undefined, values); +}; + +RecordType.prototype._createSkipper = function () { + // jshint -W054 + var args = []; + var body = 'return function skip' + unqualify(this._name) + '(tap) {\n'; + var values = []; + var i, l; + for (i = 0, l = this._fields.length; i < l; i++) { + args.push('t' + i); + values.push(this._fields[i]._type); + body += ' t' + i + '._skip(tap);\n'; + } + body += '}'; + return new Function(args.join(), body).apply(undefined, values); +}; + +RecordType.prototype._createWriter = function () { + // jshint -W054 + // We still do default handling here, in case a normal JS object is passed. + var args = []; + var body = 'return function write' + unqualify(this._name) + '(tap, val) {\n'; + var values = []; + var i, l, field, value; + for (i = 0, l = this._fields.length; i < l; i++) { + field = this._fields[i]; + args.push('t' + i); + values.push(field._type); + body += ' '; + if (field.getDefault() === undefined) { + body += 't' + i + '._write(tap, val.' + field._name + ');\n'; + } else { + value = field._type.toBuffer(field.getDefault()).toString('binary'); + // Convert the default value to a binary string ahead of time. We aren't + // converting it to a buffer to avoid retaining too much memory. If we + // had our own buffer pool, this could be an idea in the future. + args.push('d' + i); + values.push(value); + body += 'var v' + i + ' = val.' + field._name + '; '; + body += 'if (v' + i + ' === undefined) { '; + body += 'tap.writeBinary(d' + i + ', ' + value.length + ');'; + body += ' } else { t' + i + '._write(tap, v' + i + '); }\n'; + } + } + body += '}'; + return new Function(args.join(), body).apply(undefined, values); +}; + +RecordType.prototype._updateResolver = function (resolver, type, opts) { + // jshint -W054 + if (!~getAliases(this).indexOf(type._name)) { + throw new Error(f('no alias for %s in %s', type._name, this._name)); + } + + var rFields = this._fields; + var wFields = type._fields; + var wFieldsMap = utils.toMap(wFields, function (f) { return f._name; }); + + var innerArgs = []; // Arguments for reader constructor. + var resolvers = {}; // Resolvers keyed by writer field name. + var i, j, field, name, names, matches; + for (i = 0; i < rFields.length; i++) { + field = rFields[i]; + names = getAliases(field); + matches = []; + for (j = 0; j < names.length; j++) { + name = names[j]; + if (wFieldsMap[name]) { + matches.push(name); + } + } + if (matches.length > 1) { + throw new Error(f('multiple matches for %s', field.name)); + } + if (!matches.length) { + if (field.getDefault() === undefined) { + throw new Error(f('no match for default-less %s', field.name)); + } + innerArgs.push('undefined'); + } else { + name = matches[0]; + resolvers[name] = { + resolver: field._type.createResolver(wFieldsMap[name]._type, opts), + name: field._name // Reader field name. + }; + innerArgs.push(field._name); + } + } + + // See if we can add a bypass for unused fields at the end of the record. + var lazyIndex = -1; + i = wFields.length; + while (i && resolvers[wFields[--i]._name] === undefined) { + lazyIndex = i; + } + + var uname = unqualify(this._name); + var args = [uname]; + var values = [this._constructor]; + var body = ' return function read' + uname + '(tap,lazy) {\n'; + for (i = 0; i < wFields.length; i++) { + if (i === lazyIndex) { + body += ' if (!lazy) {\n'; + } + field = type._fields[i]; + name = field._name; + body += (~lazyIndex && i >= lazyIndex) ? ' ' : ' '; + if (resolvers[name] === undefined) { + args.push('t' + i); + values.push(field._type); + body += 't' + i + '._skip(tap);\n'; + } else { + args.push('t' + i); + values.push(resolvers[name].resolver); + body += 'var ' + resolvers[name].name + ' = '; + body += 't' + i + '._read(tap);\n'; + } + } + if (~lazyIndex) { + body += ' }\n'; + } + body += ' return new ' + uname + '(' + innerArgs.join() + ');\n};'; + + resolver._read = new Function(args.join(), body).apply(undefined, values); +}; + +RecordType.prototype._match = function (tap1, tap2) { + var fields = this._fields; + var i, l, field, order, type; + for (i = 0, l = fields.length; i < l; i++) { + field = fields[i]; + order = field._order; + type = field._type; + if (order) { + order *= type._match(tap1, tap2); + if (order) { + return order; + } + } else { + type._skip(tap1); + type._skip(tap2); + } + } + return 0; +}; + +RecordType.prototype._copy = function (val, opts) { + // jshint -W058 + var hook = opts && opts.fieldHook; + var values = [undefined]; + var i, l, field, value; + for (i = 0, l = this._fields.length; i < l; i++) { + field = this._fields[i]; + value = field._type._copy(val[field._name], opts); + if (hook) { + value = hook(field, value, this); + } + values.push(value); + } + return new (this._constructor.bind.apply(this._constructor, values)); +}; + +RecordType.prototype.compare = function (val1, val2) { + var fields = this._fields; + var i, l, field, name, order, type; + for (i = 0, l = fields.length; i < l; i++) { + field = fields[i]; + name = field._name; + order = field._order; + type = field._type; + if (order) { + order *= type.compare(val1[name], val2[name]); + if (order) { + return order; + } + } + } + return 0; +}; + +RecordType.prototype.random = function () { + // jshint -W058 + var fields = this._fields.map(function (f) { return f._type.random(); }); + fields.unshift(undefined); + return new (this._constructor.bind.apply(this._constructor, fields)); +}; + +RecordType.prototype.getAliases = function () { return this._aliases; }; + +RecordType.prototype.getFields = function () { return this._fields.slice(); }; + +RecordType.prototype.getRecordConstructor = function () { + return this._constructor; +}; + +RecordType.prototype.toJSON = function () { + return {name: this._name, type: 'record', fields: this._fields}; +}; + +/** + * Derived type abstract class. + * + */ +function LogicalType(attrs, opts, Types) { + Type.call(this); + LOGICAL_TYPE = this; + this._underlyingType = createType(attrs, opts); + + // Convenience type check. + if (Types && !~Types.indexOf(this._underlyingType.constructor)) { + var lType = attrs.logicalType; + var uType = this._underlyingType; + throw new Error(f('invalid underlying type for %s: %s', lType, uType)); + } +} +util.inherits(LogicalType, Type); + +LogicalType.prototype.getUnderlyingType = function () { + return this._underlyingType; +}; + +LogicalType.prototype._read = function (tap) { + return this._fromValue(this._underlyingType._read(tap)); +}; + +LogicalType.prototype._write = function (tap, val) { + this._underlyingType._write(tap, this._toValue(val)); +}; + +LogicalType.prototype._check = function (val, cb) { + return this._underlyingType._check(this._toValue(val), cb); +}; + +LogicalType.prototype._copy = function (val, opts) { + var type = this._underlyingType; + switch (opts && opts.coerce) { + case 3: // To string. + return type._copy(this._toValue(val), opts); + case 2: // From string. + return this._fromValue(type._copy(val, opts)); + default: // Normal copy. + return this._fromValue(type._copy(this._toValue(val), opts)); + } +}; + +LogicalType.prototype._updateResolver = function (resolver, type, opts) { + var _fromValue = this._resolve(type, opts); + if (_fromValue) { + resolver._read = function (tap) { return _fromValue(type._read(tap)); }; + } +}; + +LogicalType.prototype.random = function () { + return this._fromValue(this._underlyingType.random()); +}; + +LogicalType.prototype.compare = function (obj1, obj2) { + var val1 = this._toValue(obj1); + var val2 = this._toValue(obj2); + return this._underlyingType.compare(val1, val2); +}; + +LogicalType.prototype.toJSON = function () { + return this._underlyingType.toJSON(); +}; + +// Methods to be implemented. +LogicalType.prototype._fromValue = utils.abstractFunction; +LogicalType.prototype._toValue = utils.abstractFunction; +LogicalType.prototype._resolve = utils.abstractFunction; + + +// General helpers. + +/** + * Customizable long. + * + * This allows support of arbitrarily large long (e.g. larger than + * `Number.MAX_SAFE_INTEGER`). See `LongType.using` method above. + * + */ +function AbstractLongType(noUnpack) { + LongType.call(this); + this._noUnpack = !!noUnpack; +} +util.inherits(AbstractLongType, LongType); + +AbstractLongType.prototype._check = function (val, cb) { + var b = this._isValid(val); + if (!b && cb) { + cb(PATH.slice(), val, this); + } + return b; +}; + +AbstractLongType.prototype._read = function (tap) { + var buf, pos; + if (this._noUnpack) { + pos = tap.pos; + tap.skipLong(); + buf = tap.buf.slice(pos, tap.pos); + } else { + buf = tap.unpackLongBytes(tap); + } + if (tap.isValid()) { + return this._fromBuffer(buf); + } +}; + +AbstractLongType.prototype._write = function (tap, val) { + if (!this._isValid(val)) { + throwInvalidError(null, val, this); + } + var buf = this._toBuffer(val); + if (this._noUnpack) { + tap.writeFixed(buf); + } else { + tap.packLongBytes(buf); + } +}; + +AbstractLongType.prototype._copy = function (val, opts) { + switch (opts && opts.coerce) { + case 3: // To string. + return this._toJSON(val); + case 2: // From string. + return this._fromJSON(val); + default: // Normal copy. + // Slow but guarantees most consistent results. Faster alternatives would + // require assumptions on the long class used (e.g. immutability). + return this._fromJSON(JSON.parse(JSON.stringify(this._toJSON(val)))); + } +}; + +AbstractLongType.prototype.random = function () { + return this._fromJSON(LongType.prototype.random()); +}; + +// Methods to be implemented by the user. +AbstractLongType.prototype._fromBuffer = utils.abstractFunction; +AbstractLongType.prototype._toBuffer = utils.abstractFunction; +AbstractLongType.prototype._fromJSON = utils.abstractFunction; +AbstractLongType.prototype._toJSON = utils.abstractFunction; +AbstractLongType.prototype._isValid = utils.abstractFunction; +AbstractLongType.prototype.compare = utils.abstractFunction; + +/** + * Field. + * + * @param attrs {Object} The field's schema. + * @para opts {Object} Schema parsing options (the same as `Type`s'). + * + */ +function Field(attrs, opts) { + var name = attrs.name; + if (typeof name != 'string' || !NAME_PATTERN.test(name)) { + throw new Error(f('invalid field name: %s', name)); + } + + this._name = name; + this._type = createType(attrs.type, opts); + this._aliases = attrs.aliases || []; + + this._order = (function (order) { + switch (order) { + case 'ascending': + return 1; + case 'descending': + return -1; + case 'ignore': + return 0; + default: + throw new Error(f('invalid order: %j', order)); + } + })(attrs.order === undefined ? 'ascending' : attrs.order); + + var value = attrs['default']; + if (value !== undefined) { + // We need to convert defaults back to a valid format (unions are + // disallowed in default definitions, only the first type of each union is + // allowed instead). + // http://apache-avro.679487.n3.nabble.com/field-union-default-in-Java-td1175327.html + var type = this._type; + var val = type._copy(value, {coerce: 2, wrap: 2}); + // The clone call above will throw an error if the default is invalid. + if (type instanceof PrimitiveType && !(type instanceof BytesType)) { + // These are immutable. + this.getDefault = function () { return val; }; + } else { + this.getDefault = function () { return type._copy(val); }; + } + } +} + +Field.prototype.getAliases = function () { return this._aliases; }; + +Field.prototype.getDefault = function () {}; // Undefined default. + +Field.prototype.getName = function () { return this._name; }; + +Field.prototype.getOrder = function () { + return ['descending', 'ignore', 'ascending'][this._order + 1]; +}; + +Field.prototype.getType = function () { return this._type; }; + +Field.prototype.inspect = function () { return f('<Field %j>', this._name); }; + +/** + * Resolver to read a writer's schema as a new schema. + * + * @param readerType {Type} The type to convert to. + * + */ +function Resolver(readerType) { + // Add all fields here so that all resolvers share the same hidden class. + this._readerType = readerType; + this._items = null; + this._read = null; + this._size = 0; + this._symbols = null; + this._values = null; +} + +Resolver.prototype.inspect = function () { return '<Resolver>'; }; + +/** + * Read a value from a tap. + * + * @param type {Type} The type to decode. + * @param tap {Tap} The tap to read from. No checks are performed here. + * @param resolver {Resolver} Optional resolver. It must match the input type. + * @param lazy {Boolean} Skip trailing fields when using a resolver. + * + */ +function readValue(type, tap, resolver, lazy) { + if (resolver) { + if (resolver._readerType !== type) { + throw new Error('invalid resolver'); + } + return resolver._read(tap, lazy); + } else { + return type._read(tap); + } +} + +/** + * Create default parsing options. + * + * @param attrs {Object} Schema to populate options with. + * @param opts {Object} Base options. + * + */ +function getOpts(attrs, opts) { + if (attrs === null) { + // Let's be helpful for this common error. + throw new Error('invalid type: null (did you mean "null"?)'); + } + opts = opts || {}; + opts.registry = opts.registry || {}; + opts.namespace = attrs.namespace || opts.namespace; + opts.logicalTypes = opts.logicalTypes || {}; + return opts; +} + +/** + * Resolve a schema's name and aliases. + * + * @param attrs {Object} True schema (can't be a string). + * @param namespace {String} Optional parent namespace. + * @param key {String} Key where the name should be looked up (defaults to + * `name`). + * + */ +function resolveNames(attrs, namespace, key) { + namespace = attrs.namespace || namespace; + key = key || 'name'; + + var name = attrs[key]; + if (!name) { + throw new Error(f('missing %s property in schema: %j', key, attrs)); + } + return { + name: qualify(name), + aliases: attrs.aliases ? attrs.aliases.map(qualify) : [] + }; + + function qualify(name) { + if (!~name.indexOf('.') && namespace) { + name = namespace + '.' + name; + } + var tail = unqualify(name); + if (isPrimitive(tail)) { + // Primitive types cannot be defined in any namespace. + throw new Error(f('cannot rename primitive type: %j', tail)); + } + name.split('.').forEach(function (part) { + if (!NAME_PATTERN.test(part)) { + throw new Error(f('invalid name: %j', name)); + } + }); + return name; + } +} + +/** + * Remove namespace from a name. + * + * @param name {String} Full or short name. + * + */ +function unqualify(name) { + var parts = name.split('.'); + return parts[parts.length - 1]; +} + +/** + * Get all aliases for a type (including its name). + * + * @param obj {Type|Object} Typically a type or a field. Its aliases property + * must exist and be an array. + * + */ +function getAliases(obj) { + var names = [obj._name]; + var aliases = obj._aliases; + var i, l; + for (i = 0, l = aliases.length; i < l; i++) { + names.push(aliases[i]); + } + return names; +} + +/** + * Get a type's "type" (as a string, e.g. `'record'`, `'string'`). + * + * @param type {Type} Any type. + * + */ +function getTypeName(type) { + var obj = type.toJSON(); + return typeof obj == 'string' ? obj : obj.type; +} + +/** + * Check whether a type's name is a primitive. + * + * @param name {String} Type name (e.g. `'string'`, `'array'`). + * + */ +function isPrimitive(name) { + var type = TYPES[name]; + return type !== undefined && type.prototype instanceof PrimitiveType; +} + +/** + * Get the number of elements in an array block. + * + * @param tap {Tap} A tap positioned at the beginning of an array block. + * + */ +function readArraySize(tap) { + var n = tap.readLong(); + if (n < 0) { + n = -n; + tap.skipLong(); // Skip size. + } + return n; +} + +/** + * Check whether a long can be represented without precision loss. + * + * @param n {Number} The number. + * + * Two things to note: + * + * + We are not using the `Number` constants for compatibility with older + * browsers. + * + We must remove one from each bound because of rounding errors. + * + */ +function isSafeLong(n) { + return n >= -9007199254740990 && n <= 9007199254740990; +} + +/** + * Throw a somewhat helpful error on invalid object. + * + * @param path {Array} Passed from hook, but unused (because empty where this + * function is used, since we aren't keeping track of it for effiency). + * @param val {...} The object to reject. + * @param type {Type} The type to check against. + * + * This method is mostly used from `_write` to signal an invalid object for a + * given type. Note that this provides less information than calling `isValid` + * with a hook since the path is not propagated (for efficiency reasons). + * + */ +function throwInvalidError(path, val, type) { + throw new Error(f('invalid %s: %j', type, val)); +} + + +module.exports = { + createType: createType, + resolveNames: resolveNames, // Protocols use the same name resolution logic. + types: (function () { + // Export the base types along with all concrete implementations. + var obj = {Type: Type, LogicalType: LogicalType}; + var types = Object.keys(TYPES); + var i, l, Class; + for (i = 0, l = types.length; i < l; i++) { + Class = TYPES[types[i]]; + obj[Class.name] = Class; + } + return obj; + })() +};
Added: avro/trunk/lang/js/lib/utils.js URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/lib/utils.js?rev=1717850&view=auto ============================================================================== --- avro/trunk/lang/js/lib/utils.js (added) +++ avro/trunk/lang/js/lib/utils.js Thu Dec 3 21:35:44 2015 @@ -0,0 +1,632 @@ +/* jshint node: true */ + +/** + * 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. + * + */ + +'use strict'; + +/** + * Uppercase the first letter of a string. + * + * @param s {String} The string. + * + */ +function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); } + +/** + * Compare two numbers. + * + * @param n1 {Number} The first one. + * @param n2 {Number} The second one. + * + */ +function compare(n1, n2) { return n1 === n2 ? 0 : (n1 < n2 ? -1 : 1); } + +/** + * Find index of value in array. + * + * @param arr {Array} Can also be a false-ish value. + * @param v {Object} Value to find. + * + * Returns -1 if not found, -2 if found multiple times. + * + */ +function singleIndexOf(arr, v) { + var pos = -1; + var i, l; + if (!arr) { + return -1; + } + for (i = 0, l = arr.length; i < l; i++) { + if (arr[i] === v) { + if (pos >= 0) { + return -2; + } + pos = i; + } + } + return pos; +} + +/** + * Convert array to map. + * + * @param arr {Array} Elements. + * @param fn {Function} Function returning an element's key. + * + */ +function toMap(arr, fn) { + var obj = {}; + var i, elem; + for (i = 0; i < arr.length; i++) { + elem = arr[i]; + obj[fn(elem)] = elem; + } + return obj; +} + +/** + * Check whether an array has duplicates. + * + * @param arr {Array} The array. + * @param fn {Function} Optional function to apply to each element. + * + */ +function hasDuplicates(arr, fn) { + var obj = {}; + var i, l, elem; + for (i = 0, l = arr.length; i < l; i++) { + elem = arr[i]; + if (fn) { + elem = fn(elem); + } + if (obj[elem]) { + return true; + } + obj[elem] = true; + } + return false; +} + +/** + * "Abstract" function to help with "subclassing". + * + */ +function abstractFunction() { throw new Error('abstract'); } + +/** + * Generator of random things. + * + * Inspired by: http://stackoverflow.com/a/424445/1062617 + * + */ +function Lcg(seed) { + var a = 1103515245; + var c = 12345; + var m = Math.pow(2, 31); + var state = Math.floor(seed || Math.random() * (m - 1)); + + this._max = m; + this._nextInt = function () { return state = (a * state + c) % m; }; +} + +Lcg.prototype.nextBoolean = function () { + // jshint -W018 + return !!(this._nextInt() % 2); +}; + +Lcg.prototype.nextInt = function (start, end) { + if (end === undefined) { + end = start; + start = 0; + } + end = end === undefined ? this._max : end; + return start + Math.floor(this.nextFloat() * (end - start)); +}; + +Lcg.prototype.nextFloat = function (start, end) { + if (end === undefined) { + end = start; + start = 0; + } + end = end === undefined ? 1 : end; + return start + (end - start) * this._nextInt() / this._max; +}; + +Lcg.prototype.nextString = function(len, flags) { + len |= 0; + flags = flags || 'aA'; + var mask = ''; + if (flags.indexOf('a') > -1) { + mask += 'abcdefghijklmnopqrstuvwxyz'; + } + if (flags.indexOf('A') > -1) { + mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + } + if (flags.indexOf('#') > -1) { + mask += '0123456789'; + } + if (flags.indexOf('!') > -1) { + mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; + } + var result = []; + for (var i = 0; i < len; i++) { + result.push(this.choice(mask)); + } + return result.join(''); +}; + +Lcg.prototype.nextBuffer = function (len) { + var arr = []; + var i; + for (i = 0; i < len; i++) { + arr.push(this.nextInt(256)); + } + return new Buffer(arr); +}; + +Lcg.prototype.choice = function (arr) { + var len = arr.length; + if (!len) { + throw new Error('choosing from empty array'); + } + return arr[this.nextInt(len)]; +}; + +/** + * Ordered queue which returns items consecutively. + * + * This is actually a heap by index, with the added requirements that elements + * can only be retrieved consecutively. + * + */ +function OrderedQueue() { + this._index = 0; + this._items = []; +} + +OrderedQueue.prototype.push = function (item) { + var items = this._items; + var i = items.length | 0; + var j; + items.push(item); + while (i > 0 && items[i].index < items[j = ((i - 1) >> 1)].index) { + item = items[i]; + items[i] = items[j]; + items[j] = item; + i = j; + } +}; + +OrderedQueue.prototype.pop = function () { + var items = this._items; + var len = (items.length - 1) | 0; + var first = items[0]; + if (!first || first.index > this._index) { + return null; + } + this._index++; + if (!len) { + items.pop(); + return first; + } + items[0] = items.pop(); + var mid = len >> 1; + var i = 0; + var i1, i2, j, item, c, c1, c2; + while (i < mid) { + item = items[i]; + i1 = (i << 1) + 1; + i2 = (i + 1) << 1; + c1 = items[i1]; + c2 = items[i2]; + if (!c2 || c1.index <= c2.index) { + c = c1; + j = i1; + } else { + c = c2; + j = i2; + } + if (c.index >= item.index) { + break; + } + items[j] = item; + items[i] = c; + i = j; + } + return first; +}; + +/** + * A tap is a buffer which remembers what has been already read. + * + * It is optimized for performance, at the cost of failing silently when + * overflowing the buffer. This is a purposeful trade-off given the expected + * rarity of this case and the large performance hit necessary to enforce + * validity. See `isValid` below for more information. + * + */ +function Tap(buf, pos) { + this.buf = buf; + this.pos = pos | 0; +} + +/** + * Check that the tap is in a valid state. + * + * For efficiency reasons, none of the methods below will fail if an overflow + * occurs (either read, skip, or write). For this reason, it is up to the + * caller to always check that the read, skip, or write was valid by calling + * this method. + * + */ +Tap.prototype.isValid = function () { return this.pos <= this.buf.length; }; + +// Read, skip, write methods. +// +// These should fail silently when the buffer overflows. Note this is only +// required to be true when the functions are decoding valid objects. For +// example errors will still be thrown if a bad count is read, leading to a +// negative position offset (which will typically cause a failure in +// `readFixed`). + +Tap.prototype.readBoolean = function () { return !!this.buf[this.pos++]; }; + +Tap.prototype.skipBoolean = function () { this.pos++; }; + +Tap.prototype.writeBoolean = function (b) { this.buf[this.pos++] = !!b; }; + +Tap.prototype.readInt = Tap.prototype.readLong = function () { + var n = 0; + var k = 0; + var buf = this.buf; + var b, h, f, fk; + + do { + b = buf[this.pos++]; + h = b & 0x80; + n |= (b & 0x7f) << k; + k += 7; + } while (h && k < 28); + + if (h) { + // Switch to float arithmetic, otherwise we might overflow. + f = n; + fk = 268435456; // 2 ** 28. + do { + b = buf[this.pos++]; + f += (b & 0x7f) * fk; + fk *= 128; + } while (b & 0x80); + return (f % 2 ? -(f + 1) : f) / 2; + } + + return (n >> 1) ^ -(n & 1); +}; + +Tap.prototype.skipInt = Tap.prototype.skipLong = function () { + var buf = this.buf; + while (buf[this.pos++] & 0x80) {} +}; + +Tap.prototype.writeInt = Tap.prototype.writeLong = function (n) { + var buf = this.buf; + var f, m; + + if (n >= -1073741824 && n < 1073741824) { + // Won't overflow, we can use integer arithmetic. + m = n >= 0 ? n << 1 : (~n << 1) | 1; + do { + buf[this.pos] = m & 0x7f; + m >>= 7; + } while (m && (buf[this.pos++] |= 0x80)); + } else { + // We have to use slower floating arithmetic. + f = n >= 0 ? n * 2 : (-n * 2) - 1; + do { + buf[this.pos] = f & 0x7f; + f /= 128; + } while (f >= 1 && (buf[this.pos++] |= 0x80)); + } + this.pos++; +}; + +Tap.prototype.readFloat = function () { + var buf = this.buf; + var pos = this.pos; + this.pos += 4; + if (this.pos > buf.length) { + return; + } + return this.buf.readFloatLE(pos); +}; + +Tap.prototype.skipFloat = function () { this.pos += 4; }; + +Tap.prototype.writeFloat = function (f) { + var buf = this.buf; + var pos = this.pos; + this.pos += 4; + if (this.pos > buf.length) { + return; + } + return this.buf.writeFloatLE(f, pos); +}; + +Tap.prototype.readDouble = function () { + var buf = this.buf; + var pos = this.pos; + this.pos += 8; + if (this.pos > buf.length) { + return; + } + return this.buf.readDoubleLE(pos); +}; + +Tap.prototype.skipDouble = function () { this.pos += 8; }; + +Tap.prototype.writeDouble = function (d) { + var buf = this.buf; + var pos = this.pos; + this.pos += 8; + if (this.pos > buf.length) { + return; + } + return this.buf.writeDoubleLE(d, pos); +}; + +Tap.prototype.readFixed = function (len) { + var pos = this.pos; + this.pos += len; + if (this.pos > this.buf.length) { + return; + } + var fixed = new Buffer(len); + this.buf.copy(fixed, 0, pos, pos + len); + return fixed; +}; + +Tap.prototype.skipFixed = function (len) { this.pos += len; }; + +Tap.prototype.writeFixed = function (buf, len) { + len = len || buf.length; + var pos = this.pos; + this.pos += len; + if (this.pos > this.buf.length) { + return; + } + buf.copy(this.buf, pos, 0, len); +}; + +Tap.prototype.readBytes = function () { + return this.readFixed(this.readLong()); +}; + +Tap.prototype.skipBytes = function () { + var len = this.readLong(); + this.pos += len; +}; + +Tap.prototype.writeBytes = function (buf) { + var len = buf.length; + this.writeLong(len); + this.writeFixed(buf, len); +}; + +Tap.prototype.readString = function () { + var len = this.readLong(); + var pos = this.pos; + var buf = this.buf; + this.pos += len; + if (this.pos > buf.length) { + return; + } + return this.buf.utf8Slice(pos, pos + len); +}; + +Tap.prototype.skipString = function () { + var len = this.readLong(); + this.pos += len; +}; + +Tap.prototype.writeString = function (s) { + var len = Buffer.byteLength(s); + this.writeLong(len); + var pos = this.pos; + this.pos += len; + if (this.pos > this.buf.length) { + return; + } + this.buf.utf8Write(s, pos, len); +}; + +// Helper used to speed up writing defaults. + +Tap.prototype.writeBinary = function (str, len) { + var pos = this.pos; + this.pos += len; + if (this.pos > this.buf.length) { + return; + } + this.buf.binaryWrite(str, pos, len); +}; + +// Binary comparison methods. +// +// These are not guaranteed to consume the objects they are comparing when +// returning a non-zero result (allowing for performance benefits), so no other +// operations should be done on either tap after a compare returns a non-zero +// value. Also, these methods do not have the same silent failure requirement +// as read, skip, and write since they are assumed to be called on valid +// buffers. + +Tap.prototype.matchBoolean = function (tap) { + return this.buf[this.pos++] - tap.buf[tap.pos++]; +}; + +Tap.prototype.matchInt = Tap.prototype.matchLong = function (tap) { + var n1 = this.readLong(); + var n2 = tap.readLong(); + return n1 === n2 ? 0 : (n1 < n2 ? -1 : 1); +}; + +Tap.prototype.matchFloat = function (tap) { + var n1 = this.readFloat(); + var n2 = tap.readFloat(); + return n1 === n2 ? 0 : (n1 < n2 ? -1 : 1); +}; + +Tap.prototype.matchDouble = function (tap) { + var n1 = this.readDouble(); + var n2 = tap.readDouble(); + return n1 === n2 ? 0 : (n1 < n2 ? -1 : 1); +}; + +Tap.prototype.matchFixed = function (tap, len) { + return this.readFixed(len).compare(tap.readFixed(len)); +}; + +Tap.prototype.matchBytes = Tap.prototype.matchString = function (tap) { + var l1 = this.readLong(); + var p1 = this.pos; + this.pos += l1; + var l2 = tap.readLong(); + var p2 = tap.pos; + tap.pos += l2; + var b1 = this.buf.slice(p1, this.pos); + var b2 = tap.buf.slice(p2, tap.pos); + return b1.compare(b2); +}; + +// Functions for supporting custom long classes. +// +// The two following methods allow the long implementations to not have to +// worry about Avro's zigzag encoding, we directly expose longs as unpacked. + +Tap.prototype.unpackLongBytes = function () { + var res = new Buffer(8); + var n = 0; + var i = 0; // Byte index in target buffer. + var j = 6; // Bit offset in current target buffer byte. + var buf = this.buf; + var b, neg; + + b = buf[this.pos++]; + neg = b & 1; + res.fill(0); + + n |= (b & 0x7f) >> 1; + while (b & 0x80) { + b = buf[this.pos++]; + n |= (b & 0x7f) << j; + j += 7; + if (j >= 8) { + // Flush byte. + j -= 8; + res[i++] = n; + n >>= 8; + } + } + res[i] = n; + + if (neg) { + invert(res, 8); + } + + return res; +}; + +Tap.prototype.packLongBytes = function (buf) { + var neg = (buf[7] & 0x80) >> 7; + var res = this.buf; + var j = 1; + var k = 0; + var m = 3; + var n; + + if (neg) { + invert(buf, 8); + n = 1; + } else { + n = 0; + } + + var parts = [ + buf.readUIntLE(0, 3), + buf.readUIntLE(3, 3), + buf.readUIntLE(6, 2) + ]; + // Not reading more than 24 bits because we need to be able to combine the + // "carry" bits from the previous part and JavaScript only supports bitwise + // operations on 32 bit integers. + while (m && !parts[--m]) {} // Skip trailing 0s. + + // Leading parts (if any), we never bail early here since we need the + // continuation bit to be set. + while (k < m) { + n |= parts[k++] << j; + j += 24; + while (j > 7) { + res[this.pos++] = (n & 0x7f) | 0x80; + n >>= 7; + j -= 7; + } + } + + // Final part, similar to normal packing aside from the initial offset. + n |= parts[m] << j; + do { + res[this.pos] = n & 0x7f; + n >>= 7; + } while (n && (res[this.pos++] |= 0x80)); + this.pos++; + + // Restore original buffer (could make this optional?). + if (neg) { + invert(buf, 8); + } +}; + +// Helpers. + +/** + * Invert all bits in a buffer. + * + * @param buf {Buffer} Non-empty buffer to invert. + * @param len {Number} Buffer length (must be positive). + * + */ +function invert(buf, len) { + while (len--) { + buf[len] = ~buf[len]; + } +} + + +module.exports = { + abstractFunction: abstractFunction, + capitalize: capitalize, + compare: compare, + toMap: toMap, + singleIndexOf: singleIndexOf, + hasDuplicates: hasDuplicates, + Lcg: Lcg, + OrderedQueue: OrderedQueue, + Tap: Tap +}; Modified: avro/trunk/lang/js/package.json URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/package.json?rev=1717850&r1=1717849&r2=1717850&view=diff ============================================================================== --- avro/trunk/lang/js/package.json (original) +++ avro/trunk/lang/js/package.json Thu Dec 3 21:35:44 2015 @@ -1,17 +1,18 @@ { "name": "avro-js", - "version": "0.0.1", + "version": "1.8.0", "author": "Avro Developers <[email protected]>", - "description": "Avro validator for Javascript", + "description": "JavaScript Avro implementation", "contributors": [ { + "name": "Matthieu Monsch", + "email": "[email protected]" + }, + { "name": "Quinn Slack", "email": "[email protected]" - } + } ], - "scripts": { - "test": "grunt test" - }, "repository": { "type": "svn", "url": "http://svn.apache.org/repos/asf/avro/trunk/lang/js/" @@ -20,18 +21,34 @@ "avro", "json" ], - "dependencies" : { - "underscore" : "*" + "files": [ + "LICENSE", + "NOTICE", + "lib", + "etc/browser", + "etc/deprecated/validator.js" + ], + "main": "./lib", + "browser": { + "./lib": "./etc/browser/avro.js", + "crypto": "./etc/browser/crypto.js" + }, + "scripts": { + "test": "mocha --ui tdd --reporter dot", + "clean": "rm -rf node_modules" + }, + "dependencies": { + "underscore": "*" }, - "devDependencies" : { - "grunt" : "*", - "grunt-contrib-jshint" : "*", - "grunt-contrib-nodeunit" : "*", - "grunt-contrib-watch" : "*" + "devDependencies": { + "coveralls": "^2.11.4", + "istanbul": "^0.3.19", + "mocha": "^2.3.2", + "tmp": "^0.0.28" }, "noAnalyze": true, - "license": "Apache", + "license": "Apache-2.0", "engine": { - "node": ">=0.4" + "node": ">=0.11" } } Added: avro/trunk/lang/js/test/dat/Id.avsc URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/test/dat/Id.avsc?rev=1717850&view=auto ============================================================================== --- avro/trunk/lang/js/test/dat/Id.avsc (added) +++ avro/trunk/lang/js/test/dat/Id.avsc Thu Dec 3 21:35:44 2015 @@ -0,0 +1,6 @@ +{ + "type": "fixed", + "name": "Id", + "namespace": "id", + "size": 64 +} Added: avro/trunk/lang/js/test/dat/Person.avsc URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/test/dat/Person.avsc?rev=1717850&view=auto ============================================================================== --- avro/trunk/lang/js/test/dat/Person.avsc (added) +++ avro/trunk/lang/js/test/dat/Person.avsc Thu Dec 3 21:35:44 2015 @@ -0,0 +1,20 @@ +{ + "name": "Person", + "type": "record", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "age", "type": ["null", "int"], "default": null}, + { + "name": "gender", + "type": {"name": "Gender", "type": "enum", "symbols": ["FEMALE", "MALE"]} + }, + { + "name": "address", + "type": { + "name": "Address", + "type": "record", + "fields": [{"name": "zipcode", "type": "int"}] + } + } + ] +} Added: avro/trunk/lang/js/test/dat/person-10.avro URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/test/dat/person-10.avro?rev=1717850&view=auto ============================================================================== Binary files avro/trunk/lang/js/test/dat/person-10.avro (added) and avro/trunk/lang/js/test/dat/person-10.avro Thu Dec 3 21:35:44 2015 differ Added: avro/trunk/lang/js/test/dat/person-10.avro.raw URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/test/dat/person-10.avro.raw?rev=1717850&view=auto ============================================================================== Binary files avro/trunk/lang/js/test/dat/person-10.avro.raw (added) and avro/trunk/lang/js/test/dat/person-10.avro.raw Thu Dec 3 21:35:44 2015 differ Added: avro/trunk/lang/js/test/dat/person-10.no-codec.avro URL: http://svn.apache.org/viewvc/avro/trunk/lang/js/test/dat/person-10.no-codec.avro?rev=1717850&view=auto ============================================================================== Binary files avro/trunk/lang/js/test/dat/person-10.no-codec.avro (added) and avro/trunk/lang/js/test/dat/person-10.no-codec.avro Thu Dec 3 21:35:44 2015 differ
