Skip to content

Commit 9ac7ec2

Browse files
ccharlydanrocMrtenz
authored
feat: named CAIP structs (#228)
Currently, all our CAIP structs are being inferred as `string`. This PR adds a new `definePattern` which allows to using a template literal string instead. Also, this new `definePattern` allows to "name" the `pattern`. Here's an example: ```ts const HexStringPattern = pattern(string(), hexPattern); const HexString = definePattern('HexString', hexPattern); assert('foobar', HexStringPattern); // StructError: Expected a string matching `/^0x[0-9a-f]+$/` but received "foobar" assert('foobar', HexString); // StructError: Expected a value of type `HexString`, but received: `"foobar"` ``` If you think the new `definePattern` should go in a separate PR, I can split that up. I do believe this is **BREAKING CHANGE** since the error messages will be different, so we can expect the consumers of this packages to update some of there tests (but I don't really know if we consider this a breaking change in other packages?) --------- Co-authored-by: Daniel Rocha <68558152+danroc@users.noreply.github.com> Co-authored-by: Maarten Zuidhoorn <maarten@zuidhoorn.com>
1 parent 48267f8 commit 9ac7ec2

6 files changed

+98
-30
lines changed

src/caip-types.ts

+44-30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { Infer, Struct } from '@metamask/superstruct';
2-
import { is, pattern, string } from '@metamask/superstruct';
1+
import type { Infer } from '@metamask/superstruct';
2+
import { is } from '@metamask/superstruct';
3+
4+
import { definePattern } from './superstruct';
35

46
export const CAIP_CHAIN_ID_REGEX =
57
/^(?<namespace>[-a-z0-9]{3,8}):(?<reference>[-_a-zA-Z0-9]{1,32})$/u;
@@ -28,83 +30,95 @@ export const CAIP_ASSET_ID_REGEX =
2830
/**
2931
* A CAIP-2 chain ID, i.e., a human-readable namespace and reference.
3032
*/
31-
export const CaipChainIdStruct = pattern(
32-
string(),
33+
export const CaipChainIdStruct = definePattern<`${string}:${string}`>(
34+
'CaipChainId',
3335
CAIP_CHAIN_ID_REGEX,
34-
) as Struct<CaipChainId, null>;
35-
export type CaipChainId = `${string}:${string}`;
36+
);
37+
export type CaipChainId = Infer<typeof CaipChainIdStruct>;
3638

3739
/**
3840
* A CAIP-2 namespace, i.e., the first part of a CAIP chain ID.
3941
*/
40-
export const CaipNamespaceStruct = pattern(string(), CAIP_NAMESPACE_REGEX);
42+
export const CaipNamespaceStruct = definePattern(
43+
'CaipNamespace',
44+
CAIP_NAMESPACE_REGEX,
45+
);
4146
export type CaipNamespace = Infer<typeof CaipNamespaceStruct>;
4247

4348
/**
4449
* A CAIP-2 reference, i.e., the second part of a CAIP chain ID.
4550
*/
46-
export const CaipReferenceStruct = pattern(string(), CAIP_REFERENCE_REGEX);
51+
export const CaipReferenceStruct = definePattern(
52+
'CaipReference',
53+
CAIP_REFERENCE_REGEX,
54+
);
4755
export type CaipReference = Infer<typeof CaipReferenceStruct>;
4856

4957
/**
5058
* A CAIP-10 account ID, i.e., a human-readable namespace, reference, and account address.
5159
*/
52-
export const CaipAccountIdStruct = pattern(
53-
string(),
54-
CAIP_ACCOUNT_ID_REGEX,
55-
) as Struct<CaipAccountId, null>;
56-
export type CaipAccountId = `${string}:${string}:${string}`;
60+
export const CaipAccountIdStruct =
61+
definePattern<`${string}:${string}:${string}`>(
62+
'CaipAccountId',
63+
CAIP_ACCOUNT_ID_REGEX,
64+
);
65+
export type CaipAccountId = Infer<typeof CaipAccountIdStruct>;
5766

5867
/**
5968
* A CAIP-10 account address, i.e., the third part of the CAIP account ID.
6069
*/
61-
export const CaipAccountAddressStruct = pattern(
62-
string(),
70+
export const CaipAccountAddressStruct = definePattern(
71+
'CaipAccountAddress',
6372
CAIP_ACCOUNT_ADDRESS_REGEX,
6473
);
6574
export type CaipAccountAddress = Infer<typeof CaipAccountAddressStruct>;
6675

6776
/**
6877
* A CAIP-19 asset namespace, i.e., a namespace domain of an asset.
6978
*/
70-
export const CaipAssetNamespaceStruct = pattern(
71-
string(),
79+
export const CaipAssetNamespaceStruct = definePattern(
80+
'CaipAssetNamespace',
7281
CAIP_ASSET_NAMESPACE_REGEX,
7382
);
7483
export type CaipAssetNamespace = Infer<typeof CaipAssetNamespaceStruct>;
7584

7685
/**
7786
* A CAIP-19 asset reference, i.e., an identifier for an asset within a given namespace.
7887
*/
79-
export const CaipAssetReferenceStruct = pattern(
80-
string(),
88+
export const CaipAssetReferenceStruct = definePattern(
89+
'CaipAssetReference',
8190
CAIP_ASSET_REFERENCE_REGEX,
8291
);
8392
export type CaipAssetReference = Infer<typeof CaipAssetReferenceStruct>;
8493

8594
/**
8695
* A CAIP-19 asset token ID, i.e., a unique identifier for an addressable asset of a given type
8796
*/
88-
export const CaipTokenIdStruct = pattern(string(), CAIP_TOKEN_ID_REGEX);
97+
export const CaipTokenIdStruct = definePattern(
98+
'CaipTokenId',
99+
CAIP_TOKEN_ID_REGEX,
100+
);
89101
export type CaipTokenId = Infer<typeof CaipTokenIdStruct>;
90102

91103
/**
92104
* A CAIP-19 asset type identifier, i.e., a human-readable type of asset identifier.
93105
*/
94-
export const CaipAssetTypeStruct = pattern(
95-
string(),
96-
CAIP_ASSET_TYPE_REGEX,
97-
) as Struct<CaipAssetType, null>;
98-
export type CaipAssetType = `${string}:${string}/${string}:${string}`;
106+
export const CaipAssetTypeStruct =
107+
definePattern<`${string}:${string}/${string}:${string}`>(
108+
'CaipAssetType',
109+
CAIP_ASSET_TYPE_REGEX,
110+
);
111+
export type CaipAssetType = Infer<typeof CaipAssetTypeStruct>;
99112

100113
/**
101114
* A CAIP-19 asset ID identifier, i.e., a human-readable type of asset ID.
102115
*/
103-
export const CaipAssetIdStruct = pattern(
104-
string(),
105-
CAIP_ASSET_ID_REGEX,
106-
) as Struct<CaipAssetId, null>;
107-
export type CaipAssetId = `${string}:${string}/${string}:${string}/${string}`;
116+
export const CaipAssetIdStruct =
117+
definePattern<`${string}:${string}/${string}:${string}/${string}`>(
118+
'CaipAssetId',
119+
CAIP_ASSET_ID_REGEX,
120+
);
121+
export type CaipAssetId = Infer<typeof CaipAssetIdStruct>;
108122

109123
/** Known CAIP namespaces. */
110124
export enum KnownCaipNamespace {

src/index.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ describe('index', () => {
8787
"createModuleLogger",
8888
"createNumber",
8989
"createProjectLogger",
90+
"definePattern",
9091
"exactOptional",
9192
"getChecksumAddress",
9293
"getErrorMessage",

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './misc';
1515
export * from './number';
1616
export * from './opaque';
1717
export * from './promise';
18+
export * from './superstruct';
1819
export * from './time';
1920
export * from './transaction-types';
2021
export * from './versions';

src/node.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ describe('node', () => {
8888
"createNumber",
8989
"createProjectLogger",
9090
"createSandbox",
91+
"definePattern",
9192
"directoryExists",
9293
"ensureDirectoryStructureExists",
9394
"exactOptional",

src/superstruct.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { assert, is, pattern, string } from '@metamask/superstruct';
2+
3+
import { definePattern } from './superstruct';
4+
5+
describe('definePattern', () => {
6+
const hexPattern = /^0x[0-9a-f]+$/u;
7+
const HexStringPattern = pattern(string(), hexPattern);
8+
const HexString = definePattern('HexString', hexPattern);
9+
10+
it('is similar to superstruct.pattern', () => {
11+
expect(is('0xdeadbeef', HexStringPattern)).toBe(true);
12+
expect(is('0xdeadbeef', HexString)).toBe(true);
13+
expect(is('foobar', HexStringPattern)).toBe(false);
14+
expect(is('foobar', HexString)).toBe(false);
15+
});
16+
17+
it('throws and error if assert fails', () => {
18+
const value = 'foobar';
19+
expect(() => assert(value, HexString)).toThrow(
20+
`Expected a value of type \`HexString\`, but received: \`"foobar"\``,
21+
);
22+
});
23+
});

src/superstruct.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { Struct } from '@metamask/superstruct';
2+
import { define } from '@metamask/superstruct';
3+
4+
/**
5+
* Defines a new string-struct matching a regular expression.
6+
*
7+
* @example
8+
* const EthAddressStruct = definePattern('EthAddress', /^0x[0-9a-f]{40}$/iu);
9+
* type EthAddress = Infer<typeof EthAddressStruct>; // string
10+
*
11+
* const CaipChainIdStruct = defineTypedPattern<`${string}:${string}`>(
12+
* 'CaipChainId',
13+
* /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u;
14+
* );
15+
* type CaipChainId = Infer<typeof CaipChainIdStruct>; // `${string}:${string}`
16+
* @param name - Type name.
17+
* @param pattern - Regular expression to match.
18+
* @template Pattern - The pattern type, defaults to `string`.
19+
* @returns A new string-struct that matches the given pattern.
20+
*/
21+
export function definePattern<Pattern extends string = string>(
22+
name: string,
23+
pattern: RegExp,
24+
): Struct<Pattern, null> {
25+
return define<Pattern>(name, (value: unknown): boolean | string => {
26+
return typeof value === 'string' && pattern.test(value);
27+
});
28+
}

0 commit comments

Comments
 (0)