| /* 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 |
| * |
| * https://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`. |
| 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, |
| 'request': 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.assertLogicalTypes) { |
| 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 (noRef) { |
| return noRef ? getTypeName(this) : this._name; |
| }; |
| |
| Type.prototype.getSchema = function (noDeref) { |
| return stringify(this, noDeref); |
| }; |
| |
| Type.prototype.getFingerprint = function (algorithm) { |
| return utils.getHash(this.getSchema(), algorithm); |
| }; |
| |
| 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 (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 (wrap === 1 && obj === undefined) { |
| // Try promoting 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++; |
| } |
| } |
| } |
| 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; |
| this._type = attrs.type; |
| // Requests shouldn't be registered since their name is only a placeholder. |
| Type.call(this, this._type === 'request' ? undefined : 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, any) { |
| this._underlyingType._write(tap, this._toValue(any)); |
| }; |
| |
| LogicalType.prototype._check = function (any, cb) { |
| try { |
| var val = this._toValue(any); |
| } catch (err) { |
| if (cb) { |
| cb(PATH.slice(), any, this); |
| } |
| return false; |
| } |
| return this._underlyingType._check(val, cb); |
| }; |
| |
| LogicalType.prototype._copy = function (any, opts) { |
| var type = this._underlyingType; |
| switch (opts && opts.coerce) { |
| case 3: // To string. |
| return type._copy(this._toValue(any), opts); |
| case 2: // From string. |
| return this._fromValue(type._copy(any, opts)); |
| default: // Normal copy. |
| return this._fromValue(type._copy(this._toValue(any), 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; |
| } |
| |
| /** |
| * Correctly stringify an object which contains types. |
| * |
| * @param obj {Object} The object to stringify. Typically, a type itself or an |
| * object containing types. Any types inside will be expanded only once then |
| * referenced by name. |
| * @param noDeref {Boolean} Always reference types by name when possible, |
| * rather than expand it the first time it is encountered. |
| * |
| */ |
| function stringify(obj, 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: https://stackoverflow.com/q/5525795/1062617). |
| return (function (registry) { |
| return JSON.stringify(obj, 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; |
| }); |
| })({}); |
| } |
| |
| /** |
| * 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. |
| stringify: stringify, |
| 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; |
| })() |
| }; |