From df782176b91ff4c3aaa3637f158865ac81ebf89d Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 14 Nov 2024 18:25:47 -0800 Subject: [PATCH 1/5] Loosen up the contract on the constructor of `BaseStruct`. --- src/structy/export/BaseStruct.js | 21 +++++++++------- src/structy/tests/BaseStruct.test.js | 37 ++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/structy/export/BaseStruct.js b/src/structy/export/BaseStruct.js index 0eeb18eb0..88f2c76bf 100644 --- a/src/structy/export/BaseStruct.js +++ b/src/structy/export/BaseStruct.js @@ -6,24 +6,27 @@ import { AskIf, MustBe } from '@this/typey'; /** * Base class for type-checked "structures." Each concrete subclass is expected - * to pass a plain object in its `super()` constructor call (or pass nothing or + * to pass an object in its `super()` constructor call (or pass nothing or * `null` for an all-default construction) which is suitable for parsing by this - * (base) class. This class defines the mechanism by which a plain object gets - * mapped into properties on the constructed instance, including running - * validation on each property and a final overall validation. + * (base) class. This class defines the mechanism by which an object gets mapped + * into properties on the constructed instance, including running validation on + * each property and a final overall validation. * * Instances of this class are always frozen. */ export class BaseStruct { /** - * Constructs an instance. + * Constructs an instance. The argument, if non-`null`, is taken to be a + * "plain-like" object, in that its own enumerable string-keyed properties are + * what matter. Instances of (concrete subclasses of) this class can be used + * as arguments. * - * @param {?object} [rawObject] Raw object to parse. This is expected to be - * either a plain object, or `null` to have all default values. The latter - * is equivalent to passing `{}` (an empty object). + * @param {?object} [rawObject] Raw object to parse, or `null` to have all + * default values. Passing `null` is equivalent to passing `{}` (an empty + * plain object). */ constructor(rawObject = null) { - rawObject = (rawObject === null) ? {} : MustBe.plainObject(rawObject); + rawObject = (rawObject === null) ? {} : MustBe.object(rawObject); this.#fillInObject(rawObject); Object.freeze(this); diff --git a/src/structy/tests/BaseStruct.test.js b/src/structy/tests/BaseStruct.test.js index b338af576..0bfcd72ed 100644 --- a/src/structy/tests/BaseStruct.test.js +++ b/src/structy/tests/BaseStruct.test.js @@ -61,7 +61,6 @@ describe('using the (base) class directly', () => { ${123} ${'abc'} ${[1]} - ${new Map()} `('throws given invalid argument $arg', ({ arg }) => { expect(() => new BaseStruct(arg)).toThrow(); }); @@ -70,6 +69,16 @@ describe('using the (base) class directly', () => { // ...because the base class doesn't define any properties. expect(() => new BaseStruct({ what: 'nope' })).toThrow(/Extra property:/); }); + + test('throws given a non-empty (non-plain) object', () => { + // ...because the base class doesn't define any properties. + + const obj = { + get florp() { return 'like'; } + }; + + expect(() => new BaseStruct(obj)).toThrow(/Extra property:/); + }) }); describe('_impl_propertyPrefix', () => { @@ -156,7 +165,7 @@ describe('using the (base) class directly', () => { }); }); -describe('using a subclass', () => { +describe('using a subclass with one defaultable property and one required property', () => { class SomeStruct extends BaseStruct { // @defaultConstructor @@ -184,6 +193,30 @@ describe('using a subclass', () => { } } + describe('constructor()', () => { + test.each` + args + ${[]} + ${[null]} + `('throws given `$args` (because there is a required property)', ({ args }) => { + expect(() => new SomeStruct(...args)).toThrow(/florp/); + }); + + test.each('accepts the required property via a plain object', () => { + const arg = { florp: 987 }; + const got = new SomeStruct(arg); + expect(got.florp).toBe(987); + }); + + test.each('accepts the required property via a non-plain object', () => { + const arg = { + get florp() { return 789; } + }; + const got = new SomeStruct(arg); + expect(got.florp).toBe(789); + }); + }); + describe('eval()', () => { describe('with no `defaults`', () => { test('given a direct instance of the class, returns it', () => { From d2a06841bb52fb566b53eec2a8cf47070df03a9b Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 14 Nov 2024 18:26:53 -0800 Subject: [PATCH 2/5] Changelog. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d9268ee0..765eeddef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ Breaking changes: Other notable changes: * `loggy-intf` / `loggy`: * Minor tweaks to "human" (non-JSON) log rendering. +* `structy`: + * Started allowing any object (plain or not) to be used as the argument to the + `BaseStruct` constructor. * `webapp-builtins`: * Simplified naming scheme for preserved log files: Names now always include a `-` suffix after the date. From ce4b702dd2d290a1b2570a3733a8040a08906594 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 14 Nov 2024 18:33:22 -0800 Subject: [PATCH 3/5] Add tests. --- src/structy/tests/BaseStruct.test.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/structy/tests/BaseStruct.test.js b/src/structy/tests/BaseStruct.test.js index 0bfcd72ed..bae6764a7 100644 --- a/src/structy/tests/BaseStruct.test.js +++ b/src/structy/tests/BaseStruct.test.js @@ -78,7 +78,7 @@ describe('using the (base) class directly', () => { }; expect(() => new BaseStruct(obj)).toThrow(/Extra property:/); - }) + }); }); describe('_impl_propertyPrefix', () => { @@ -199,22 +199,30 @@ describe('using a subclass with one defaultable property and one required proper ${[]} ${[null]} `('throws given `$args` (because there is a required property)', ({ args }) => { - expect(() => new SomeStruct(...args)).toThrow(/florp/); + expect(() => new SomeStruct(...args)).toThrow(/Missing.*florp/); }); - test.each('accepts the required property via a plain object', () => { + test('accepts the required property via a plain object', () => { const arg = { florp: 987 }; const got = new SomeStruct(arg); expect(got.florp).toBe(987); }); - test.each('accepts the required property via a non-plain object', () => { + test('accepts the required property via a non-plain object', () => { const arg = { get florp() { return 789; } }; const got = new SomeStruct(arg); expect(got.florp).toBe(789); }); + + test('throws given an extra property in a non-plain object', () => { + const arg = { + get florp() { return 789; }, + get fleep() { return 'eep'; } + }; + expect(() => new SomeStruct(arg)).toThrow(/Extra.*fleep/); + }); }); describe('eval()', () => { From bbb879e4157311e9063fc79d7c47a2e54012b334 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 14 Nov 2024 18:35:57 -0800 Subject: [PATCH 4/5] Add a test. --- src/structy/tests/BaseStruct.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/structy/tests/BaseStruct.test.js b/src/structy/tests/BaseStruct.test.js index bae6764a7..8c34375b0 100644 --- a/src/structy/tests/BaseStruct.test.js +++ b/src/structy/tests/BaseStruct.test.js @@ -216,6 +216,13 @@ describe('using a subclass with one defaultable property and one required proper expect(got.florp).toBe(789); }); + test('accepts an instance of itself', () => { + const arg = new SomeStruct({ abc: 'yes', florp: 999 }); + const got = new SomeStruct(arg); + expect(got.abc).toBe(arg.abc); + expect(got.florp).toBe(arg.florp); + }) + test('throws given an extra property in a non-plain object', () => { const arg = { get florp() { return 789; }, From 68d1213fb64b52673d122faf35cc5cc363bab1b2 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 14 Nov 2024 18:37:27 -0800 Subject: [PATCH 5/5] Appease the linter. --- src/structy/tests/BaseStruct.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structy/tests/BaseStruct.test.js b/src/structy/tests/BaseStruct.test.js index 8c34375b0..f13dfc871 100644 --- a/src/structy/tests/BaseStruct.test.js +++ b/src/structy/tests/BaseStruct.test.js @@ -221,7 +221,7 @@ describe('using a subclass with one defaultable property and one required proper const got = new SomeStruct(arg); expect(got.abc).toBe(arg.abc); expect(got.florp).toBe(arg.florp); - }) + }); test('throws given an extra property in a non-plain object', () => { const arg = {