diff --git a/lib/Struct.js b/lib/Struct.js index 88936c3..18c8f01 100644 --- a/lib/Struct.js +++ b/lib/Struct.js @@ -1,14 +1,34 @@ 'use strict'; -function Struct(name, defs) { +/** + * Creates a named Struct class with typed fields, supporting fixed and variable-length binary + * encoding. + * + * @param {string} name - The name of the generated class. + * @param {Object.} defs - An object mapping field names to their data type + * definitions. Each data type must expose `length`, `defaultValue`, `fromBuffer`, and `toBuffer`. + * @param {Object} [opts] - Optional configuration. + * @param {'default'|'skip'} [opts.encodeMissingFieldsBehavior='default'] - Controls how fields + * with `undefined` values are handled during encoding. `'default'` fills them with the type's + * default value; `'skip'` omits them entirely from the encoded output. + * @returns {typeof Struct} The generated Struct class. + */ +function Struct(name, defs, opts) { Object.seal(defs); + const encodeMissingFieldsBehavior = (opts && opts.encodeMissingFieldsBehavior) || 'default'; let size = 0; let varsize = false; + for (const dt of Object.values(defs)) { if (typeof dt.length === 'number' && dt.length > 0) { size += dt.length; } else varsize = true; } + + if (encodeMissingFieldsBehavior === 'skip') { + varsize = true; + } + const r = { [name]: class { @@ -20,6 +40,10 @@ function Struct(name, defs) { } // eslint-disable-next-line no-restricted-syntax for (const key in defs) { + if (encodeMissingFieldsBehavior === 'skip' && typeof this[key] === 'undefined') { + continue; + } + if (typeof props[key] === 'undefined') { this[key] = defs[key].defaultValue; } @@ -102,6 +126,7 @@ function Struct(name, defs) { // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const p in defs) { + if (encodeMissingFieldsBehavior === 'skip' && typeof this[p] === 'undefined') continue; // eslint-disable-next-line no-shadow let varsize = defs[p].length; if (defs[p].length <= 0) { diff --git a/package-lock.json b/package-lock.json index a28062f..0b9e90c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@athombv/data-types", - "version": "1.1.4", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4ae67fe..20f57a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athombv/data-types", - "version": "1.1.4", + "version": "1.2.0", "description": "Binary Data Parsers", "main": "index.js", "types": "index.d.ts", diff --git a/test/Struct.test.js b/test/Struct.test.js index e751e12..afdf412 100644 --- a/test/Struct.test.js +++ b/test/Struct.test.js @@ -32,6 +32,113 @@ describe('Struct', function() { it('should parse test data to buffer', function() { assert(dataBuf.equals(Buffer.from('0474657374f40103040100020003000400020201', 'hex'))); }); + it('should encode missing fields with default values by default', function() { + const S = Struct('DefaultMissing', { + a: DataTypes.uint8, + b: DataTypes.uint8, + }); + const instance = new S({ a: 42 }); + const buf = instance.toBuffer(); + + // Both fields encoded: a=42, b=0 (default) + assert.equal(buf.length, 2); + assert.equal(buf[0], 42); + assert.equal(buf[1], 0); + }); + it('should skip missing fields in nested struct without padding', function() { + const Inner = Struct('InnerSkip', { + x: DataTypes.uint8, + y: DataTypes.uint8, + }, { encodeMissingFieldsBehavior: 'skip' }); + + const Outer = Struct('OuterWithInner', { + id: DataTypes.uint8, + inner: Inner, + }); + + const instance = new Outer({ + id: 5, + inner: { x: 10 }, + }); + + const buf = instance.toBuffer(); + // id (1 byte) + inner.x (1 byte), no padding for missing inner.y + assert.equal(buf.length, 2); + assert.equal(buf[0], 5); + assert.equal(buf[1], 10); + }); + + it('should skip missing fields in array of structs without padding', function() { + const Item = Struct('ItemSkip', { + a: DataTypes.uint8, + b: DataTypes.uint8, + }, { encodeMissingFieldsBehavior: 'skip' }); + + const S = Struct('ArrayContainer', { + items: DataTypes.Array8(Item), + }); + + const instance = new S({ + items: [ + { a: 1 }, + { a: 2, b: 20 }, + { b: 30 }, + ], + }); + + const buf = instance.toBuffer(); + // Array8: 1 byte length + items + // item1: 1 byte (a=1) + // item2: 2 bytes (a=2, b=20) + // item3: 1 byte (b=30) + // Total: 1 + 1 + 2 + 1 = 5 + assert.equal(buf.length, 5); + assert.equal(buf[0], 3); // array length + assert.equal(buf[1], 1); // item1.a + assert.equal(buf[2], 2); // item2.a + assert.equal(buf[3], 20); // item2.b + assert.equal(buf[4], 30); // item3.b + }); + + it('should handle mixed skip and default behavior in nested structures', function() { + const SkipInner = Struct('SkipInner', { + x: DataTypes.uint8, + y: DataTypes.uint8, + }, { encodeMissingFieldsBehavior: 'skip' }); + + const DefaultInner = Struct('DefaultInner', { + p: DataTypes.uint8, + q: DataTypes.uint8, + }); + + const Outer = Struct('MixedOuter', { + skip: SkipInner, + default: DefaultInner, + }); + + const instance = new Outer({ + skip: { x: 5 }, + default: { p: 10 }, + }); + + const buf = instance.toBuffer(); + // skip: 1 byte (x=5) + // default: 2 bytes (p=10, q=0) + // Total: 3 + assert.equal(buf.length, 3); + assert.equal(buf[0], 5); + assert.equal(buf[1], 10); + assert.equal(buf[2], 0); + }); + it('should produce identical output with skip option when all fields provided', function() { + const defsA = { a: DataTypes.uint8, b: DataTypes.uint8 }; + const defsB = { a: DataTypes.uint8, b: DataTypes.uint8 }; + const Default = Struct('AllFieldsDefault', defsA); + const Skip = Struct('AllFieldsSkip', defsB, { encodeMissingFieldsBehavior: 'skip' }); + const d = new Default({ a: 1, b: 2 }); + const s = new Skip({ a: 1, b: 2 }); + assert(d.toBuffer().equals(s.toBuffer())); + }); it('should parse test data from buffer', function() { const refData = TestStruct.fromBuffer(dataBuf);