Skip to content

Commit

Permalink
Loosen the requirement on the argument to the BaseStruct constructor (
Browse files Browse the repository at this point in the history
#434)

This PR changes `BaseStruct` to accept _any_ kind of object in its
constructor, instead of being persnickety and demanding only plain
objects.
  • Loading branch information
danfuzz authored Nov 15, 2024
2 parents 7bacdc5 + 68d1213 commit 928876b
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 11 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `-<num>` suffix after the date.
Expand Down
21 changes: 12 additions & 9 deletions src/structy/export/BaseStruct.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
52 changes: 50 additions & 2 deletions src/structy/tests/BaseStruct.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -184,6 +193,45 @@ 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(/Missing.*florp/);
});

test('accepts the required property via a plain object', () => {
const arg = { florp: 987 };
const got = new SomeStruct(arg);
expect(got.florp).toBe(987);
});

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('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; },
get fleep() { return 'eep'; }
};
expect(() => new SomeStruct(arg)).toThrow(/Extra.*fleep/);
});
});

describe('eval()', () => {
describe('with no `defaults`', () => {
test('given a direct instance of the class, returns it', () => {
Expand Down

0 comments on commit 928876b

Please sign in to comment.