avsc
Version:
Avro for JavaScript
1,731 lines (1,536 loc) • 94.8 kB
JavaScript
/* 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