UNPKG

@solana/codecs-data-structures

Version:

Codecs for various data structures

247 lines (235 loc) 10.3 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { Codec, combineCodec, Decoder, Encoder, ReadonlyUint8Array } from '@solana/codecs-core'; import { SOLANA_ERROR__CODECS__INVALID_PATTERN_MATCH_BYTES, SOLANA_ERROR__CODECS__INVALID_PATTERN_MATCH_VALUE, SolanaError, } from '@solana/errors'; import { getUnionDecoder, getUnionEncoder } from './union'; import { GetEncoderTypeFromVariants, GetUnionCodecType, GetUnionDecoderType, GetUnionEncoderType } from './utils'; // A boolean predicate whose parameter is checked bivariantly (method-syntax escape hatch), so a // predicate that narrows to a subtype of the variant's value type — e.g. `(value: number)` against a // number codec whose value type is `number | bigint` — is still accepted, mirroring `getPredicateCodec`. type BivariantBooleanPredicate<TFrom> = { predicate(value: TFrom): boolean }['predicate']; type PatternMatchEncoderEntry<TNarrowed, TFrom = TNarrowed> = TNarrowed extends TFrom ? // Type predicate with narrowed encoder | readonly [(value: TFrom) => value is TNarrowed, Encoder<TNarrowed>] // Boolean predicate with original encoder | readonly [BivariantBooleanPredicate<TFrom>, Encoder<TFrom>] : never; type PatternMatchDecoderEntry<TTo> = readonly [(bytes: ReadonlyUint8Array) => boolean, Decoder<TTo>]; /** Extracts the tuple of variant encoders from a tuple of `[predicate, encoder]` entries. */ type GetPatternMatchEncoders<TPatterns extends readonly PatternMatchEncoderEntry<any>[]> = { [I in keyof TPatterns]: TPatterns[I] extends readonly [unknown, infer TEncoder extends Encoder<any>] ? TEncoder : never; }; /** Extracts the tuple of variant decoders from a tuple of `[predicate, decoder]` entries. */ type GetPatternMatchDecoders<TPatterns extends readonly PatternMatchDecoderEntry<any>[]> = { [I in keyof TPatterns]: TPatterns[I] extends readonly [unknown, infer TDecoder extends Decoder<any>] ? TDecoder : never; }; /** Extracts the tuple of variant codecs from a tuple of `[valuePredicate, bytesPredicate, codec]` entries. */ type GetPatternMatchCodecs<TPatterns extends readonly PatternMatchCodecEntry<any>[]> = { [I in keyof TPatterns]: TPatterns[I] extends readonly [unknown, unknown, infer TCodec extends Codec<any>] ? TCodec : never; }; /** * Returns an encoder that selects which variant encoder to use based on pattern matching. * * This encoder evaluates the value against a series of predicate functions in order, * and uses the first matching encoder to encode the value. * * @typeParam TFrom - The type of the value to encode. * * @param patterns - An array of `[predicate, encoder]` pairs. Predicates are tested in order * and the first matching encoder is used to encode the value. Note that predicates can be either * type predicates that narrow the type of the value, or boolean predicates. If using type predicates, * the encoder can be for the narrowed type. * @returns An encoder that selects the appropriate variant based on the matched pattern. * * @throws Throws a {@link SOLANA_ERROR__CODECS__INVALID_PATTERN_MATCH_VALUE} error * if the value does not match any of the specified patterns. * * @example * Encoding values using pattern matching. * ```ts * const encoder = getPatternMatchEncoder([ * [(n: number) => n < 256, getU8Encoder()], * [(n: number) => n < 2 ** 16, getU16Encoder()], * [(n: number) => n < 2 ** 32, getU32Encoder()] * ]); * * encoder.encode(42); * // 0x2a * // └── Small number encoded as u8 * * encoder.encode(1000); * // 0xe803 * // └── Medium number encoded as u16 * * * encoder.encode(100_000); * // 0xa0860100 * // └── Large number encoded as u32 * * ender.encode(2 ** 32 + 1); * // Throws an error because the value does not match any pattern * ``` * * @see {@link getPatternMatchCodec} */ export function getPatternMatchEncoder<const TPatterns extends readonly PatternMatchEncoderEntry<any>[]>( patterns: TPatterns & readonly PatternMatchEncoderEntry<GetEncoderTypeFromVariants<GetPatternMatchEncoders<TPatterns>>>[], ): GetUnionEncoderType<GetPatternMatchEncoders<TPatterns>>; export function getPatternMatchEncoder<TFrom>(patterns: PatternMatchEncoderEntry<TFrom>[]): Encoder<TFrom> { return getUnionEncoder( patterns.map(([, encoder]) => encoder), (value: TFrom) => { const index = patterns.findIndex(([predicate]) => predicate(value)); if (index === -1) { throw new SolanaError(SOLANA_ERROR__CODECS__INVALID_PATTERN_MATCH_VALUE); } return index; }, ); } /** * Returns a decoder that selects which variant decoder to use based on pattern matching. * * This decoder evaluates the byte array against a series of predicate functions in order, * and uses the first matching decoder to decode the value. * * @typeParam TTo - The type of the value to decode. * * @param patterns - An array of `[predicate, decoder]` pairs. Predicates are tested in order * and the first matching decoder is used to decode the byte array. * @returns A decoder that selects the appropriate variant based on the matched byte pattern. * * @throws Throws a {@link SOLANA_ERROR__CODECS__INVALID_PATTERN_MATCH_BYTES} error * if the byte array does not match any of the specified patterns. * * @example * Decoding values using pattern matching on bytes. * ```ts * const decoder = getPatternMatchDecoder([ * [(bytes) => bytes.length === 1, getU8Decoder()], * [(bytes) => bytes.length === 2, getU16Decoder()], * [(bytes) => bytes.length <= 4, getU32Decoder()] * ]); * * decoder.decode(new Uint8Array([0x2a])); // 42 (decoded as u8) * decoder.decode(new Uint8Array([0xe8, 0x03])) // 1000 (decoded as u16) * decoder.decode(new Uint8Array([0xa0, 0x86, 0x01, 0x00])) // 100_000 (decoded as u32) * decoder.decode(new Uint8Array([0xa0, 0x86, 0x01, 0x00, 0x00])) * // Throws an error because the bytes do not match any pattern * ``` * * @see {@link getPatternMatchCodec} * @see {@link getPatternMatchEncoder} */ export function getPatternMatchDecoder<const TPatterns extends readonly PatternMatchDecoderEntry<any>[]>( patterns: TPatterns, ): GetUnionDecoderType<GetPatternMatchDecoders<TPatterns>>; export function getPatternMatchDecoder<TTo>( patterns: [(value: ReadonlyUint8Array) => boolean, Decoder<TTo>][], ): Decoder<TTo> { return getUnionDecoder( patterns.map(([, decoder]) => decoder), (value: ReadonlyUint8Array) => { const index = patterns.findIndex(([predicate]) => predicate(value)); if (index === -1) { throw new SolanaError(SOLANA_ERROR__CODECS__INVALID_PATTERN_MATCH_BYTES, { bytes: value, }); } return index; }, ); } type PatternMatchCodecEntry<TNarrowedFrom, TFrom = TNarrowedFrom, TTo = TNarrowedFrom> = TNarrowedFrom extends TFrom ? TTo extends TNarrowedFrom ? | readonly [ (value: TFrom) => value is TNarrowedFrom, (bytes: ReadonlyUint8Array) => boolean, Codec<TNarrowedFrom, TTo>, ] | readonly [BivariantBooleanPredicate<TFrom>, (bytes: ReadonlyUint8Array) => boolean, Codec<TFrom, TTo>] : never : never; /** * Returns a codec that selects which variant codec to use based on pattern matching. * * This codec evaluates values and byte arrays against a series of predicate functions in order, * using the first matching codec for encoding or decoding. * * @typeParam TFrom - The type of the value to encode. * @typeParam TTo - The type of the value to decode. * * @param patterns - An array of `[valuePredicate, bytesPredicate, codec]` triples. Predicates * are tested in order and the first match determines the codec used. During encoding, * `valuePredicate` receives the value to encode. During decoding, `bytesPredicate` receives * the byte array. * @returns A codec that selects the appropriate variant based on the matched pattern. * * @throws Throws a {@link SOLANA_ERROR__CODECS__INVALID_PATTERN_MATCH_VALUE} error * if a value being encoded does not match any of the specified patterns. * @throws Throws a {@link SOLANA_ERROR__CODECS__INVALID_PATTERN_MATCH_BYTES} error * if a byte array being decoded does not match any of the specified patterns. * * @example * Encoding and decoding using pattern matching. * ```ts * const codec = getPatternMatchCodec([ * [ * (n: number) => n < 256, * (bytes) => bytes.length === 1, * getU8Codec(), * ], * [ * (n: number) => n < 2 ** 16, * (bytes) => bytes.length === 2, * getU16Codec(), * ], * [ * (n: number) => n < 2 ** 32, * (bytes) => bytes.length <= 4, * getU32Codec(), * ] * ]); * * const bytes1 = codec.encode(42); // 0x2a, encoded as u8 * const value1 = codec.decode(bytes1); // 42, decoded as u8 * * const bytes2 = codec.encode(1000); // 0xe803, encoded as u16 * const value2 = codec.decode(bytes2); // 1000, decoded as u16 * * const bytes3 = codec.encode(100_000); //0xa0860100, encoded as u32 * const value3 = codec.decode(bytes3); // 100_000, decoded as u32 * * codec.encode(2 ** 32 + 1); * // throws, no encode pattern matches * codec.decode(new Uint8Array([0xa0, 0x86, 0x01, 0x00, 0x00])) * // throws, no decode pattern matches * ``` * * @see {@link getPatternMatchEncoder} * @see {@link getPatternMatchDecoder} * @see {@link getUnionCodec} */ export function getPatternMatchCodec<const TPatterns extends readonly PatternMatchCodecEntry<any>[]>( patterns: TPatterns & readonly PatternMatchCodecEntry<GetEncoderTypeFromVariants<GetPatternMatchCodecs<TPatterns>>>[], ): GetUnionCodecType<GetPatternMatchCodecs<TPatterns>>; export function getPatternMatchCodec<TFrom, TTo extends TFrom = TFrom>( patterns: PatternMatchCodecEntry<TFrom, TFrom, TTo>[], ): Codec<TFrom, TTo> { return combineCodec( getPatternMatchEncoder(patterns.map(([valuePredicate, , codec]) => [valuePredicate, codec]) as any), getPatternMatchDecoder(patterns.map(([, bytesPredicate, codec]) => [bytesPredicate, codec]) as any), ); }