Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typesafe library #99

Closed
wants to merge 15 commits into from
Closed
59 changes: 25 additions & 34 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,49 @@ module.exports = {
browser: true,
es6: true,
node: true,
mocha: true
mocha: true,
},
globals: {
BigInt: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 10,
project: "./tsconfig.json"
project: "./tsconfig.json",
},
plugins: [
"@typescript-eslint",
"eslint-plugin-import",
"prettier"
],
plugins: ["@typescript-eslint", "eslint-plugin-import", "prettier"],
extends: [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
],
settings: {},
rules: {
"prettier/prettier": "error",
//doesnt work, it reports false errors
"constructor-super": "off",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {
"allowExpressions": true
}],
"@typescript-eslint/explicit-function-return-type": ["error", {allowExpressions: true}],
"@typescript-eslint/func-call-spacing": "error",
"@typescript-eslint/interface-name-prefix": ["error", "always"],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-unused-vars": ["error", {
"varsIgnorePattern": "^_"
}],
"@typescript-eslint/ban-ts-ignore": "warn",
"@typescript-eslint/no-unused-vars": ["error", {varsIgnorePattern: "^_"}],
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/semi": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"import/no-extraneous-dependencies": ["error", {
"devDependencies": false,
"optionalDependencies": false,
"peerDependencies": false
}],
"import/no-extraneous-dependencies": [
"error",
{devDependencies: false, optionalDependencies: false, peerDependencies: false},
],
"func-call-spacing": "off",
"max-len": ["error", {
"code": 120
}],
"max-len": [
"error",
{
code: 120,
},
],
//if --fix is run it messes imports like /lib/presets/minimal & /lib/presets/mainnet
"import/no-duplicates": "off",
"new-parens": "error",
Expand All @@ -69,16 +60,16 @@ module.exports = {
"object-literal-sort-keys": 0,
"no-prototype-builtins": 0,
"prefer-const": "error",
"quotes": ["error", "double"],
"semi": "off"
quotes: ["error", "double"],
semi: "off",
},
"overrides": [
overrides: [
{
"files": ["**/test/**/*.ts"],
"rules": {
files: ["**/test/**/*.ts"],
rules: {
"import/no-extraneous-dependencies": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}
]
"@typescript-eslint/no-explicit-any": "off",
},
},
],
};
2 changes: 2 additions & 0 deletions .mocharc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
colors: true
require: ts-node/register
38 changes: 19 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"scripts": {
"build": "yarn build-lib && yarn build-types",
"build-lib": "babel src -x .ts -d lib --source-maps",
"build-types": "tsc --incremental --declaration --outDir lib --emitDeclarationOnly",
"build-types": "tsc --outDir lib -p tsconfig.build.json",
"build-web": "webpack --mode production --entry ./lib/web.js --output ./dist/ssz.min.js",
"build:docs": "typedoc --exclude src/index.ts,src/web.ts --out docs src",
"build:release": "yarn clean && yarn run build && yarn build-web && yarn run build:docs",
Expand All @@ -35,28 +35,28 @@
"case": "^1.6.3"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/cli": "^7.12.8",
"@babel/core": "^7.12.9",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-bigint": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-typescript": "^7.8.3",
"@babel/register": "^7.8.3",
"@types/chai": "^4.2.9",
"@types/mocha": "^7.0.1",
"@types/node": "^13.7.4",
"@typescript-eslint/eslint-plugin": "^2.20.0",
"@typescript-eslint/parser": "^2.20.0",
"@babel/preset-env": "^7.12.7",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.1",
"@types/chai": "^4.2.0",
"@types/mocha": "^8.0.3",
"@types/node": "^14.14.17",
"@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0",
"chai": "^4.2.0",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.1",
"eslint": "^7.14.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.1.4",
"mocha": "^7.0.1",
"nyc": "^15.0.0",
"mocha": "^8.3.0",
"nyc": "^15.1.0",
"prettier": "^2.0.5",
"ts-node": "^8.6.2",
"typescript": "^3.8.2"
"ts-node": "^9.1.1",
"typescript": "^4.2.0"
},
"keywords": [
"ethereum",
Expand Down
16 changes: 9 additions & 7 deletions src/backings/tree/treeValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {ITreeBacked, TreeBacked, ValueOf} from "./interface";
// There exists circular dependencies here that prevent easy separation
// In this file, there's proxy logic that wraps TreeValue and TreeValue and its subclasses

type TreeValueConstructor<T> = {
type TreeValueConstructor<T extends CompositeValue> = {
new (type: CompositeType<T>, tree: Tree): TreeValue<T>;
};

Expand Down Expand Up @@ -54,14 +54,16 @@ export function getTreeValueClass<T extends CompositeValue>(type: CompositeType<
}
} else if (isContainerType(type)) {
return (ContainerTreeValue as unknown) as TreeValueConstructor<T>;
} else {
throw Error("Unknown type class");
}
}

/**
* Wrap a TreeValue in a Proxy that adds ergonomic getter/setter
*/
export function proxyWrapTreeValue<T extends CompositeValue>(value: TreeValue<T>): TreeBacked<T> {
return (new Proxy(value, TreeProxyHandler as unknown) as unknown) as TreeBacked<T>;
return (new Proxy(value, (TreeProxyHandler as unknown) as ProxyHandler<TreeValue<T>>) as unknown) as TreeBacked<T>;
}

/**
Expand All @@ -84,7 +86,7 @@ export const TreeProxyHandler: ProxyHandler<TreeValue<CompositeValue>> = {
return target.getPropertyNames() as (string | symbol)[];
},

getOwnPropertyDescriptor(target: TreeValue<CompositeValue>, property: PropertyKey): PropertyDescriptor {
getOwnPropertyDescriptor(target: TreeValue<CompositeValue>, property: PropertyKey): PropertyDescriptor | undefined {
if (target.type.getPropertyType(property as keyof CompositeValue)) {
return {
configurable: true,
Expand Down Expand Up @@ -301,7 +303,7 @@ export class BasicListTreeValue<T extends List<unknown>> extends BasicArrayTreeV
}
}

export class CompositeListTreeValue<T extends List<object>> extends CompositeArrayTreeValue<T> {
export class CompositeListTreeValue<T extends List<Record<string, unknown>>> extends CompositeArrayTreeValue<T> {
type: CompositeListType<T>;

constructor(type: CompositeListType<T>, tree: Tree) {
Expand Down Expand Up @@ -331,8 +333,8 @@ export class ContainerTreeValue<T extends CompositeValue> extends TreeValue<T> {
}

getProperty<P extends keyof T>(property: P): ValueOf<T, P> {
if (!this.type.fields[property as string]) {
return undefined;
if (!this.type.fields[property]) {
return undefined as ValueOf<T, P>;
}
const propType = this.type.getPropertyType(property);
const propValue = this.type.tree_getProperty(this.tree, property);
Expand Down Expand Up @@ -396,7 +398,7 @@ export class ContainerTreeValue<T extends CompositeValue> extends TreeValue<T> {
let i = 0;
for (const value of this.type.tree_readonlyIterateValues(this.tree)) {
const propName = keys[i] as string;
const propType = this.type.getPropertyType(propName);
const propType = this.type.getPropertyType(propName as keyof T);
if (isCompositeType(propType)) {
yield [propName, createTreeBacked(propType, value as Tree) as ValueOf<T>];
} else {
Expand Down
8 changes: 3 additions & 5 deletions src/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/interface-name-prefix */

/**
* These interfaces are consistent across all backings.
* As long as these interfaces are respected, the backing can be abstracted entirely.
Expand All @@ -15,10 +13,10 @@ export type Vector<T> = ArrayLike<T>;

export interface List<T> extends ArrayLike<T> {
push(...values: T[]): number;
pop(): T;
pop(): T | undefined;
}

export type Container<T extends object> = T;
export type Container<T extends Record<string, unknown>> = T;

export type ByteVector = Vector<number>;

Expand All @@ -31,7 +29,7 @@ export interface ObjectLike {
[fieldName: string]: any;
}

export type CompositeValue = Record<string, unknown> | ArrayLike<unknown> | {};
export type CompositeValue = Record<string, unknown> | ArrayLike<unknown> | Record<string, never>;

/**
* The Json interface is used for json-serializable input
Expand Down
1 change: 0 additions & 1 deletion src/types/basic/abstract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
/* eslint-disable @typescript-eslint/member-ordering */
/* eslint-disable @typescript-eslint/no-unused-vars */
import {isTypeOf, Type} from "../type";
Expand Down
1 change: 0 additions & 1 deletion src/types/basic/boolean.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
import {Json} from "../../interface";
import {isTypeOf, Type} from "../type";
import {BasicType} from "./abstract";
Expand Down
10 changes: 5 additions & 5 deletions src/types/basic/uint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
import {Json} from "../../interface";
import {isTypeOf, Type} from "../type";
import {BasicType} from "./abstract";
Expand Down Expand Up @@ -35,20 +34,21 @@ export function isNumberUintType(type: Type<unknown>): type is NumberUintType {
}

export class NumberUintType extends UintType<number> {
_maxBigInt: BigInt;
_maxBigInt?: BigInt;
constructor(options: IUintOptions) {
super(options);
this._typeSymbols.add(NUMBER_UINT_TYPE);
}

struct_assertValidValue(value: unknown): asserts value is number {
if (
value !== Infinity &&
(!Number.isSafeInteger(value as number) || value > BigInt(2) ** (BigInt(8) * BigInt(this.byteLength)))
typeof value !== "number" ||
(value !== Infinity &&
(!Number.isSafeInteger(value) || value > BigInt(2) ** (BigInt(8) * BigInt(this.byteLength))))
) {
throw new Error("Uint value is not a number");
}
if ((value as number) < 0) {
if (value < 0) {
throw new Error("Uint value must be gte 0");
}
}
Expand Down
57 changes: 48 additions & 9 deletions src/types/composite/abstract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
/* eslint-disable @typescript-eslint/member-ordering */
/* eslint-disable @typescript-eslint/no-unused-vars */
import {CompositeValue, Json} from "../../interface";
Expand Down Expand Up @@ -28,8 +27,8 @@ export function isCompositeType(type: Type<unknown>): type is CompositeType<Comp
*
*/
export abstract class CompositeType<T extends CompositeValue> extends Type<T> {
_chunkDepth: number;
_defaultNode: Node;
_chunkDepth?: number;
_defaultNode?: Node;

constructor() {
super();
Expand Down Expand Up @@ -101,8 +100,8 @@ export abstract class CompositeType<T extends CompositeValue> extends Type<T> {
throw new Error(`End param: ${end} is greater than length: ${data.length}`);
}
const length = end - start;
if (!this.hasVariableSerializedLength() && length !== this.struct_getSerializedLength(null)) {
throw new Error(`Incorrect data length ${length}, expect ${this.struct_getSerializedLength(null)}`);
if (!this.hasVariableSerializedLength() && length !== this.struct_getSerializedLength()) {
throw new Error(`Incorrect data length ${length}, expect ${this.struct_getSerializedLength()}`);
}
if (end - start < this.getMinSerializedLength()) {
throw new Error(`Data length ${length} is too small, expect at least ${this.getMinSerializedLength()}`);
Expand All @@ -121,17 +120,57 @@ export abstract class CompositeType<T extends CompositeValue> extends Type<T> {

abstract getMinSerializedLength(): number;
abstract getMaxSerializedLength(): number;
abstract struct_getSerializedLength(struct: T): number;
abstract tree_getSerializedLength(tree: Tree): number;

/**
* Get the serialized length in number of bytes
*
* For fixed-length types, the type alone is sufficient
* For variable-length types, the underlying data is required
*
* @param struct value - only needed if the type is variable-length
*/
abstract struct_getSerializedLength(struct?: T): number;

/**
* Get the serialized length in number of bytes
*
* For fixed-length types, the type alone is sufficient
* For variable-length types, the underlying data is required
*
* @param tree value - only needed if the type is variable-length
*/
abstract tree_getSerializedLength(tree?: Tree): number;

/**
* Is the type variable-length
*/
abstract hasVariableSerializedLength(): boolean;

abstract bytes_getVariableOffsets(target: Uint8Array): [number, number][];

abstract getMaxChunkCount(): number;
struct_getChunkCount(struct: T): number {

/**
* Get the number of merkle nodes at `type.getChunkDepth()`
*
* For fixed-length types, the type alone is sufficient
* For variable-length types, the underlying data is required
*
* @param struct - value - only needed if the type is variable-length
*/
struct_getChunkCount(struct?: T): number {
return this.getMaxChunkCount();
}

tree_getChunkCount(target: Tree): number {
/**
* Get the number of merkle nodes at `type.getChunkDepth()`
*
* For fixed-length types, the type alone is sufficient
* For variable-length types, the underlying data is required
*
* @param target - value - only needed if the type is variable-length
*/
tree_getChunkCount(target?: Tree): number {
return this.getMaxChunkCount();
}

Expand Down
Loading