Skip to content

Commit

Permalink
Union: support variants with no data beyond the discriminator
Browse files Browse the repository at this point in the history
Allow a union variant that provides no layout, i.e. all information
required is conveyed by the identity of the variant, which is exposed as
the union discriminator.

Closes #20.
  • Loading branch information
pabigot committed Mar 13, 2018
1 parent 2177856 commit 3d9105b
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 46 deletions.
13 changes: 10 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [Unreleased]

* **API** Allow the layout parameter of
a [VariantLayout][doc:VariantLayout] to be omitted in cases where no
data beyond the discriminator is required,
resolving [issue #20][issue#20].

## [1.1.0] - 2018-01-06

* **API** Add a third parameter to Structure specifying it should decode
Expand Down Expand Up @@ -157,14 +162,15 @@
[doc:NearInt64]: http://pabigot.github.io/buffer-layout/module-Layout-NearInt64.html
[doc:OffsetLayout]: http://pabigot.github.io/buffer-layout/module-Layout-OffsetLayout.html
[doc:patchIssue3992]: http://pabigot.github.io/buffer-layout/module-patchIssue3992.html
[doc:Union]: http://pabigot.github.io/buffer-layout/module-Layout-Union.html
[doc:Union.getSourceVariant]: http://pabigot.github.io/buffer-layout/module-Layout-Union.html#getSourceVariant
[doc:UnionDiscriminator]: http://pabigot.github.io/buffer-layout/module-Layout-UnionDiscriminator.html
[doc:Sequence]: http://pabigot.github.io/buffer-layout/module-Layout-Sequence.html
[doc:Sequence.count]: http://pabigot.github.io/buffer-layout/module-Layout-Sequence.html#count
[doc:Structure]: http://pabigot.github.io/buffer-layout/module-Layout-Structure.html
[doc:Structure.layoutFor]: http://pabigot.github.io/buffer-layout/module-Layout-Structure.html#layoutFor
[doc:Structure.offsetOf]: http://pabigot.github.io/buffer-layout/module-Layout-Structure.html#offsetOf
[doc:Union]: http://pabigot.github.io/buffer-layout/module-Layout-Union.html
[doc:Union.getSourceVariant]: http://pabigot.github.io/buffer-layout/module-Layout-Union.html#getSourceVariant
[doc:UnionDiscriminator]: http://pabigot.github.io/buffer-layout/module-Layout-UnionDiscriminator.html
[doc:VariantLayout]: http://pabigot.github.io/buffer-layout/module-Layout-VariantLayout.html
[issue#1]: https://github.com/pabigot/buffer-layout/issues/1
[issue#2]: https://github.com/pabigot/buffer-layout/issues/2
[issue#3]: https://github.com/pabigot/buffer-layout/issues/3
Expand All @@ -182,6 +188,7 @@
[issue#15]: https://github.com/pabigot/buffer-layout/issues/15
[issue#17]: https://github.com/pabigot/buffer-layout/issues/17
[issue#19]: https://github.com/pabigot/buffer-layout/issues/19
[issue#20]: https://github.com/pabigot/buffer-layout/issues/20
[ci:travis]: https://travis-ci.org/pabigot/buffer-layout
[ci:coveralls]: https://coveralls.io/github/pabigot/buffer-layout
[node:issue#3992]: https://github.com/nodejs/node/issues/3992
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,14 @@ The buffer-layout way:

const t = lo.u8('t');
const un = lo.union(t, lo.seq(lo.u8(), 4, 'u8'));
const nul = un.addVariant('n'.charCodeAt(0), 'nul');
const u32 = un.addVariant('w'.charCodeAt(0), lo.u32(), 'u32');
const s16 = un.addVariant('h'.charCodeAt(0), lo.seq(lo.s16(), 2), 's16');
const f32 = un.addVariant('f'.charCodeAt(0), lo.f32(), 'f32');
const b = Buffer.alloc(un.span);
assert.deepEqual(un.decode(b), {t: 0, u8: [0, 0, 0, 0]});
assert.deepEqual(un.decode(Buffer.from('6e01020304', 'hex')),
{t: nul.variant});
assert.deepEqual(un.decode(Buffer.from('7778563412', 'hex')),
{u32: 0x12345678});
assert.deepEqual(un.decode(Buffer.from('660000bd41', 'hex')),
Expand All @@ -156,6 +160,11 @@ representing the union and the variants:
lo.bindConstructorLayout(Union,
lo.union(lo.u8('t'), lo.seq(lo.u8(), 4, 'u8')));

function Vn() {}
util.inherits(Vn, Union);
lo.bindConstructorLayout(Vn,
Union.layout_.addVariant('n'.charCodeAt(0), 'nul'));

function Vu32(v) { this.u32 = v; }
util.inherits(Vu32, Union);
lo.bindConstructorLayout(Vu32,
Expand Down Expand Up @@ -186,6 +195,14 @@ representing the union and the variants:
v.encode(b);
assert.equal(Buffer.from('660000bd41', 'hex').compare(b), 0);

b.fill(0xFF);
v = new Vn();
v.encode(b);
assert.equal(Buffer.from('6effffffff', 'hex').compare(b), 0);

Note that one variant (`'n'`) carries no data, leaving the remainder of
the union unchanged.

See
[Layout.makeDestinationObject()](http://pabigot.github.io/buffer-layout/module-Layout-Layout.html#makeDestinationObject)
and
Expand Down
118 changes: 80 additions & 38 deletions lib/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -1628,26 +1628,49 @@ class Union extends Layout {
/**
* Method to infer a registered Union variant compatible with `src`.
*
* @param {Object} src - an object presumed to be compatible with the content of the Union.
* The first satisified rule in the following sequence defines the
* return value:
* * If `src` has properties matching the Union discriminator and
* the default layout, `undefined` is returned regardless of the
* value of the discriminator property (this ensures the default
* layout will be used);
* * If `src` has a property matching the Union discriminator, the
* value of the discriminator identifies a registered variant, and
* either (a) the variant has no layout, or (b) `src` has the
* variant's property, then the variant is returned (because the
* source satisfies the constraints of the variant it identifies);
* * If `src` does not have a property matching the Union
* discriminator, but does have a property matching a registered
* variant, then the variant is returned (because the source
* matches a variant without an explicit conflict);
* * An error is thrown (because we either can't identify a variant,
* or we were explicitly told the variant but can't satisfy it).
*
* @return {(undefined|VariantLayout)} - If `src` has properties
* matching the Union discriminator and default layout properties this
* returns `undefined`. Otherwise, if `src` has a property matching a
* {@link VariantLayout#property|variant property} that variant is
* returned. Otherwise an error is thrown.
* @param {Object} src - an object presumed to be compatible with
* the content of the Union.
*
* @return {(undefined|VariantLayout)} - as described above.
*
* @throws {Error} - if `src` cannot be associated with a default or
* registered variant.
*/
defaultGetSourceVariant(src) {
if (src.hasOwnProperty(this.discriminator.property)
&& src.hasOwnProperty(this.defaultLayout.property)) {
return undefined;
}
for (const k in this.registry) {
const lo = this.registry[k];
if (src.hasOwnProperty(lo.property)) {
return lo;
if (src.hasOwnProperty(this.discriminator.property)) {
if (this.defaultLayout
&& src.hasOwnProperty(this.defaultLayout.property)) {
return undefined;
}
const vlo = this.registry[src[this.discriminator.property]];
if ((!vlo.layout)
|| src.hasOwnProperty(vlo.property)) {
return vlo;
}
} else {
for (const tag in this.registry) {
const vlo = this.registry[tag];
if (src.hasOwnProperty(vlo.property)) {
return vlo;
}
}
}
throw new Error('unable to infer src variant');
Expand Down Expand Up @@ -1769,13 +1792,14 @@ class Union extends Layout {
* @param {Number} variant - initializer for {@link
* VariantLayout#variant|variant}.
*
* @param {Layout} layout - initializer for {@link
* VariantLayout#layout|layout}.
* @param {Layout} [layout] - initializer for {@link
* VariantLayout#layout|layout}. If absent the variant carries no
* data.
*
* @param {String} property - initializer for {@link
* @param {String} [property] - initializer for {@link
* Layout#property|property}. Unlike many other layouts, variant
* layouts must include a property so they can be identified within
* their containing @{link Union}.
* their containing @{link Union}, unless `layout` is unnecessary.
*
* @augments {Layout}
*/
Expand All @@ -1787,20 +1811,27 @@ class VariantLayout extends Layout {
if ((!Number.isInteger(variant)) || (0 > variant)) {
throw new TypeError('variant must be a (non-negative) integer');
}
if (!(layout instanceof Layout)) {
throw new TypeError('layout must be a Layout');
}
if ((null !== union.defaultLayout)
&& (0 <= layout.span)
&& (layout.span > union.defaultLayout.span)) {
throw new Error('variant span exceeds span of containing union');
if (('string' === typeof layout)
&& (undefined === property)) {
property = layout;
layout = null;
}
if ('string' !== typeof property) {
throw new TypeError('variant must have a String property');
if (layout) {
if (!(layout instanceof Layout)) {
throw new TypeError('layout must be a Layout');
}
if ((null !== union.defaultLayout)
&& (0 <= layout.span)
&& (layout.span > union.defaultLayout.span)) {
throw new Error('variant span exceeds span of containing union');
}
if ('string' !== typeof property) {
throw new TypeError('variant must have a String property');
}
}
let span = union.span;
if (0 > union.span) {
span = layout.span;
span = layout ? layout.span : 0;
if ((0 <= span) && union.usesPrefixDiscriminator) {
span += union.discriminator.layout.span;
}
Expand All @@ -1817,8 +1848,9 @@ class VariantLayout extends Layout {

/** The {@link Layout} to be used when reading/writing the
* non-discriminator part of the {@link
* VariantLayout#union|union}. */
this.layout = layout;
* VariantLayout#union|union}. If `null` the variant carries no
* data. */
this.layout = layout || null;
}

/** @override */
Expand Down Expand Up @@ -1852,7 +1884,11 @@ class VariantLayout extends Layout {
if (this.union.usesPrefixDiscriminator) {
contentOffset = this.union.discriminator.layout.span;
}
dest[this.property] = this.layout.decode(b, offset + contentOffset);
if (this.layout) {
dest[this.property] = this.layout.decode(b, offset + contentOffset);
} else if (this.union.usesPrefixDiscriminator) {
dest[this.union.discriminator.property] = this.variant;
}
return dest;
}

Expand All @@ -1865,23 +1901,29 @@ class VariantLayout extends Layout {
if (this.union.usesPrefixDiscriminator) {
contentOffset = this.union.discriminator.layout.span;
}
if (!src.hasOwnProperty(this.property)) {
if (this.layout
&& (!src.hasOwnProperty(this.property))) {
throw new TypeError('variant lacks property ' + this.property);
}
this.union.discriminator.encode(this.variant, b, offset);
this.layout.encode(src[this.property], b, offset + contentOffset);
const span = contentOffset + this.layout.getSpan(b, offset + contentOffset);
if ((0 <= this.union.span)
&& (span > this.union.span)) {
throw new Error('encoded variant overruns containing union');
let span = contentOffset;
if (this.layout) {
this.layout.encode(src[this.property], b, offset + contentOffset);
span += this.layout.getSpan(b, offset + contentOffset);
if ((0 <= this.union.span)
&& (span > this.union.span)) {
throw new Error('encoded variant overruns containing union');
}
}
return span;
}

/** Delegate {@link Layout#fromArray|fromArray} to {@link
* VariantLayout#layout|layout}. */
fromArray(values) {
return this.layout.fromArray(values);
if (this.layout) {
return this.layout.fromArray(values);
}
}
}

Expand Down

0 comments on commit 3d9105b

Please sign in to comment.