Skip to content

Commit

Permalink
Extract serializer modules and use in core umi library (#68)
Browse files Browse the repository at this point in the history
* Create new packages for sub-libraries

* Extract umi-options package

* Extract umi-serializers-core

* Extract umi-serializers-encodings

* Extract umi-public-keys

* Extract umi-serializers-numbers

* Refactor toDataView helper

* Create numberFactory helper

* Refactor integer tests

* Refactor other tests

* Extract each number serializer into its own file

* Cleanup unused test helpers

* Extract umi-serializers

* Refactor tests

* Remove option and pubkey code from main umi lib

* Extract serializers and export as sub-path

* Import serializer options from lib

* Re-exports subset of serializers for backwards compatibility

And mark them as deprecated

* Remove unused export

* Fix tests

* Delegate dataViewSerializer to umi-serializers

* Add deprecation notices for serializer interfaces

* Fix test and linting

* Add serializers submodule to externals of consumers

* Add changeset

* Add OptionOrNullable type
  • Loading branch information
lorisleiva authored Jun 19, 2023
1 parent abb3011 commit 4accd34
Show file tree
Hide file tree
Showing 201 changed files with 4,461 additions and 2,755 deletions.
14 changes: 14 additions & 0 deletions .changeset/brown-ties-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@metaplex-foundation/umi-transaction-factory-web3js': patch
'@metaplex-foundation/umi-serializers-encodings': patch
'@metaplex-foundation/umi-serializer-data-view': patch
'@metaplex-foundation/umi-serializers-numbers': patch
'@metaplex-foundation/umi-serializers-core': patch
'@metaplex-foundation/umi-serializer-beet': patch
'@metaplex-foundation/umi-public-keys': patch
'@metaplex-foundation/umi-serializers': patch
'@metaplex-foundation/umi-options': patch
'@metaplex-foundation/umi': patch
---

Extract serializer modules and use in core umi library
9 changes: 9 additions & 0 deletions packages/umi-options/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# umi-options

A TypeScript implementation of Rust Options

## Installation

```sh
npm install @metaplex-foundation/umi-options
```
3 changes: 3 additions & 0 deletions packages/umi-options/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../babel.config.json"
}
55 changes: 55 additions & 0 deletions packages/umi-options/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@metaplex-foundation/umi-options",
"version": "0.0.1",
"description": "A TypeScript implementation of Rust Options",
"license": "MIT",
"sideEffects": false,
"module": "dist/esm/index.mjs",
"main": "dist/cjs/index.cjs",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
},
"files": [
"/dist/cjs",
"/dist/esm",
"/dist/types",
"/src"
],
"scripts": {
"lint": "eslint --ext js,ts,tsx src",
"lint:fix": "eslint --fix --ext js,ts,tsx src",
"clean": "rimraf dist",
"build": "pnpm clean && tsc && tsc -p test/tsconfig.json && rollup -c",
"test": "ava"
},
"devDependencies": {
"@ava/typescript": "^3.0.1",
"ava": "^5.1.0"
},
"publishConfig": {
"access": "public"
},
"author": "Metaplex Maintainers <[email protected]>",
"homepage": "https://metaplex.com",
"repository": {
"url": "https://github.com/metaplex-foundation/umi.git"
},
"typedoc": {
"entryPoint": "./src/index.ts",
"readmeFile": "./README.md",
"displayName": "umi-options"
},
"ava": {
"typescript": {
"compile": false,
"rewritePaths": {
"src/": "dist/test/src/",
"test/": "dist/test/test/"
}
}
}
}
16 changes: 16 additions & 0 deletions packages/umi-options/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createConfigs } from '../../rollup.config';
import pkg from './package.json';

export default createConfigs({
pkg,
builds: [
{
dir: 'dist/esm',
format: 'es',
},
{
dir: 'dist/cjs',
format: 'cjs',
},
],
});
81 changes: 81 additions & 0 deletions packages/umi-options/src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Defines a type `T` that can also be `null`.
* @category Utils — Options
*/
export type Nullable<T> = T | null;

/**
* An implementation of the Rust Option type in JavaScript.
* It can be one of the following:
* - <code>{@link Some}<T></code>: Meaning there is a value of type T.
* - <code>{@link None}</code>: Meaning there is no value.
*
* @category Utils — Options
*/
export type Option<T> = Some<T> | None;

/**
* Defines a looser type that can be used when serializing an {@link Option}.
* This allows us to pass null or the Option value directly whilst still
* supporting the Option type for use-cases that need more type safety.
*
* @category Utils — Options
*/
export type OptionOrNullable<T> = Option<T> | Nullable<T>;

/**
* Represents an option of type `T` that has a value.
*
* @see {@link Option}
* @category Utils — Options
*/
export type Some<T> = { __option: 'Some'; value: T };

/**
* Represents an option of type `T` that has no value.
*
* @see {@link Option}
* @category Utils — Options
*/
export type None = { __option: 'None' };

/**
* Creates a new {@link Option} of type `T` that has a value.
*
* @see {@link Option}
* @category Utils — Options
*/
export const some = <T>(value: T): Option<T> => ({ __option: 'Some', value });

/**
* Creates a new {@link Option} of type `T` that has no value.
*
* @see {@link Option}
* @category Utils — Options
*/
export const none = <T>(): Option<T> => ({ __option: 'None' });

/**
* Whether the given data is an {@link Option}.
* @category Utils — Options
*/
export const isOption = <T = unknown>(input: any): input is Option<T> =>
input &&
typeof input === 'object' &&
'__option' in input &&
((input.__option === 'Some' && 'value' in input) ||
input.__option === 'None');

/**
* Whether the given {@link Option} is a {@link Some}.
* @category Utils — Options
*/
export const isSome = <T>(option: Option<T>): option is Some<T> =>
option.__option === 'Some';

/**
* Whether the given {@link Option} is a {@link None}.
* @category Utils — Options
*/
export const isNone = <T>(option: Option<T>): option is None =>
option.__option === 'None';
3 changes: 3 additions & 0 deletions packages/umi-options/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './common';
export * from './unwrapOption';
export * from './unwrapOptionRecursively';
50 changes: 50 additions & 0 deletions packages/umi-options/src/unwrapOption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Nullable, Option, isSome, none, some } from './common';

/**
* Unwraps the value of an {@link Option} of type `T`
* or returns a fallback value that defaults to `null`.
*
* @category Utils — Options
*/
export function unwrapOption<T>(option: Option<T>): Nullable<T>;
export function unwrapOption<T, U>(option: Option<T>, fallback: () => U): T | U;
export function unwrapOption<T, U = null>(
option: Option<T>,
fallback?: () => U
): T | U {
if (isSome(option)) return option.value;
return fallback ? fallback() : (null as U);
}

/**
* Wraps a nullable value into an {@link Option}.
*
* @category Utils — Options
*/
export const wrapNullable = <T>(nullable: Nullable<T>): Option<T> =>
nullable !== null ? some(nullable) : none<T>();

/**
* Unwraps the value of an {@link Option} of type `T`.
* If the option is a {@link Some}, it returns its value,
* Otherwise, it returns `null`.
*
* @category Utils — Options
* @deprecated Use {@link unwrapOption} instead.
*/
export const unwrapSome = <T>(option: Option<T>): Nullable<T> =>
isSome(option) ? option.value : null;

/**
* Unwraps the value of an {@link Option} of type `T`
* or returns a custom fallback value.
* If the option is a {@link Some}, it returns its value,
* Otherwise, it returns the return value of the provided fallback callback.
*
* @category Utils — Options
* @deprecated Use {@link unwrapOption} instead.
*/
export const unwrapSomeOrElse = <T, U>(
option: Option<T>,
fallback: () => U
): T | U => (isSome(option) ? option.value : fallback());
65 changes: 65 additions & 0 deletions packages/umi-options/src/unwrapOptionRecursively.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { None, Some, isOption, isSome } from './common';

/**
* A type that defines the recursive unwrapping of a type `T`
* such that all nested {@link Option} types are unwrapped.
*
* For each nested {@link Option} type, if the option is a {@link Some},
* it returns the type of its value, otherwise, it returns the provided
* fallback type `U` which defaults to `null`.
*
* @category Utils — Options
*/
type UnwrappedOption<T, U = null> = T extends Some<infer TValue>
? UnwrappedOption<TValue, U>
: T extends None
? U
: T extends object
? { [key in keyof T]: UnwrappedOption<T[key], U> }
: T extends Array<infer TItem>
? Array<UnwrappedOption<TItem, U>>
: T;

/**
* Recursively go through a type `T`such that all
* nested {@link Option} types are unwrapped.
*
* For each nested {@link Option} type, if the option is a {@link Some},
* it returns its value, otherwise, it returns the provided fallback value
* which defaults to `null`.
*
* @category Utils — Options
*/
export function unwrapOptionRecursively<T>(input: T): UnwrappedOption<T>;
export function unwrapOptionRecursively<T, U>(
input: T,
fallback: () => U
): UnwrappedOption<T, U>;
export function unwrapOptionRecursively<T, U = null>(
input: T,
fallback?: () => U
): UnwrappedOption<T, U> {
// Because null passes `typeof input === 'object'`.
if (!input) return input as UnwrappedOption<T, U>;
const next = <X>(x: X) =>
(fallback
? unwrapOptionRecursively(x, fallback)
: unwrapOptionRecursively(x)) as UnwrappedOption<X, U>;

// Handle Option.
if (isOption(input)) {
if (isSome(input)) return next(input.value) as UnwrappedOption<T, U>;
return (fallback ? fallback() : null) as UnwrappedOption<T, U>;
}

// Walk.
if (Array.isArray(input)) {
return input.map(next) as UnwrappedOption<T, U>;
}
if (typeof input === 'object') {
return Object.fromEntries(
Object.entries(input).map(([k, v]) => [k, next(v)])
) as UnwrappedOption<T, U>;
}
return input as UnwrappedOption<T, U>;
}
26 changes: 26 additions & 0 deletions packages/umi-options/test/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import test from 'ava';
import { isNone, isSome, none, Option, some } from '../src';

test('it can create Some and None options', (t) => {
const optionA: Option<number> = some(42);
t.deepEqual(optionA, { __option: 'Some', value: 42 });

const optionB: Option<null> = some(null);
t.deepEqual(optionB, { __option: 'Some', value: null });

const optionC: Option<unknown> = none();
t.deepEqual(optionC, { __option: 'None' });

const optionD: Option<string> = none<string>();
t.deepEqual(optionD, { __option: 'None' });
});

test('it can check if an option is Some or None', (t) => {
const optionA = some(42);
t.true(isSome(optionA));
t.false(isNone(optionA));

const optionB = none<number>();
t.false(isSome(optionB));
t.true(isNone(optionB));
});
11 changes: 11 additions & 0 deletions packages/umi-options/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../../tsconfig.json",
"include": ["./**/*"],
"compilerOptions": {
"module": "commonjs",
"outDir": "../dist/test",
"declarationDir": null,
"declaration": false,
"emitDeclarationOnly": false
}
}
36 changes: 36 additions & 0 deletions packages/umi-options/test/unwrapOption.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import test from 'ava';
import { none, some, unwrapOption, wrapNullable } from '../src';

test('it can unwrap an Option as a Nullable', (t) => {
t.is(unwrapOption(some(42)), 42);
t.is(unwrapOption(some(null)), null);
t.is(unwrapOption(some('hello')), 'hello');
t.is(unwrapOption(none()), null);
t.is(unwrapOption(none<number>()), null);
t.is(unwrapOption(none<string>()), null);
});

test('it can unwrap an Option using a fallback callback', (t) => {
const fallbackA = () => 42 as const;
t.is(unwrapOption(some(1), fallbackA), <number | 42>1);
t.is(unwrapOption(some('A'), fallbackA), <string | 42>'A');
t.is(unwrapOption(none(), fallbackA), <unknown | 42>42);

const fallbackB = () => {
throw new Error('Fallback Error');
};
t.is(unwrapOption(some(1), fallbackB), 1);
t.is(unwrapOption(some('A'), fallbackB), 'A');
t.throws(() => unwrapOption(none(), fallbackB), {
message: 'Fallback Error',
});
});

test('it can wrap a Nullable as an Option', (t) => {
t.deepEqual(wrapNullable(42), some(42));
t.deepEqual(wrapNullable('hello'), some('hello'));
t.deepEqual(wrapNullable(false), some(false));
t.deepEqual(wrapNullable(undefined), some(undefined));
t.deepEqual(wrapNullable<string>(null), none<string>());
t.deepEqual(wrapNullable<number>(null), none<number>());
});
Loading

0 comments on commit 4accd34

Please sign in to comment.