From daec8b92be62766ec9399df844213d173771185e Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Mon, 2 Mar 2026 09:49:45 +0100 Subject: [PATCH 1/7] feat: add behavior for encoding missing fields in Struct --- lib/Struct.js | 4 +++- test/Struct.test.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/Struct.js b/lib/Struct.js index 88936c3..25d0918 100644 --- a/lib/Struct.js +++ b/lib/Struct.js @@ -1,7 +1,8 @@ 'use strict'; -function Struct(name, defs) { +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)) { @@ -102,6 +103,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/test/Struct.test.js b/test/Struct.test.js index e751e12..010ef6f 100644 --- a/test/Struct.test.js +++ b/test/Struct.test.js @@ -32,6 +32,42 @@ 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 when encodeMissingFieldsBehavior is skip', function() { + const S = Struct('SkipMissing', { + a: DataTypes.uint8, + b: DataTypes.uint8, + c: DataTypes.uint8, + }, { encodeMissingFieldsBehavior: 'skip' }); + const instance = new S({ a: 10, c: 30 }); + // b is set to defaultValue (0) by constructor, so it won't be skipped + // To truly skip, the field must be undefined + delete instance.b; + const buf = instance.toBuffer(); + assert.equal(buf.length, 2); + assert.equal(buf[0], 10); + assert.equal(buf[1], 30); + }); + 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); From 1e7bd11d36de49379192ff15b29bcba0340fe7f9 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Mon, 2 Mar 2026 10:04:44 +0100 Subject: [PATCH 2/7] chore: don't set default value if `encodeMissingFieldsBehavor` is `skip` --- lib/Struct.js | 4 ++++ test/Struct.test.js | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Struct.js b/lib/Struct.js index 25d0918..5c9662f 100644 --- a/lib/Struct.js +++ b/lib/Struct.js @@ -21,6 +21,10 @@ function Struct(name, defs, opts) { } // 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; } diff --git a/test/Struct.test.js b/test/Struct.test.js index 010ef6f..5c9a675 100644 --- a/test/Struct.test.js +++ b/test/Struct.test.js @@ -51,9 +51,6 @@ describe('Struct', function() { c: DataTypes.uint8, }, { encodeMissingFieldsBehavior: 'skip' }); const instance = new S({ a: 10, c: 30 }); - // b is set to defaultValue (0) by constructor, so it won't be skipped - // To truly skip, the field must be undefined - delete instance.b; const buf = instance.toBuffer(); assert.equal(buf.length, 2); assert.equal(buf[0], 10); From a20cb37798fad6b580c341126da8f60333b18d6b Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Mon, 2 Mar 2026 10:05:44 +0100 Subject: [PATCH 3/7] test: add assertions for unset fields in 'skip' mode --- test/Struct.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/Struct.test.js b/test/Struct.test.js index 5c9a675..f51f17d 100644 --- a/test/Struct.test.js +++ b/test/Struct.test.js @@ -39,6 +39,7 @@ describe('Struct', function() { }); 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); @@ -51,6 +52,10 @@ describe('Struct', function() { c: DataTypes.uint8, }, { encodeMissingFieldsBehavior: 'skip' }); const instance = new S({ a: 10, c: 30 }); + + // In 'skip' mode, missing fields should remain unset + assert.strictEqual(instance.b, undefined); + const buf = instance.toBuffer(); assert.equal(buf.length, 2); assert.equal(buf[0], 10); From 456bb9b76e3dfffac2c98d283d25193c120a6c35 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Mon, 2 Mar 2026 10:16:55 +0100 Subject: [PATCH 4/7] feat: enhance behavior for skipping missing fields in nested structs --- lib/Struct.js | 6 ++++ test/Struct.test.js | 87 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/lib/Struct.js b/lib/Struct.js index 5c9662f..742945b 100644 --- a/lib/Struct.js +++ b/lib/Struct.js @@ -5,11 +5,17 @@ function Struct(name, defs, opts) { 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 { diff --git a/test/Struct.test.js b/test/Struct.test.js index f51f17d..afdf412 100644 --- a/test/Struct.test.js +++ b/test/Struct.test.js @@ -45,21 +45,90 @@ describe('Struct', function() { assert.equal(buf[0], 42); assert.equal(buf[1], 0); }); - it('should skip missing fields when encodeMissingFieldsBehavior is skip', function() { - const S = Struct('SkipMissing', { + 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, - c: DataTypes.uint8, }, { encodeMissingFieldsBehavior: 'skip' }); - const instance = new S({ a: 10, c: 30 }); - // In 'skip' mode, missing fields should remain unset - assert.strictEqual(instance.b, undefined); + 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(); - assert.equal(buf.length, 2); - assert.equal(buf[0], 10); - assert.equal(buf[1], 30); + // 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 }; From 09121c5a3c4e5e14ab29efc920f4f70d01c41e25 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Mon, 2 Mar 2026 11:49:38 +0100 Subject: [PATCH 5/7] chore: add JSDoc to struct function --- lib/Struct.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/Struct.js b/lib/Struct.js index 742945b..45452f3 100644 --- a/lib/Struct.js +++ b/lib/Struct.js @@ -1,5 +1,17 @@ 'use strict'; +/** + * 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'; From f85514f59ff4a371d81de8fd4f25de3b8f46bcf5 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Mon, 2 Mar 2026 13:11:22 +0100 Subject: [PATCH 6/7] chore: fix linting --- lib/Struct.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/Struct.js b/lib/Struct.js index 45452f3..18c8f01 100644 --- a/lib/Struct.js +++ b/lib/Struct.js @@ -1,15 +1,16 @@ 'use strict'; /** - * Creates a named Struct class with typed fields, supporting fixed and variable-length binary encoding. + * 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.} 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. + * @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) { From 7ae232082ac1e5b0216afaaf0fa99fd2974c5861 Mon Sep 17 00:00:00 2001 From: Homey Github Actions Bot Date: Fri, 6 Mar 2026 08:48:08 +0000 Subject: [PATCH 7/7] 1.2.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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",