diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index f1e34c3f64..5163760f24 100644 --- a/packages/examples/packages/browserify-plugin/snap.manifest.json +++ b/packages/examples/packages/browserify-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "2WLcpc3RXg2Up0zeSMxWS/2rsVI1bC1iHCHAhzc3DOA=", + "shasum": "2XIZ9NDtR8XhO4hsAxS89GtjVkpwj/iTuHUv2P0TKYk=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify/snap.manifest.json b/packages/examples/packages/browserify/snap.manifest.json index 14eb4eb126..9460a92c3e 100644 --- a/packages/examples/packages/browserify/snap.manifest.json +++ b/packages/examples/packages/browserify/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "LQYiFCEu2fQXCuzfYHVpFfqffCbzzUjaqH7kPwgBfNc=", + "shasum": "4U6ZsKhhX3XQp20W7rLNWpaExEasUgcSU/+Q/UQQNns=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snaps-sdk/src/index.ts b/packages/snaps-sdk/src/index.ts index c90eb4c6fa..761189b66c 100644 --- a/packages/snaps-sdk/src/index.ts +++ b/packages/snaps-sdk/src/index.ts @@ -11,6 +11,7 @@ export { enumValue, typedUnion, selectiveUnion, + nonEmptyRecord, } from './internals'; // Re-exported from `@metamask/utils` for convenience. diff --git a/packages/snaps-sdk/src/internals/structs.test.ts b/packages/snaps-sdk/src/internals/structs.test.ts index 64e310cf6c..e5942b7744 100644 --- a/packages/snaps-sdk/src/internals/structs.test.ts +++ b/packages/snaps-sdk/src/internals/structs.test.ts @@ -5,9 +5,16 @@ import { object, string, validate, + any, } from '@metamask/superstruct'; -import { enumValue, literal, typedUnion, union } from './structs'; +import { + enumValue, + literal, + typedUnion, + union, + nonEmptyRecord, +} from './structs'; import type { BoxElement } from '../jsx'; import { Footer, Icon, Text, Button, Box } from '../jsx'; import { @@ -145,3 +152,27 @@ describe('typedUnion', () => { ]); }); }); + +describe('nonEmptyRecord', () => { + it.each([ + { foo: 'bar' }, + { + a: { + b: 'c', + }, + }, + ])('validates "%p"', (value) => { + const struct = nonEmptyRecord(string(), any()); + + expect(is(value, struct)).toBe(true); + }); + + it.each(['foo', 42, null, undefined, [], {}, [1, 2, 3]])( + 'does not validate "%p"', + (value) => { + const struct = nonEmptyRecord(string(), any()); + + expect(is(value, struct)).toBe(false); + }, + ); +}); diff --git a/packages/snaps-sdk/src/internals/structs.ts b/packages/snaps-sdk/src/internals/structs.ts index f635a8422f..288e4cb6c4 100644 --- a/packages/snaps-sdk/src/internals/structs.ts +++ b/packages/snaps-sdk/src/internals/structs.ts @@ -2,6 +2,8 @@ import type { AnyStruct, Infer, InferStructTuple } from '@metamask/superstruct'; import { Struct, define, + record, + refine, literal as superstructLiteral, union as superstructUnion, } from '@metamask/superstruct'; @@ -217,3 +219,19 @@ export function selectiveUnion AnyStruct>( }, }); } + +/** + * Refine a struct to be a non-empty record and disallows usage of arrays. + * + * @param Key - The struct for the record key. + * @param Value - The struct for the record value. + * @returns The refined struct. + */ +export function nonEmptyRecord( + Key: Struct, + Value: Struct, +) { + return refine(record(Key, Value), 'Non-empty record', (value) => { + return !Array.isArray(value) && Object.keys(value).length > 0; + }); +}