Skip to content

Commit

Permalink
feat: named CAIP structs (#228)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: Maarten Zuidhoorn <[email protected]>
  • Loading branch information
3 people authored Jan 29, 2025
1 parent 48267f8 commit 9ac7ec2
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 30 deletions.
74 changes: 44 additions & 30 deletions src/caip-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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';

export const CAIP_CHAIN_ID_REGEX =
/^(?<namespace>[-a-z0-9]{3,8}):(?<reference>[-_a-zA-Z0-9]{1,32})$/u;
Expand Down Expand Up @@ -28,83 +30,95 @@ 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<CaipChainId, null>;
export type CaipChainId = `${string}:${string}`;
);
export type CaipChainId = Infer<typeof CaipChainIdStruct>;

/**
* 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<typeof CaipNamespaceStruct>;

/**
* 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<typeof CaipReferenceStruct>;

/**
* 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<CaipAccountId, null>;
export type CaipAccountId = `${string}:${string}:${string}`;
export const CaipAccountIdStruct =
definePattern<`${string}:${string}:${string}`>(
'CaipAccountId',
CAIP_ACCOUNT_ID_REGEX,
);
export type CaipAccountId = Infer<typeof CaipAccountIdStruct>;

/**
* 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<typeof CaipAccountAddressStruct>;

/**
* 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<typeof CaipAssetNamespaceStruct>;

/**
* 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<typeof CaipAssetReferenceStruct>;

/**
* 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<typeof CaipTokenIdStruct>;

/**
* 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<CaipAssetType, null>;
export type CaipAssetType = `${string}:${string}/${string}:${string}`;
export const CaipAssetTypeStruct =
definePattern<`${string}:${string}/${string}:${string}`>(
'CaipAssetType',
CAIP_ASSET_TYPE_REGEX,
);
export type CaipAssetType = Infer<typeof CaipAssetTypeStruct>;

/**
* 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<CaipAssetId, null>;
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<typeof CaipAssetIdStruct>;

/** Known CAIP namespaces. */
export enum KnownCaipNamespace {
Expand Down
1 change: 1 addition & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe('index', () => {
"createModuleLogger",
"createNumber",
"createProjectLogger",
"definePattern",
"exactOptional",
"getChecksumAddress",
"getErrorMessage",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions src/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describe('node', () => {
"createNumber",
"createProjectLogger",
"createSandbox",
"definePattern",
"directoryExists",
"ensureDirectoryStructureExists",
"exactOptional",
Expand Down
23 changes: 23 additions & 0 deletions src/superstruct.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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', () => {
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"\``,
);
});
});
28 changes: 28 additions & 0 deletions src/superstruct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Struct } from '@metamask/superstruct';
import { define } from '@metamask/superstruct';

/**
* Defines a new string-struct matching a regular expression.
*
* @example
* const EthAddressStruct = definePattern('EthAddress', /^0x[0-9a-f]{40}$/iu);
* type EthAddress = Infer<typeof EthAddressStruct>; // string
*
* const CaipChainIdStruct = defineTypedPattern<`${string}:${string}`>(
* 'CaipChainId',
* /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/u;
* );
* type CaipChainId = Infer<typeof CaipChainIdStruct>; // `${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<Pattern extends string = string>(
name: string,
pattern: RegExp,
): Struct<Pattern, null> {
return define<Pattern>(name, (value: unknown): boolean | string => {
return typeof value === 'string' && pattern.test(value);
});
}

0 comments on commit 9ac7ec2

Please sign in to comment.