UNPKG

avsc

Version:
1,731 lines (1,536 loc) 94.8 kB
/* jshint node: true */ // TODO: Make it easier to implement custom types. This will likely require // exposing the `Tap` object, perhaps under another name. Probably worth a // major release. // TODO: Allow configuring when to write the size when writing arrays and maps, // and customizing their block size. // TODO: Code-generate `compare` and `clone` record and union methods. 'use strict'; /** * This module defines all Avro data types and their serialization logic. * */ var utils = require('./utils'), buffer = require('buffer'), util = require('util'); var Buffer = buffer.Buffer; // Convenience imports. var Tap = utils.Tap; var debug = util.debuglog('avsc:types'); var f = util.format; // All non-union concrete (i.e. non-logical) 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 }; // Random generator. var RANDOM = new utils.Lcg(); // Encoding tap (shared for performance). var TAP = new Tap(utils.newSlowBuffer(1024)); // Currently active logical type, used for name redirection. var LOGICAL_TYPE = null; // Underlying types of logical types currently being instantiated. This is used // to be able to reference names (i.e. for branches) during instantiation. var UNDERLYING_TYPES = []; /** * "Abstract" base Avro type. * * 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. * + `union`s which will be "unwrapped" unless the `wrapUnions` option is set. * * See individual subclasses for details. */ function Type(schema, opts) { var type; if (LOGICAL_TYPE) { type = LOGICAL_TYPE; UNDERLYING_TYPES.push([LOGICAL_TYPE, this]); LOGICAL_TYPE = null; } else { type = this; } // Lazily instantiated hash string. It will be generated the first time the // type's default fingerprint is computed (for example when using `equals`). // We use a mutable object since types are frozen after instantiation. this._hash = new Hash(); this.name = undefined; this.aliases = undefined; this.doc = (schema && schema.doc) ? '' + schema.doc : undefined; if (schema) { // This is a complex (i.e. non-primitive) type. var name = schema.name; var namespace = schema.namespace === undefined ? opts && opts.namespace : schema.namespace; if (name !== undefined) { // This isn't an anonymous type. name = maybeQualify(name, namespace); if (isPrimitive(name)) { // Avro doesn't allow redefining primitive names. throw new Error(f('cannot rename primitive type: %j', name)); } var registry = opts && opts.registry; if (registry) { if (registry[name] !== undefined) { throw new Error(f('duplicate type name: %s', name)); } registry[name] = type; } } else if (opts && opts.noAnonymousTypes) { throw new Error(f('missing name property in schema: %j', schema)); } this.name = name; this.aliases = schema.aliases ? schema.aliases.map(function (s) { return maybeQualify(s, namespace); }) : []; } } Type.forSchema = function (schema, opts) { opts = opts || {}; opts.registry = opts.registry || {}; var UnionType = (function (wrapUnions) { if (wrapUnions === true) { wrapUnions = 'always'; } else if (wrapUnions === false) { wrapUnions = 'never'; } else if (wrapUnions === undefined) { wrapUnions = 'auto'; } else if (typeof wrapUnions == 'string') { wrapUnions = wrapUnions.toLowerCase(); } switch (wrapUnions) { case 'always': return WrappedUnionType; case 'never': return UnwrappedUnionType; case 'auto': return undefined; // Determined dynamically later on. default: throw new Error(f('invalid wrap unions option: %j', wrapUnions)); } })(opts.wrapUnions); if (schema === null) { // Let's be helpful for this common error. throw new Error('invalid type: null (did you mean "null"?)'); } if (Type.isType(schema)) { return schema; } var type; if (opts.typeHook && (type = opts.typeHook(schema, opts))) { if (!Type.isType(type)) { throw new Error(f('invalid typehook return value: %j', type)); } return type; } if (typeof schema == 'string') { // Type reference. schema = maybeQualify(schema, opts.namespace); type = opts.registry[schema]; if (type) { // Type was already defined, return it. return type; } if (isPrimitive(schema)) { // 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[schema] = Type.forSchema({type: schema}, opts); } throw new Error(f('undefined type name: %s', schema)); } if (schema.logicalType && opts.logicalTypes && !LOGICAL_TYPE) { var DerivedType = opts.logicalTypes[schema.logicalType]; if (DerivedType) { var namespace = opts.namespace; var registry = {}; Object.keys(opts.registry).forEach(function (key) { registry[key] = opts.registry[key]; }); try { debug('instantiating logical type for %s', schema.logicalType); return new DerivedType(schema, opts); } catch (err) { debug('failed to instantiate logical type for %s', schema.logicalType); if (opts.assertLogicalTypes) { // 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.namespace = namespace; opts.registry = registry; } } } if (Array.isArray(schema)) { // Union. // We temporarily clear the logical type since we instantiate the branch's // types before the underlying union's type (necessary to decide whether the // union is ambiguous or not). var logicalType = LOGICAL_TYPE; LOGICAL_TYPE = null; var types = schema.map(function (obj) { return Type.forSchema(obj, opts); }); if (!UnionType) { UnionType = isAmbiguous(types) ? WrappedUnionType : UnwrappedUnionType; } LOGICAL_TYPE = logicalType; type = new UnionType(types, 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(schema, opts); })(schema.type); } return type; }; Type.forValue = function (val, opts) { opts = opts || {}; // Sentinel used when inferring the types of empty arrays. opts.emptyArrayType = opts.emptyArrayType || Type.forSchema({ type: 'array', items: 'null' }); // Optional custom inference hook. if (opts.valueHook) { var type = opts.valueHook(val, opts); if (type !== undefined) { if (!Type.isType(type)) { throw new Error(f('invalid value hook return value: %j', type)); } return type; } } // Default inference logic. switch (typeof val) { case 'string': return Type.forSchema('string', opts); case 'boolean': return Type.forSchema('boolean', opts); case 'number': if ((val | 0) === val) { return Type.forSchema('int', opts); } else if (Math.abs(val) < 9007199254740991) { return Type.forSchema('float', opts); } return Type.forSchema('double', opts); case 'object': if (val === null) { return Type.forSchema('null', opts); } else if (Array.isArray(val)) { if (!val.length) { return opts.emptyArrayType; } return Type.forSchema({ type: 'array', items: Type.forTypes( val.map(function (v) { return Type.forValue(v, opts); }), opts ) }, opts); } else if (Buffer.isBuffer(val)) { return Type.forSchema('bytes', opts); } var fieldNames = Object.keys(val); if (fieldNames.some(function (s) { return !utils.isValidName(s); })) { // We have to fall back to a map. return Type.forSchema({ type: 'map', values: Type.forTypes(fieldNames.map(function (s) { return Type.forValue(val[s], opts); }), opts) }, opts); } return Type.forSchema({ type: 'record', fields: fieldNames.map(function (s) { return {name: s, type: Type.forValue(val[s], opts)}; }) }, opts); default: throw new Error(f('cannot infer type from: %j', val)); } }; Type.forTypes = function (types, opts) { if (!types.length) { throw new Error('no types to combine'); } if (types.length === 1) { return types[0]; // Nothing to do. } opts = opts || {}; // Extract any union types, with special care for wrapped unions (see below). var expanded = []; var numWrappedUnions = 0; var isValidWrappedUnion = true; types.forEach(function (type) { switch (type.typeName) { case 'union:unwrapped': isValidWrappedUnion = false; expanded = expanded.concat(type.types); break; case 'union:wrapped': numWrappedUnions++; expanded = expanded.concat(type.types); break; case 'null': expanded.push(type); break; default: isValidWrappedUnion = false; expanded.push(type); } }); if (numWrappedUnions) { if (!isValidWrappedUnion) { // It is only valid to combine wrapped unions when no other type is // present other than wrapped unions and nulls (otherwise the values of // others wouldn't be valid in the resulting union). throw new Error('cannot combine wrapped union'); } var branchTypes = {}; expanded.forEach(function (type) { var name = type.branchName; var branchType = branchTypes[name]; if (!branchType) { branchTypes[name] = type; } else if (!type.equals(branchType)) { throw new Error('inconsistent branch type'); } }); var wrapUnions = opts.wrapUnions; var unionType; opts.wrapUnions = true; try { unionType = Type.forSchema(Object.keys(branchTypes).map(function (name) { return branchTypes[name]; }), opts); } catch (err) { opts.wrapUnions = wrapUnions; throw err; } opts.wrapUnions = wrapUnions; return unionType; } // Group types by category, similar to the logic for unwrapped unions. var bucketized = {}; expanded.forEach(function (type) { var bucket = getTypeBucket(type); var bucketTypes = bucketized[bucket]; if (!bucketTypes) { bucketized[bucket] = bucketTypes = []; } bucketTypes.push(type); }); // Generate the "augmented" type for each group. var buckets = Object.keys(bucketized); var augmented = buckets.map(function (bucket) { var bucketTypes = bucketized[bucket]; if (bucketTypes.length === 1) { return bucketTypes[0]; } else { switch (bucket) { case 'null': case 'boolean': return bucketTypes[0]; case 'number': return combineNumbers(bucketTypes); case 'string': return combineStrings(bucketTypes, opts); case 'buffer': return combineBuffers(bucketTypes, opts); case 'array': // Remove any sentinel arrays (used when inferring from empty arrays) // to avoid making things nullable when they shouldn't be. bucketTypes = bucketTypes.filter(function (t) { return t !== opts.emptyArrayType; }); if (!bucketTypes.length) { // We still don't have a real type, just return the sentinel. return opts.emptyArrayType; } return Type.forSchema({ type: 'array', items: Type.forTypes(bucketTypes.map(function (t) { return t.itemsType; }), opts) }, opts); default: return combineObjects(bucketTypes, opts); } } }); if (augmented.length === 1) { return augmented[0]; } else { // We return an (unwrapped) union of all augmented types. return Type.forSchema(augmented, opts); } }; Type.isType = function (/* any, [prefix] ... */) { var l = arguments.length; if (!l) { return false; } var any = arguments[0]; if ( !any || typeof any._update != 'function' || typeof any.fingerprint != 'function' ) { // Not fool-proof, but most likely good enough. return false; } if (l === 1) { // No type names specified, we are done. return true; } // We check if at least one of the prefixes matches. var typeName = any.typeName; var i; for (i = 1; i < l; i++) { if (typeName.indexOf(arguments[i]) === 0) { return true; } } return false; }; Type.__reset = function (size) { debug('resetting type buffer to %d', size); TAP.buf = utils.newSlowBuffer(size); }; Object.defineProperty(Type.prototype, 'branchName', { enumerable: true, get: function () { var type = Type.isType(this, 'logical') ? this.underlyingType : this; if (type.name) { return type.name; } if (Type.isType(type, 'abstract')) { return type._concreteTypeName; } return Type.isType(type, 'union') ? undefined : type.typeName; } }); Type.prototype.clone = function (val, opts) { if (opts) { opts = { coerce: !!opts.coerceBuffers | 0, // Coerce JSON to Buffer. fieldHook: opts.fieldHook, qualifyNames: !!opts.qualifyNames, skip: !!opts.skipMissingFields, wrap: !!opts.wrapUnions | 0 // Wrap first match into union. }; return this._copy(val, opts); } else { // If no modifications are required, we can get by with a serialization // roundtrip (generally much faster than a standard deep copy). return this.fromBuffer(this.toBuffer(val)); } }; Type.prototype.compare = utils.abstractFunction; Type.prototype.compareBuffers = function (buf1, buf2) { return this._match(new Tap(buf1), new Tap(buf2)); }; Type.prototype.createResolver = function (type, opts) { if (!Type.isType(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.isType(this, 'union', 'logical') && Type.isType(type, 'logical')) { // Trying to read a logical type as a built-in: unwrap the logical type. // Note that we exclude unions to support resolving into unions containing // logical types. return this.createResolver(type.underlyingType, opts); } opts = opts || {}; opts.registry = opts.registry || {}; var resolver, key; if ( Type.isType(this, 'record', 'error') && Type.isType(type, 'record', 'error') ) { // We allow conversions between records and errors. 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.isType(type, 'union')) { 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._update(resolver, type, opts); } if (!resolver._read) { throw new Error(f('cannot read %s as %s', type, this)); } return Object.freeze(resolver); }; Type.prototype.decode = function (buf, pos, resolver) { var tap = new Tap(buf, pos); 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, pos); 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.equals = function (type, opts) { var canon = ( // Canonical equality. Type.isType(type) && this.fingerprint().equals(type.fingerprint()) ); if (!canon || !(opts && opts.strict)) { return canon; } return ( JSON.stringify(this.schema({exportAttrs: true})) === JSON.stringify(type.schema({exportAttrs: true})) ); }; Type.prototype.fingerprint = function (algorithm) { if (!algorithm) { if (!this._hash.str) { var schemaStr = JSON.stringify(this.schema()); this._hash.str = utils.getHash(schemaStr).toString('binary'); } return utils.bufferFrom(this._hash.str, 'binary'); } else { return utils.getHash(JSON.stringify(this.schema()), algorithm); } }; 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.fromString = function (str) { return this._copy(JSON.parse(str), {coerce: 2}); }; Type.prototype.inspect = function () { var typeName = this.typeName; var className = getClassName(typeName); if (isPrimitive(typeName)) { // The class name is sufficient to identify the type. return f('<%s>', className); } else { // We add a little metadata for convenience. var obj = this.schema({exportAttrs: true, noDeref: true}); if (typeof obj == 'object' && !Type.isType(this, 'logical')) { obj.type = undefined; // Would be redundant with constructor name. } return f('<%s %j>', className, obj); } }; Type.prototype.isValid = function (val, opts) { // We only have a single flag for now, so no need to complicate things. var flags = (opts && opts.noUndeclaredFields) | 0; var errorHook = opts && opts.errorHook; var hook, path; if (errorHook) { path = []; hook = function (any, type) { errorHook.call(this, path.slice(), any, type, val); }; } return this._check(val, flags, hook, path); }; Type.prototype.random = utils.abstractFunction; Type.prototype.schema = function (opts) { // Copy the options to avoid mutating the original options object when we add // the registry of dereferenced types. return this._attrs({ exportAttrs: !!(opts && opts.exportAttrs), noDeref: !!(opts && opts.noDeref) }); }; Type.prototype.toBuffer = function (val) { TAP.pos = 0; this._write(TAP, val); var buf = utils.newBuffer(TAP.pos); if (TAP.isValid()) { TAP.buf.copy(buf, 0, 0, TAP.pos); } else { this._write(new Tap(buf), val); } return buf; }; Type.prototype.toJSON = function () { // Convenience to allow using `JSON.stringify(type)` to get a type's schema. return this.schema({exportAttrs: true}); }; Type.prototype.toString = function (val) { if (val === undefined) { // Consistent behavior with standard `toString` expectations. return JSON.stringify(this.schema({noDeref: true})); } return JSON.stringify(this._copy(val, {coerce: 3})); }; Type.prototype.wrap = function (val) { var Branch = this._branchConstructor; return Branch === null ? null : new Branch(val); }; Type.prototype._attrs = function (opts) { // This function handles a lot of the common logic to schema generation // across types, for example keeping track of which types have already been // de-referenced (i.e. derefed). opts.derefed = opts.derefed || {}; var name = this.name; if (name !== undefined) { if (opts.noDeref || opts.derefed[name]) { return name; } opts.derefed[name] = true; } var schema = {}; // The order in which we add fields to the `schema` object matters here. // 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 (see // http://stackoverflow.com/q/5525795/1062617). if (this.name !== undefined) { schema.name = name; } schema.type = this.typeName; var derefedSchema = this._deref(schema, opts); if (derefedSchema !== undefined) { // We allow the original schema to be overridden (this will happen for // primitive types and logical types). schema = derefedSchema; } if (opts.exportAttrs) { if (this.aliases && this.aliases.length) { schema.aliases = this.aliases; } if (this.doc !== undefined) { schema.doc = this.doc; } } return schema; }; Type.prototype._createBranchConstructor = function () { // jshint -W054 var name = this.branchName; if (name === 'null') { return null; } var attr = ~name.indexOf('.') ? 'this[\'' + name + '\']' : 'this.' + name; var body = 'return function Branch$(val) { ' + attr + ' = val; };'; var Branch = (new Function(body))(); Branch.type = this; Branch.prototype.unwrap = new Function('return ' + attr + ';'); Branch.prototype.unwrapped = Branch.prototype.unwrap; // Deprecated. return Branch; }; Type.prototype._peek = function (tap) { var pos = tap.pos; var val = this._read(tap); tap.pos = pos; return val; }; Type.prototype._check = utils.abstractFunction; Type.prototype._copy = utils.abstractFunction; Type.prototype._deref = utils.abstractFunction; Type.prototype._match = utils.abstractFunction; Type.prototype._read = utils.abstractFunction; Type.prototype._skip = utils.abstractFunction; Type.prototype._update = utils.abstractFunction; Type.prototype._write = utils.abstractFunction; // "Deprecated" getters (will be explicitly deprecated in 5.1). Type.prototype.getAliases = function () { return this.aliases; }; Type.prototype.getFingerprint = Type.prototype.fingerprint; Type.prototype.getName = function (asBranch) { return (this.name || !asBranch) ? this.name : this.branchName; }; Type.prototype.getSchema = Type.prototype.schema; Type.prototype.getTypeName = function () { return this.typeName; }; // 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(noFreeze) { Type.call(this); this._branchConstructor = this._createBranchConstructor(); if (!noFreeze) { // Abstract long types can't be frozen at this stage. Object.freeze(this); } } util.inherits(PrimitiveType, Type); PrimitiveType.prototype._update = function (resolver, type) { if (type.typeName === this.typeName) { resolver._read = this._read; } }; PrimitiveType.prototype._copy = function (val) { this._check(val, undefined, throwInvalidError); return val; }; PrimitiveType.prototype._deref = function () { return this.typeName; }; PrimitiveType.prototype.compare = utils.compare; /** Nulls. */ function NullType() { PrimitiveType.call(this); } util.inherits(NullType, PrimitiveType); NullType.prototype._check = function (val, flags, hook) { var b = val === null; if (!b && hook) { hook(val, this); } return b; }; NullType.prototype._read = function () { return null; }; NullType.prototype._skip = function () {}; NullType.prototype._write = function (tap, val) { if (val !== null) { throwInvalidError(val, this); } }; NullType.prototype._match = function () { return 0; }; NullType.prototype.compare = NullType.prototype._match; NullType.prototype.typeName = 'null'; NullType.prototype.random = NullType.prototype._read; /** Booleans. */ function BooleanType() { PrimitiveType.call(this); } util.inherits(BooleanType, PrimitiveType); BooleanType.prototype._check = function (val, flags, hook) { var b = typeof val == 'boolean'; if (!b && hook) { hook(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(val, this); } tap.writeBoolean(val); }; BooleanType.prototype._match = function (tap1, tap2) { return tap1.matchBoolean(tap2); }; BooleanType.prototype.typeName = 'boolean'; BooleanType.prototype.random = function () { return RANDOM.nextBoolean(); }; /** Integers. */ function IntType() { PrimitiveType.call(this); } util.inherits(IntType, PrimitiveType); IntType.prototype._check = function (val, flags, hook) { var b = val === (val | 0); if (!b && hook) { hook(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(val, this); } tap.writeInt(val); }; IntType.prototype._match = function (tap1, tap2) { return tap1.matchInt(tap2); }; IntType.prototype.typeName = 'int'; IntType.prototype.random = function () { return RANDOM.nextInt(1000) | 0; }; /** * 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 `__with` 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, flags, hook) { var b = typeof val == 'number' && val % 1 === 0 && isSafeLong(val); if (!b && hook) { hook(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(val, this); } tap.writeLong(val); }; LongType.prototype._match = function (tap1, tap2) { return tap1.matchLong(tap2); }; LongType.prototype._update = function (resolver, type) { switch (type.typeName) { case 'int': resolver._read = type._read; break; case 'abstract:long': case 'long': resolver._read = this._read; // In case `type` is an `AbstractLongType`. } }; LongType.prototype.typeName = 'long'; LongType.prototype.random = function () { return RANDOM.nextInt(); }; LongType.__with = 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 Object.freeze(type); }; /** Floats. */ function FloatType() { PrimitiveType.call(this); } util.inherits(FloatType, PrimitiveType); FloatType.prototype._check = function (val, flags, hook) { var b = typeof val == 'number'; if (!b && hook) { hook(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(val, this); } tap.writeFloat(val); }; FloatType.prototype._match = function (tap1, tap2) { return tap1.matchFloat(tap2); }; FloatType.prototype._update = function (resolver, type) { switch (type.typeName) { case 'float': case 'int': resolver._read = type._read; break; case 'abstract:long': case 'long': // No need to worry about precision loss here since we're always rounding // to float anyway. resolver._read = function (tap) { return tap.readLong(); }; } }; FloatType.prototype.typeName = 'float'; FloatType.prototype.random = function () { return RANDOM.nextFloat(1e3); }; /** Doubles. */ function DoubleType() { PrimitiveType.call(this); } util.inherits(DoubleType, PrimitiveType); DoubleType.prototype._check = function (val, flags, hook) { var b = typeof val == 'number'; if (!b && hook) { hook(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(val, this); } tap.writeDouble(val); }; DoubleType.prototype._match = function (tap1, tap2) { return tap1.matchDouble(tap2); }; DoubleType.prototype._update = function (resolver, type) { switch (type.typeName) { case 'double': case 'float': case 'int': resolver._read = type._read; break; case 'abstract:long': case 'long': // Similar to inside `FloatType`, no need to worry about precision loss // here since we're always rounding to double anyway. resolver._read = function (tap) { return tap.readLong(); }; } }; DoubleType.prototype.typeName = 'double'; DoubleType.prototype.random = function () { return RANDOM.nextFloat(); }; /** Strings. */ function StringType() { PrimitiveType.call(this); } util.inherits(StringType, PrimitiveType); StringType.prototype._check = function (val, flags, hook) { var b = typeof val == 'string'; if (!b && hook) { hook(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(val, this); } tap.writeString(val); }; StringType.prototype._match = function (tap1, tap2) { return tap1.matchString(tap2); }; StringType.prototype._update = function (resolver, type) { switch (type.typeName) { case 'bytes': case 'string': resolver._read = this._read; } }; StringType.prototype.typeName = 'string'; StringType.prototype.random = function () { return RANDOM.nextString(RANDOM.nextInt(32)); }; /** * 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, flags, hook) { var b = Buffer.isBuffer(val); if (!b && hook) { hook(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(val, this); } tap.writeBytes(val); }; BytesType.prototype._match = function (tap1, tap2) { return tap1.matchBytes(tap2); }; BytesType.prototype._update = StringType.prototype._update; BytesType.prototype._copy = function (obj, opts) { var buf; switch ((opts && opts.coerce) | 0) { case 3: // Coerce buffers to strings. this._check(obj, undefined, 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 = utils.bufferFrom(obj, 'binary'); this._check(buf, undefined, throwInvalidError); return buf; case 1: // Coerce buffer JSON representation to buffers. if (!isJsonBuffer(obj)) { throw new Error(f('cannot coerce to buffer: %j', obj)); } buf = utils.bufferFrom(obj.data); this._check(buf, undefined, throwInvalidError); return buf; default: // Copy buffer. this._check(obj, undefined, throwInvalidError); return utils.bufferFrom(obj); } }; BytesType.prototype.compare = Buffer.compare; BytesType.prototype.typeName = 'bytes'; BytesType.prototype.random = function () { return RANDOM.nextBuffer(RANDOM.nextInt(32)); }; /** Base "abstract" Avro union type. */ function UnionType(schema, opts) { Type.call(this); if (!Array.isArray(schema)) { throw new Error(f('non-array union schema: %j', schema)); } if (!schema.length) { throw new Error('empty union'); } this.types = Object.freeze(schema.map(function (obj) { return Type.forSchema(obj, opts); })); this._branchIndices = {}; this.types.forEach(function (type, i) { if (Type.isType(type, 'union')) { throw new Error('unions cannot be directly nested'); } var branch = type.branchName; if (this._branchIndices[branch] !== undefined) { throw new Error(f('duplicate union branch name: %j', branch)); } this._branchIndices[branch] = i; }, this); } util.inherits(UnionType, Type); UnionType.prototype._branchConstructor = function () { throw new Error('unions cannot be directly wrapped'); }; UnionType.prototype._skip = function (tap) { this.types[tap.readLong()]._skip(tap); }; 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._deref = function (schema, opts) { return this.types.map(function (t) { return t._attrs(opts); }); }; UnionType.prototype.getTypes = function () { return this.types; }; /** * "Natural" union type. * * This representation doesn't require a wrapping object and is therefore * simpler and generally closer to what users expect. However it cannot be used * to represent all Avro unions since some lead to ambiguities (e.g. if two * number types are in the union). * * Currently, this union supports at most one type in each of the categories * below: * * + `null` * + `boolean` * + `int`, `long`, `float`, `double` * + `string`, `enum` * + `bytes`, `fixed` * + `array` * + `map`, `record` */ function UnwrappedUnionType(schema, opts) { UnionType.call(this, schema, opts); this._dynamicBranches = null; this._bucketIndices = {}; this.types.forEach(function (type, index) { if (Type.isType(type, 'abstract', 'logical')) { if (!this._dynamicBranches) { this._dynamicBranches = []; } this._dynamicBranches.push({index: index, type: type}); } else { var bucket = getTypeBucket(type); if (this._bucketIndices[bucket] !== undefined) { throw new Error(f('ambiguous unwrapped union: %j', this)); } this._bucketIndices[bucket] = index; } }, this); Object.freeze(this); } util.inherits(UnwrappedUnionType, UnionType); UnwrappedUnionType.prototype._getIndex = function (val) { var index = this._bucketIndices[getValueBucket(val)]; if (this._dynamicBranches) { // Slower path, we must run the value through all branches. index = this._getBranchIndex(val, index); } return index; }; UnwrappedUnionType.prototype._getBranchIndex = function (any, index) { var logicalBranches = this._dynamicBranches; var i, l, branch; for (i = 0, l = logicalBranches.length; i < l; i++) { branch = logicalBranches[i]; if (branch.type._check(any)) { if (index === undefined) { index = branch.index; } else { // More than one branch matches the value so we aren't guaranteed to // infer the correct type. We throw rather than corrupt data. This can // be fixed by "tightening" the logical types. throw new Error('ambiguous conversion'); } } } return index; }; UnwrappedUnionType.prototype._check = function (val, flags, hook, path) { var index = this._getIndex(val); var b = index !== undefined; if (b) { return this.types[index]._check(val, flags, hook, path); } if (hook) { hook(val, this); } return b; }; UnwrappedUnionType.prototype._read = function (tap) { var index = tap.readLong(); var branchType = this.types[index]; if (branchType) { return branchType._read(tap); } else { throw new Error(f('invalid union index: %s', index)); } }; UnwrappedUnionType.prototype._write = function (tap, val) { var index = this._getIndex(val); if (index === undefined) { throwInvalidError(val, this); } tap.writeLong(index); if (val !== null) { this.types[index]._write(tap, val); } }; UnwrappedUnionType.prototype._update = function (resolver, type, opts) { // jshint -W083 // (The loop exits after the first function is created.) var i, l, typeResolver; for (i = 0, l = this.types.length; i < l; i++) { try { typeResolver = this.types[i].createResolver(type, opts); } catch (err) { continue; } resolver._read = function (tap) { return typeResolver._read(tap); }; return; } }; UnwrappedUnionType.prototype._copy = function (val, opts) { var coerce = opts && opts.coerce | 0; var wrap = opts && opts.wrap | 0; var index; if (wrap === 2) { // We are parsing a default, so always use the first branch's type. index = 0; } else { switch (coerce) { case 1: // Using the `coerceBuffers` option can cause corruption and erroneous // failures with unwrapped unions (in rare cases when the union also // contains a record which matches a buffer's JSON representation). if (isJsonBuffer(val) && this._bucketIndices.buffer !== undefined) { index = this._bucketIndices.buffer; } else { index = this._getIndex(val); } break; case 2: // Decoding from JSON, we must unwrap the value. if (val === null) { index = this._bucketIndices['null']; } else if (typeof val === 'object') { var keys = Object.keys(val); if (keys.length === 1) { index = this._branchIndices[keys[0]]; val = val[keys[0]]; } } break; default: index = this._getIndex(val); } if (index === undefined) { throwInvalidError(val, this); } } var type = this.types[index]; if (val === null || wrap === 3) { return type._copy(val, opts); } else { switch (coerce) { case 3: // Encoding to JSON, we wrap the value. var obj = {}; obj[type.branchName] = type._copy(val, opts); return obj; default: return type._copy(val, opts); } } }; UnwrappedUnionType.prototype.compare = function (val1, val2) { var index1 = this._getIndex(val1); var index2 = this._getIndex(val2); if (index1 === undefined) { throwInvalidError(val1, this); } else if (index2 === undefined) { throwInvalidError(val2, this); } else if (index1 === index2) { return this.types[index1].compare(val1, val2); } else { return utils.compare(index1, index2); } }; UnwrappedUnionType.prototype.typeName = 'union:unwrapped'; UnwrappedUnionType.prototype.random = function () { var index = RANDOM.nextInt(this.types.length); return this.types[index].random(); }; /** * Compatible union type. * * Values of this type 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). */ function WrappedUnionType(schema, opts) { UnionType.call(this, schema, opts); Object.freeze(this); } util.inherits(WrappedUnionType, UnionType); WrappedUnionType.prototype._check = function (val, flags, hook, path) { var b = false; if (val === null) { // Shortcut type lookup in this case. b = this._branchIndices['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._branchIndices[name]; if (index !== undefined) { if (hook) { // Slow path. path.push(name); b = this.types[index]._check(val[name], flags, hook, path); path.pop(); return b; } else { return this.types[index]._check(val[name], flags); } } } } if (!b && hook) { hook(val, this); } return b; }; WrappedUnionType.prototype._read = function (tap) { var type = this.types[tap.readLong()]; if (!type) { throw new Error(f('invalid union index')); } var Branch = type._branchConstructor; if (Branch === null) { return null; } else { return new Branch(type._read(tap)); } }; WrappedUnionType.prototype._write = function (tap, val) { var index, keys, name; if (val === null) { index = this._branchIndices['null']; if (index === undefined) { throwInvalidError(val, this); } tap.writeLong(index); } else { keys = Object.keys(val); if (keys.length === 1) { name = keys[0]; index = this._branchIndices[name]; } if (index === undefined) { throwInvalidError(val, this); } tap.writeLong(index); this.types[index]._write(tap, val[name]); } }; WrappedUnionType.prototype._update = function (resolver, type, opts) { // jshint -W083 // (The loop exits after the first function is created.) var i, l, typeResolver, Branch; for (i = 0, l = this.types.length; i < l; i++) { try { typeResolver = this.types[i].createResolver(type, opts); } catch (err) { continue; } Branch = this.types[i]._branchConstructor; if (Branch) { resolver._read = function (tap) { return new Branch(typeResolver._read(tap)); }; } else { resolver._read = function () { return null; }; } return; } }; WrappedUnionType.prototype._copy = function (val, opts) { var wrap = opts && opts.wrap | 0; if (wrap === 2) { var firstType = this.types[0]; // Promote into first type (used for schema defaults). if (val === null && firstType.typeName === 'null') { return null; } return new firstType._branchConstructor(firstType._copy(val, opts)); } if (val === null && this._branchIndices['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._branchIndices[name]; if (i === undefined && opts.qualifyNames) { // 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 === utils.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 wrap === 3 ? obj : new this.types[i]._branchConstructor(obj); } throwInvalidError(val, this); }; WrappedUnionType.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._branchIndices[name1]; if (name1 === name2) { return name1 === 'null' ? 0 : this.types[index].compare(val1[name1], val2[name1]); } else { return utils.compare(index, this._branchIndices[name2]); } }; WrappedUnionType.prototype.typeName = 'union:wrapped'; WrappedUnionType.prototype.random = function () { var index = RANDOM.nextInt(this.types.length); var type = this.types[index]; var Branch = type._branchConstructor; if (!Branch) { return null; } return new Branch(type.random()); }; /** * 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(schema, opts) { Type.call(this, schema, opts); if (!Array.isArray(schema.symbols) || !schema.symbols.length) { throw new Error(f('invalid enum symbols: %j', schema.symbols)); } this.symbols = Object.freeze(schema.symbols.slice()); this._indices = {}; this.symbols.forEach(function (symbol, i) { if (!utils.isValidName(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); this.default = schema.default; if (this.default !== undefined && this._indices[this.default] === undefined) { throw new Error(f('invalid %s default: %j', this, this.default)); } this._branchConstructor = this._createBranchConstructor(); Object.freeze(this); } util.inherits(EnumType, Type); EnumType.prototype._check = function (val, flags, hook) { var b = this._indices[val] !== undefined; if (!b && hook) { hook(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(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._update = function (resolver, type, opts) { var symbols = this.symbols; if ( type.typeName === 'enum' && hasCompatibleName(this, type, !opts.ignoreNamespaces) && ( type.symbols.every(function (s) { return ~symbols.indexOf(s); }) || this.default !== undefined ) ) { resolver.symbols = type.symbols.map(function (s) { return this._indices[s] === undefined ? this.default : s; }, this); resolver._read = type._read; } }; EnumType.prototype._copy = function (val) { this._check(val, undefined, throwInvalidError); return val; }; EnumType.prototype._deref = function (schema) { schema.symbols = this.symbols; }; EnumType.prototype.getSymbols = function () { return this.symbols; }; EnumType.prototype.typeName = 'enum'; EnumType.prototype.random = function () { return RANDOM.choice(this.symbols); }; /** Avro fixed type. Represented simply as a `Buffer`. */ function FixedType(schema, opts) { Type.call(this, schema, opts); if (schema.size !== (schema.size | 0) || schema.size < 0) { throw new Error(f('invalid %s size', this.branchName)); } this.size = schema.size | 0; this._branchConstructor = this._createBranchConstructor(); Object.free