-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: New version of ua-utils, aligned with below-zero's model of con…
…figurables
- Loading branch information
1 parent
32a99f4
commit 69a6e07
Showing
12 changed files
with
424 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dist | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"extensions": ["ts"], | ||
"spec": ["**/*.test.*"], | ||
"loader": "ts-node/esm" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dist/ | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
<p align="center"> | ||
<a href="https://layerzero.network"> | ||
<img alt="LayerZero" style="max-width: 500px" src="https://d3a2dpnnrypp5h.cloudfront.net/bridge-app/lz.png"/> | ||
</a> | ||
</p> | ||
|
||
<h1 align="center">@layerzerolabs/ua-utils</h1> | ||
|
||
<!-- The badges section --> | ||
<p align="center"> | ||
<!-- Shields.io NPM published package version --> | ||
<a href="https://www.npmjs.com/package/@layerzerolabs/ua-utils"><img alt="NPM Version" src="https://img.shields.io/npm/v/@layerzerolabs/ua-utils"/></a> | ||
<!-- Shields.io NPM downloads --> | ||
<a href="https://www.npmjs.com/package/@layerzerolabs/ua-utils"><img alt="Downloads" src="https://img.shields.io/npm/dm/@layerzerolabs/ua-utils"/></a> | ||
<!-- Shields.io license badge --> | ||
<a href="https://www.npmjs.com/package/@layerzerolabs/ua-utils"><img alt="NPM License" src="https://img.shields.io/npm/l/@layerzerolabs/ua-utils"/></a> | ||
</p> | ||
|
||
## Installation | ||
|
||
```bash | ||
yarn add @layerzerolabs/ua-utils | ||
|
||
pnpm add @layerzerolabs/ua-utils | ||
|
||
npm install @layerzerolabs/ua-utils | ||
``` | ||
|
||
## Usage | ||
|
||
### `createProperty` | ||
|
||
Creates a `Property` object - an abstract wrapper around a getter and setter of a (typically contract) property. | ||
|
||
`Property` objects can be evaluated - this evaluation results in a `PropertyState` object which comes in two varieties: | ||
|
||
- `Configured` object represents a property whose desired value matches the current, actual value | ||
- `Misconfigured` object represents a property whose desired value does not match the current value. It can be further executed using its `configure` method to set the desired property value | ||
|
||
```typescript | ||
import type { Contract } from "@etherspropject/contracts" | ||
import { createProperty, isMisconfigured } from "@layerzerolabs/ua-utils" | ||
|
||
// In this example we'll creating a property that is executed within a context of a contract | ||
// | ||
// What this means is that the property requires a contract to be evaluated. This context | ||
// is then passed to the getter, setter and the desired value getter | ||
const myContractProperty = createProperty({ | ||
get: (contract: Contract) => contract.getFavouriteAddress(), | ||
set: (contract: Contract, favouriteAddress: string) => contract.setFavouriteAddress(favouriteAddress), | ||
desired: (contract: Contract) => "0x00000000219ab540356cbb839cbe05303d7705fa", | ||
}) | ||
|
||
// Let's pretend we have a contract at hand | ||
declare const myContract: Contract | ||
|
||
// We'll evaluate this property by passing myContract in. This contract will then be passed | ||
// to the getter, setter and desired value getter | ||
// | ||
// The result of this evaluation is a PropertyState object | ||
const state = await myContractProperty(myContract) | ||
|
||
// This package comes with two type narrowing utilities for working with PropertyState objects: | ||
// | ||
// - isConfigured | ||
// - isMisconfigured | ||
// | ||
// Using these we can discern between the two varieties of PropertyState | ||
if (isMisconfigured(state)) { | ||
await state.configure() | ||
} | ||
``` | ||
|
||
In the example above we used a simple `Contract` object as our context. We can use arbitrary context as the following example shows: | ||
|
||
```typescript | ||
import { createProperty, isMisconfigured } from "@layerzerolabs/ua-utils" | ||
|
||
const myContractPropertyWithParams = createProperty({ | ||
get: (contract: Contract, when: number, where: string) => contract.getFavouriteAddress(when, where), | ||
set: (contract: Contract, when: number, where: string, favouriteAddress: string) => | ||
contract.setFavouriteAddress(when, where, favouriteAddress), | ||
desired: (contract: Contract, when: number, where: string) => "0x00000000219ab540356cbb839cbe05303d7705fa", | ||
}) | ||
|
||
// We'll again pretend we have a contract at hand | ||
declare const myContract: Contract | ||
const when = Date.now() | ||
const where = "Antractica" | ||
|
||
// We'll evaluate this property by passing the required context in - in this case | ||
// the context consists of a contract, a numeric value and a string value | ||
// | ||
// The result of this evaluation is a PropertyState object | ||
const state = await myContractPropertyWithParams(myContract, when, where) | ||
|
||
// The execution goes just like before | ||
if (isMisconfigured(state)) { | ||
await state.configure() | ||
} | ||
``` | ||
|
||
### `isConfigured` | ||
|
||
Helper type assertion utility that narrows down the `PropertyState` type to `Configured`: | ||
|
||
```typescript | ||
import { PropertyState, isConfigured } from "@layerzerolabs/ua-utils" | ||
|
||
declare const state: PropertyState | ||
|
||
if (isConfigured(state)) { | ||
// state is now Configured, no action is needed as the property is in its desired state | ||
} | ||
``` | ||
|
||
### `isMisconfigured` | ||
|
||
Helper type assertion utility that narrows down the `PropertyState` type to `Misconfigured`: | ||
|
||
```typescript | ||
import { PropertyState, isMisconfigured } from "@layerzerolabs/ua-utils" | ||
|
||
declare const state: PropertyState | ||
|
||
if (isMisconfigured(state)) { | ||
// state is now Misconfigured, we can e.g. call .configure | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"name": "@layerzerolabs/ua-utils", | ||
"description": "Utilities for working with LayerZero projects", | ||
"version": "0.1.0", | ||
"license": "MIT", | ||
"private": true, | ||
"main": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"module": "./dist/index.mjs", | ||
"exports": { | ||
"types": "./dist/index.d.ts", | ||
"require": "./dist/index.js", | ||
"import": "./dist/index.mjs" | ||
}, | ||
"files": [ | ||
"./dist/index.*" | ||
], | ||
"scripts": { | ||
"build": "npx tsup", | ||
"clean": "rm -rf dist", | ||
"dev": "npx tsup --watch", | ||
"lint": "npx eslint '**/*.{js,ts,json}'", | ||
"prebuild": "npx tsc --noEmit -p tsconfig.build.json", | ||
"test": "mocha --parallel" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/LayerZero-Labs/lz-utils.git", | ||
"directory": "packages/ua-utils" | ||
}, | ||
"devDependencies": { | ||
"@types/mocha": "^10.0.1", | ||
"@types/sinon": "^17.0.2", | ||
"chai": "^4.3.10", | ||
"mocha": "^10.2.0", | ||
"sinon": "^17.0.1", | ||
"ts-node": "^10.9.1", | ||
"tsup": "~7.2.0", | ||
"typescript": "^5.2.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./property" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import assert from "assert" | ||
|
||
/** | ||
* The central concept of this module - a structure that can be evaluated into a `PropertyState` | ||
* | ||
* `Property` has three basic functions: | ||
* | ||
* - Property getter that gets the current value of the property | ||
* - Property setter that sets value of the property | ||
* - Property desired value getter that grabs the value this property should be set to | ||
*/ | ||
export type Property<TContext extends unknown[], TValue = unknown, TResult = unknown> = ( | ||
...context: TContext | ||
) => Promise<PropertyState<TValue, TResult>> | ||
|
||
/** | ||
* Type encapsulating two states of a configurable property: `Configured` and `Misconfigured` | ||
* | ||
* Property property is understood as anything that has a getter and setter | ||
* and its value needs to match a desired value (coming from some sort of a configuration). | ||
*/ | ||
export type PropertyState<TValue = unknown, TResult = unknown> = Configured<TValue> | Misconfigured<TValue, TResult> | ||
|
||
/** | ||
* Interface for configured state of a configurable property. | ||
* | ||
* In configured state, the current value of the property matches its desired state | ||
* and no action is necessary. | ||
*/ | ||
export interface Configured<TValue = unknown> { | ||
value: TValue | ||
desiredValue?: never | ||
configure?: never | ||
} | ||
|
||
/** | ||
* Interface for misconfigured state of a configurable property. | ||
* | ||
* In misconfigured state, the current value of the property does not match its desired state | ||
* and an action needs to be taken to synchronize these two. | ||
*/ | ||
export interface Misconfigured<TValue = unknown, TResult = unknown> { | ||
value: TValue | ||
desiredValue: TValue | ||
configure: () => TResult | Promise<TResult> | ||
} | ||
|
||
export type GetPropertyValue<TContext extends unknown[], TValue = unknown> = (...context: TContext) => TValue | Promise<TValue> | ||
|
||
export type SetPropertyValue<TContext extends unknown[], TValue = unknown, TResult = unknown> = ( | ||
...params: [...TContext, TValue] | ||
) => TResult | Promise<TResult> | ||
|
||
export interface PropertyOptions<TContext extends unknown[], TValue, TResult> { | ||
desired: GetPropertyValue<TContext, TValue> | ||
get: GetPropertyValue<TContext, TValue> | ||
set: SetPropertyValue<TContext, TValue, TResult> | ||
} | ||
|
||
/** | ||
* Property factory, the central functional piece of this module. | ||
* | ||
* @param `PropertyOptions<TContext extends unknown[], TValue = unknown, TResult = unknown>` | ||
* | ||
* @returns `Property<TContext, TValue, TResult>` | ||
*/ | ||
export const createProperty = | ||
<TContext extends unknown[], TValue = unknown, TResult = unknown>({ | ||
get, | ||
set, | ||
desired, | ||
}: PropertyOptions<TContext, TValue, TResult>): Property<TContext, TValue, TResult> => | ||
async (...context) => { | ||
// First we grab the current & desired states of the property | ||
const [value, desiredValue] = await Promise.all([get(...context), desired(...context)]) | ||
|
||
// Now we compare the current & desired states using value equality (i.e. values | ||
// with the same shape will be considered equal) | ||
// | ||
// We'll use the native deep equality function that throws an AssertionError | ||
// when things don't match so we need to try/catch and understand the catch branch | ||
// as inequality | ||
try { | ||
assert.deepStrictEqual(value, desiredValue) | ||
|
||
// The values matched, we return a Configured | ||
return { value } | ||
} catch { | ||
// The values did not match, we'll return a Misconfigured | ||
return { value, desiredValue, configure: async () => set(...context, desiredValue) } | ||
} | ||
} | ||
|
||
/** | ||
* Type assertion utility for narrowing the `PropertyState` type to `Misconfigured` type | ||
* | ||
* @param value `PropertyState<TValue, TResult>` | ||
* @returns `value is Misconfigured<TValue, TResult>` | ||
*/ | ||
export const isMisconfigured = <TValue = unknown, TResult = unknown>( | ||
value: PropertyState<TValue, TResult> | ||
): value is Misconfigured<TValue, TResult> => "configure" in value && "desiredValue" in value && typeof value.configure === "function" | ||
|
||
/** | ||
* Type assertion utility for narrowing the `PropertyState` type to `Configured` type | ||
* | ||
* @param value `PropertyState<TValue, TResult>` | ||
* @returns `value is Configured<TValue, TResult>` | ||
*/ | ||
export const isConfigured = <TValue = unknown>(value: PropertyState<TValue>): value is Configured<TValue> => !isMisconfigured(value) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { expect } from "chai" | ||
import { describe } from "mocha" | ||
import sinon from "sinon" | ||
import { createProperty, isConfigured, isMisconfigured } from "../src/property" | ||
|
||
describe("property", () => { | ||
describe("isMisconfigured", () => { | ||
it("should return true if value is Misconfigured", () => { | ||
expect(isMisconfigured({ value: false, desiredValue: true, configure: () => {} })).to.be.true | ||
expect(isMisconfigured({ value: null, desiredValue: null, configure: () => {} })).to.be.true | ||
expect(isMisconfigured({ value: 0, desiredValue: 0, configure: () => {} })).to.be.true | ||
}) | ||
|
||
it("should return false if value is Configured", () => { | ||
expect(isMisconfigured({ value: false })).to.be.false | ||
expect(isMisconfigured({ value: true })).to.be.false | ||
expect(isMisconfigured({ value: 1 })).to.be.false | ||
}) | ||
}) | ||
|
||
describe("isConfigured", () => { | ||
it("should return false if value is Configured", () => { | ||
expect(isConfigured({ value: false, desiredValue: true, configure: () => {} })).to.be.false | ||
expect(isConfigured({ value: null, desiredValue: null, configure: () => {} })).to.be.false | ||
expect(isConfigured({ value: 0, desiredValue: 0, configure: () => {} })).to.be.false | ||
}) | ||
|
||
it("should return true if value is Misconfigured", () => { | ||
expect(isConfigured({ value: false })).to.be.true | ||
expect(isConfigured({ value: true })).to.be.true | ||
expect(isConfigured({ value: 1 })).to.be.true | ||
}) | ||
}) | ||
|
||
describe("createProperty", () => { | ||
it("should return Configured if the current and desired values match", async () => { | ||
const currentValue = [1, "two", { three: true }] | ||
const desiredValue = [1, "two", { three: true }] | ||
|
||
const property = createProperty({ | ||
desired: async () => desiredValue, | ||
get: () => currentValue, | ||
set: () => {}, | ||
}) | ||
|
||
expect(isMisconfigured(await property())).to.be.false | ||
}) | ||
|
||
it("should return Misconfigured if the current and desired don't match", async () => { | ||
const currentValue = [1, "two", { three: true }] | ||
const desiredValue = [1, "two", { three: false }] | ||
|
||
const property = createProperty({ | ||
desired: async () => desiredValue, | ||
get: () => currentValue, | ||
set: () => {}, | ||
}) | ||
|
||
expect(isMisconfigured(await property())).to.be.true | ||
}) | ||
|
||
it("should call the setter with desired value when executed", async () => { | ||
const currentValue = [1, "two", { three: true }] | ||
const desiredValue = [1, "two", { three: false }] | ||
|
||
const set = sinon.spy() | ||
const property = createProperty({ | ||
desired: () => desiredValue, | ||
get: () => currentValue, | ||
set, | ||
}) | ||
|
||
const state = await property() | ||
await state.configure?.() | ||
|
||
expect(set.calledOnceWith(desiredValue)).to.be.true | ||
}) | ||
|
||
it("should call the setter with context when executed", async () => { | ||
const currentValue = [1, "two", { three: true }] | ||
const desiredValue = [1, "two", { three: false }] | ||
|
||
const set = sinon.spy() | ||
const property = createProperty({ | ||
desired: (context: string) => desiredValue, | ||
get: (context: string) => currentValue, | ||
set, | ||
}) | ||
|
||
const state = await property("context") | ||
await state.configure?.() | ||
|
||
expect(set.calledOnceWith("context", desiredValue)).to.be.true | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.