From 2297ebe76b9ec8c048886c0099fbc71687a8f02c Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 28 Jan 2025 17:07:32 +0100 Subject: [PATCH 1/9] feat: add definePattern (typed superstruct.pattern) --- src/superstruct.test.ts | 17 +++++++++++++++++ src/superstruct.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/superstruct.test.ts create mode 100644 src/superstruct.ts diff --git a/src/superstruct.test.ts b/src/superstruct.test.ts new file mode 100644 index 000000000..c9e8c7c9b --- /dev/null +++ b/src/superstruct.test.ts @@ -0,0 +1,17 @@ +import { is, pattern, string } from '@metamask/superstruct'; + +import { definePattern } from './superstruct'; + +describe('definePattern', () => { + const hexPattern = /^0x[0-9a-f]+$/u; + + it('is similar to superstruct.pattern', () => { + const HexStringPattern = pattern(string(), hexPattern); + const HexString = definePattern('HexString', hexPattern); + + expect(is('0xdeadbeef', HexStringPattern)).toBe(true); + expect(is('0xdeadbeef', HexString)).toBe(true); + expect(is('foobar', HexStringPattern)).toBe(false); + expect(is('foobar', HexString)).toBe(false); + }); +}); diff --git a/src/superstruct.ts b/src/superstruct.ts new file mode 100644 index 000000000..5e97f8419 --- /dev/null +++ b/src/superstruct.ts @@ -0,0 +1,35 @@ +import type { Struct } from '@metamask/superstruct'; +import { define } from '@metamask/superstruct'; + +/** + * Defines a new string-struct matching a regular expression. + * + * Example: + * + * ```ts + * const EthAddressStruct = definePattern('EthAddress', /^0x[0-9a-f]{40}$/iu); + * type EthAddress = Infer; // string + * + * const CaipChainIdStruct = defineTypedPattern<`${string}:${string}`>( + * 'CaipChainId', + * /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u; + * ); + * type CaipChainId = Infer; // `${string}:${string}` + * + * ``` + * + * @param name - Type name. + * @param pattern - Regular expression to match. + * @template Pattern - The pattern type, defaults to `string`. + * @returns A new string-struct that matches the given pattern. + */ +export function definePattern( + name: string, + pattern: RegExp, +): Struct { + return define( + name, + (value: unknown): boolean => + typeof value === 'string' && pattern.test(value), + ); +} From 3518871dc71e46b83bbfa20e3ebbd910d031b329 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 28 Jan 2025 17:08:16 +0100 Subject: [PATCH 2/9] refactor!: use definePattern for all CAIP structs --- src/caip-types.ts | 70 ++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/src/caip-types.ts b/src/caip-types.ts index 5ce5742f0..74baf4a98 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -1,6 +1,8 @@ import type { Infer, Struct } from '@metamask/superstruct'; import { is, pattern, string } from '@metamask/superstruct'; +import { definePattern } from './superstruct'; + export const CAIP_CHAIN_ID_REGEX = /^(?[-a-z0-9]{3,8}):(?[-_a-zA-Z0-9]{1,32})$/u; @@ -28,38 +30,45 @@ export const CAIP_ASSET_ID_REGEX = /** * A CAIP-2 chain ID, i.e., a human-readable namespace and reference. */ -export const CaipChainIdStruct = pattern( - string(), +export const CaipChainIdStruct = definePattern<`${string}:${string}`>( + 'CaipChainId', CAIP_CHAIN_ID_REGEX, -) as Struct; -export type CaipChainId = `${string}:${string}`; +); +export type CaipChainId = Infer; /** * A CAIP-2 namespace, i.e., the first part of a CAIP chain ID. */ -export const CaipNamespaceStruct = pattern(string(), CAIP_NAMESPACE_REGEX); +export const CaipNamespaceStruct = definePattern( + 'CaipNamespace', + CAIP_NAMESPACE_REGEX, +); export type CaipNamespace = Infer; /** * A CAIP-2 reference, i.e., the second part of a CAIP chain ID. */ -export const CaipReferenceStruct = pattern(string(), CAIP_REFERENCE_REGEX); +export const CaipReferenceStruct = definePattern( + 'CaipReference', + CAIP_REFERENCE_REGEX, +); export type CaipReference = Infer; /** * A CAIP-10 account ID, i.e., a human-readable namespace, reference, and account address. */ -export const CaipAccountIdStruct = pattern( - string(), - CAIP_ACCOUNT_ID_REGEX, -) as Struct; -export type CaipAccountId = `${string}:${string}:${string}`; +export const CaipAccountIdStruct = + definePattern<`${string}:${string}:${string}`>( + 'CaipAccountId', + CAIP_ACCOUNT_ID_REGEX, + ); +export type CaipAccountId = Infer; /** * A CAIP-10 account address, i.e., the third part of the CAIP account ID. */ -export const CaipAccountAddressStruct = pattern( - string(), +export const CaipAccountAddressStruct = definePattern( + 'CaipAccountAddress', CAIP_ACCOUNT_ADDRESS_REGEX, ); export type CaipAccountAddress = Infer; @@ -67,8 +76,8 @@ export type CaipAccountAddress = Infer; /** * A CAIP-19 asset namespace, i.e., a namespace domain of an asset. */ -export const CaipAssetNamespaceStruct = pattern( - string(), +export const CaipAssetNamespaceStruct = definePattern( + 'CaipAssetNamespace', CAIP_ASSET_NAMESPACE_REGEX, ); export type CaipAssetNamespace = Infer; @@ -76,8 +85,8 @@ export type CaipAssetNamespace = Infer; /** * A CAIP-19 asset reference, i.e., an identifier for an asset within a given namespace. */ -export const CaipAssetReferenceStruct = pattern( - string(), +export const CaipAssetReferenceStruct = definePattern( + 'CaipAssetReference', CAIP_ASSET_REFERENCE_REGEX, ); export type CaipAssetReference = Infer; @@ -85,26 +94,31 @@ export type CaipAssetReference = Infer; /** * A CAIP-19 asset token ID, i.e., a unique identifier for an addressable asset of a given type */ -export const CaipTokenIdStruct = pattern(string(), CAIP_TOKEN_ID_REGEX); +export const CaipTokenIdStruct = definePattern( + 'CaipTokenId', + CAIP_TOKEN_ID_REGEX, +); export type CaipTokenId = Infer; /** * A CAIP-19 asset type identifier, i.e., a human-readable type of asset identifier. */ -export const CaipAssetTypeStruct = pattern( - string(), - CAIP_ASSET_TYPE_REGEX, -) as Struct; -export type CaipAssetType = `${string}:${string}/${string}:${string}`; +export const CaipAssetTypeStruct = + definePattern<`${string}:${string}/${string}:${string}`>( + 'CaipAssetType', + CAIP_ASSET_TYPE_REGEX, + ); +export type CaipAssetType = Infer; /** * A CAIP-19 asset ID identifier, i.e., a human-readable type of asset ID. */ -export const CaipAssetIdStruct = pattern( - string(), - CAIP_ASSET_ID_REGEX, -) as Struct; -export type CaipAssetId = `${string}:${string}/${string}:${string}/${string}`; +export const CaipAssetIdStruct = + definePattern<`${string}:${string}/${string}:${string}/${string}`>( + 'CaipAssetId', + CAIP_ASSET_ID_REGEX, + ); +export type CaipAssetId = Infer; /** Known CAIP namespaces. */ export enum KnownCaipNamespace { From 8969892f20958089ddec21c04c7d76934b49e949 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 28 Jan 2025 18:04:48 +0100 Subject: [PATCH 3/9] chore: lint --- src/caip-types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/caip-types.ts b/src/caip-types.ts index 74baf4a98..13a0d9fa8 100644 --- a/src/caip-types.ts +++ b/src/caip-types.ts @@ -1,5 +1,5 @@ -import type { Infer, Struct } from '@metamask/superstruct'; -import { is, pattern, string } from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; +import { is } from '@metamask/superstruct'; import { definePattern } from './superstruct'; From a9207048025c997cdbc3a705113def7fc0b327d5 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 28 Jan 2025 18:42:20 +0100 Subject: [PATCH 4/9] test: add failing test for definePattern --- src/superstruct.test.ts | 14 ++++++++++---- src/superstruct.ts | 8 +++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/superstruct.test.ts b/src/superstruct.test.ts index c9e8c7c9b..79fe5020d 100644 --- a/src/superstruct.test.ts +++ b/src/superstruct.test.ts @@ -1,17 +1,23 @@ -import { is, pattern, string } from '@metamask/superstruct'; +import { assert, is, pattern, string } from '@metamask/superstruct'; import { definePattern } from './superstruct'; describe('definePattern', () => { const hexPattern = /^0x[0-9a-f]+$/u; + const HexStringPattern = pattern(string(), hexPattern); + const HexString = definePattern('HexString', hexPattern); it('is similar to superstruct.pattern', () => { - const HexStringPattern = pattern(string(), hexPattern); - const HexString = definePattern('HexString', hexPattern); - expect(is('0xdeadbeef', HexStringPattern)).toBe(true); expect(is('0xdeadbeef', HexString)).toBe(true); expect(is('foobar', HexStringPattern)).toBe(false); expect(is('foobar', HexString)).toBe(false); }); + + it('throws and error if assert fails', () => { + const value = 'foobar'; + expect(() => assert(value, HexString)).toThrow( + `Expected a value of type \`HexString\`, but received: \`"foobar"\``, + ); + }); }); diff --git a/src/superstruct.ts b/src/superstruct.ts index 5e97f8419..ab7494849 100644 --- a/src/superstruct.ts +++ b/src/superstruct.ts @@ -27,9 +27,7 @@ export function definePattern( name: string, pattern: RegExp, ): Struct { - return define( - name, - (value: unknown): boolean => - typeof value === 'string' && pattern.test(value), - ); + return define(name, (value: unknown): boolean | string => { + return typeof value === 'string' && pattern.test(value); + }); } From c88c8febfe88c80b559ce40ef10a8970255e3dfc Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 29 Jan 2025 10:29:41 +0100 Subject: [PATCH 5/9] chore: fix jsdocs Co-authored-by: Daniel Rocha <68558152+danroc@users.noreply.github.com> --- src/superstruct.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/superstruct.ts b/src/superstruct.ts index ab7494849..5970ed33e 100644 --- a/src/superstruct.ts +++ b/src/superstruct.ts @@ -15,7 +15,6 @@ import { define } from '@metamask/superstruct'; * /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u; * ); * type CaipChainId = Infer; // `${string}:${string}` - * * ``` * * @param name - Type name. From 9eac4206a8d2b1b2323b5824f9dd388ecd749bf9 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 29 Jan 2025 10:58:18 +0100 Subject: [PATCH 6/9] chore: better jsdoc (@example) Co-authored-by: Maarten Zuidhoorn --- src/superstruct.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/superstruct.ts b/src/superstruct.ts index 5970ed33e..3107caaf9 100644 --- a/src/superstruct.ts +++ b/src/superstruct.ts @@ -4,9 +4,7 @@ import { define } from '@metamask/superstruct'; /** * Defines a new string-struct matching a regular expression. * - * Example: - * - * ```ts + * @example * const EthAddressStruct = definePattern('EthAddress', /^0x[0-9a-f]{40}$/iu); * type EthAddress = Infer; // string * @@ -15,7 +13,6 @@ import { define } from '@metamask/superstruct'; * /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u; * ); * type CaipChainId = Infer; // `${string}:${string}` - * ``` * * @param name - Type name. * @param pattern - Regular expression to match. From fd3102e1c312842ed6bd034ac4479cc82639251a Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 29 Jan 2025 11:00:55 +0100 Subject: [PATCH 7/9] chore: lint --- src/superstruct.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/superstruct.ts b/src/superstruct.ts index 3107caaf9..bc4e4aa4e 100644 --- a/src/superstruct.ts +++ b/src/superstruct.ts @@ -13,7 +13,6 @@ import { define } from '@metamask/superstruct'; * /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u; * ); * type CaipChainId = Infer; // `${string}:${string}` - * * @param name - Type name. * @param pattern - Regular expression to match. * @template Pattern - The pattern type, defaults to `string`. From 7f8e76409805de90500a6687203cc0215b0fab35 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 29 Jan 2025 11:02:17 +0100 Subject: [PATCH 8/9] chore: update index.ts --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index d3f0813ce..81c653a0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export * from './misc'; export * from './number'; export * from './opaque'; export * from './promise'; +export * from './superstruct'; export * from './time'; export * from './transaction-types'; export * from './versions'; From 044d318c76c50909cd618f40678d3a2aaf4d6569 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 29 Jan 2025 11:08:19 +0100 Subject: [PATCH 9/9] test: fix snapshots --- src/index.test.ts | 1 + src/node.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 3ccf23082..d5cff297e 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -87,6 +87,7 @@ describe('index', () => { "createModuleLogger", "createNumber", "createProjectLogger", + "definePattern", "exactOptional", "getChecksumAddress", "getErrorMessage", diff --git a/src/node.test.ts b/src/node.test.ts index d28d2f3bd..30383f464 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -88,6 +88,7 @@ describe('node', () => { "createNumber", "createProjectLogger", "createSandbox", + "definePattern", "directoryExists", "ensureDirectoryStructureExists", "exactOptional",