Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PR description you say that the struct is being inferred as a string, but it looks like we were using a type assertion before to ensure that this was not the case. So, was it really being inferred as a string? It's not clear to me what effects these changes have, as the only tests being added in this PR are for definePattern and not for any of the structs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pattern(string(), ...) would result in a type like this Struct<string, ...>

Meaning that using Infer<typeof MyType> would be inferred as string.

With the new definePattern helper I've added, we now uses a generic type to inject it into the Struct like so:

export const CaipChainIdStruct = pattern(
  string(),
  CAIP_CHAIN_ID_REGEX,
); // Would be: Struct<string, null> (which is why we were type-casting)

export const CaipChainIdStruct = definePattern<`${string}:${string}`>(
  'CaipChainId',
  CAIP_CHAIN_ID_REGEX,
); // Would be: Struct<`${string}:${string}`, null> (no more type-casting required)

In addition to that, we use define under the hood, which allows use to have a "named" struct (and better error messages).

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);
});
}
Loading