Skip to content

Commit

Permalink
feat(codec): molecule friendly error message (ckb-js#403)
Browse files Browse the repository at this point in the history
* feat(codec): codec human-readable eror

* feat(codec): add  friend error for union and option

* feat(codec): add  friend error revert unexpected changes

* feat(codec): optimize code structure for codec pack error

* feat(codec): optimize code structure for codec pack error

* Update packages/codec/src/error.ts

Co-authored-by: Yonghui Lin <homura.dev@gmail.com>

* Update packages/codec/src/error.ts

Co-authored-by: Yonghui Lin <homura.dev@gmail.com>

* feat(codec): some document comment change

Co-authored-by: Yonghui Lin <homura.dev@gmail.com>
  • Loading branch information
IronLu233 and homura authored Sep 14, 2022
1 parent 3b458f6 commit 08b7f8d
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 38 deletions.
1 change: 1 addition & 0 deletions packages/codec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
]
},
"devDependencies": {
"escape-string-regexp": "^4.0.0",
"js-yaml": "^4.1.0"
}
}
56 changes: 56 additions & 0 deletions packages/codec/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// lc for lumos codec
export const CODEC_OPTIONAL_PATH = "__lc_option__";
type CodecOptionalPath = typeof CODEC_OPTIONAL_PATH;
export class CodecBaseParseError extends Error {
constructor(message: string, public expectedType: string) {
super(message);
}
}

const CODEC_EXECUTE_ERROR_NAME = "CodecExecuteError";
export function isCodecExecuteError(
error: unknown
): error is CodecExecuteError {
if (!(error instanceof Error)) return false;
return error.name === CODEC_EXECUTE_ERROR_NAME;
}

/**
* This Error class can collect CodecBaseParseError, and put an human-readable error
*/
export class CodecExecuteError extends Error {
name = CODEC_EXECUTE_ERROR_NAME;
constructor(private origin: CodecBaseParseError) {
super();
}

keys: (number | string | CodecOptionalPath)[] = [];

public updateKey(key: number | string | symbol): void {
this.keys.push(key as number | string);
this.message = this.getPackErrorMessage();
}

private getPackErrorMessage(): string {
type CodecPath = number | string | CodecOptionalPath;

const reducer = (acc: string, cur: CodecPath, index: number) => {
if (cur === CODEC_OPTIONAL_PATH) {
cur = index === 0 ? "?" : "?.";
} else if (typeof cur === "number") {
cur = `[${cur}]`;
} else {
cur = `.${cur}`;
}
return acc + cur;
};

const path = this.keys.reduceRight(reducer, "input");

return `Expect type ${this.origin.expectedType} at ${path} but got error: ${
this.origin.message
}
${this.origin.stack?.replace(/Error:.+?\n/, "")}
`;
}
}
19 changes: 15 additions & 4 deletions packages/codec/src/high-order/nested.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
UnpackParam,
UnpackResult,
} from "../base";
import { trackCodeExecuteError } from "../utils";
import { CODEC_OPTIONAL_PATH } from "../error";

export interface NullableCodec<C extends AnyCodec = AnyCodec> extends AnyCodec {
pack(packable?: PackParam<C>): PackResult<C>;
Expand All @@ -19,7 +21,9 @@ export function createNullableCodec<C extends AnyCodec = AnyCodec>(
return {
pack: (packable) => {
if (packable == null) return packable;
return codec.pack(packable);
return trackCodeExecuteError(CODEC_OPTIONAL_PATH, () =>
codec.pack(packable)
);
},
unpack: (unpackable) => {
if (unpackable == null) return unpackable;
Expand All @@ -38,7 +42,7 @@ export type ObjectCodec<Shape extends ObjectCodecShape = ObjectCodecShape> =
>;

/**
* a high-order codec that helps to organise multiple codecs together into a single object
* a high-order codec that helps to organize multiple codecs together into a single object
* @param codecShape
* @example
* ```ts
Expand All @@ -62,7 +66,11 @@ export function createObjectCodec<Shape extends ObjectCodecShape>(
const result = {} as { [key in keyof Shape]: PackResult<Shape[key]> };

codecEntries.forEach(([key, itemCodec]) => {
Object.assign(result, { [key]: itemCodec.pack(packableObj[key]) });
Object.assign(result, {
[key]: trackCodeExecuteError(key, () =>
itemCodec.pack(packableObj[key])
),
});
});

return result;
Expand All @@ -88,7 +96,10 @@ export type ArrayCodec<C extends AnyCodec> = Codec<

export function createArrayCodec<C extends AnyCodec>(codec: C): ArrayCodec<C> {
return {
pack: (items) => items.map((item) => codec.pack(item)),
pack: (items) =>
items.map((item, index) =>
trackCodeExecuteError(index, () => codec.pack(item))
),
unpack: (items) => items.map((item) => codec.unpack(item)),
};
}
Expand Down
112 changes: 86 additions & 26 deletions packages/codec/src/molecule/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,24 @@
* | union | item-type-id | item |
*/

import type {
import {
BytesCodec,
Fixed,
FixedBytesCodec,
PackParam,
UnpackResult,
createBytesCodec,
createFixedBytesCodec,
isFixedCodec,
} from "../base";
import { createBytesCodec, createFixedBytesCodec, isFixedCodec } from "../base";
import { Uint32LE } from "../number";
import { concat } from "../bytes";
import { CodecBaseParseError } from "../error";
import {
createObjectCodec,
createArrayCodec,
createNullableCodec,
} from "../high-order";

type NullableKeys<O extends Record<string, unknown>> = {
[K in keyof O]-?: [O[K] & (undefined | null)] extends [never] ? never : K;
Expand Down Expand Up @@ -53,14 +61,21 @@ export type UnionCodec<T extends Record<string, BytesCodec>> = BytesCodec<
{ [key in keyof T]: { type: key; value: PackParam<T[key]> } }[keyof T]
>;

/**
* The array is a fixed-size type: it has a fixed-size inner type and a fixed length.
* The size of an array is the size of inner type times the length.
* @param itemCodec the fixed-size array item codec
* @param itemCount
*/
export function array<T extends FixedBytesCodec>(
itemCodec: T,
itemCount: number
): ArrayCodec<T> & Fixed {
const enhancedArrayCodec = createArrayCodec(itemCodec);
return createFixedBytesCodec({
byteLength: itemCodec.byteLength * itemCount,
pack(items) {
const itemsBuf = items.map((item) => itemCodec.pack(item));
const itemsBuf = enhancedArrayCodec.pack(items);
return concat(...itemsBuf);
},
unpack(buf) {
Expand Down Expand Up @@ -93,22 +108,26 @@ function checkShape<T>(shape: T, fields: (keyof T)[]) {
}
}

/**
* Struct is a fixed-size type: all fields in struct are fixed-size and it has a fixed quantity of fields.
* The size of a struct is the sum of all fields' size.
* @param shape a object contains all fields' codec
* @param fields the shape's keys. It provide an order for serialization/deserialization.
*/
export function struct<T extends Record<string, FixedBytesCodec>>(
shape: T,
fields: (keyof T)[]
): ObjectCodec<T> & Fixed {
checkShape(shape, fields);

const objectCodec = createObjectCodec(shape);
return createFixedBytesCodec({
byteLength: fields.reduce((sum, field) => sum + shape[field].byteLength, 0),
pack(obj) {
const packed = objectCodec.pack(
obj as { [K in keyof T]: PackParam<T[K]> }
);
return fields.reduce((result, field) => {
const itemCodec = shape[field];

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const item = obj[field];
return concat(result, itemCodec.pack(item));
return concat(result, packed[field]);
}, Uint8Array.from([]));
},
unpack(buf) {
Expand All @@ -130,15 +149,19 @@ export function struct<T extends Record<string, FixedBytesCodec>>(
});
}

/**
* Vector with fixed size item codec
* @param itemCodec fixed-size vector item codec
*/
export function fixvec<T extends FixedBytesCodec>(itemCodec: T): ArrayCodec<T> {
return createBytesCodec({
pack(items) {
const arrayCodec = createArrayCodec(itemCodec);
return concat(
Uint32LE.pack(items.length),
items.reduce(
(buf, item) => concat(buf, itemCodec.pack(item)),
new ArrayBuffer(0)
)
arrayCodec
.pack(items)
.reduce((buf, item) => concat(buf, item), new ArrayBuffer(0))
);
},
unpack(buf) {
Expand All @@ -153,17 +176,22 @@ export function fixvec<T extends FixedBytesCodec>(itemCodec: T): ArrayCodec<T> {
});
}

/**
* Vector with dynamic size item codec
* @param itemCodec the vector item codec. It can be fixed-size or dynamic-size.
* For example, you can create a recursive vector with this.
*/
export function dynvec<T extends BytesCodec>(itemCodec: T): ArrayCodec<T> {
return createBytesCodec({
pack(obj) {
const packed = obj.reduce(
const arrayCodec = createArrayCodec(itemCodec);
const packed = arrayCodec.pack(obj).reduce(
(result, item) => {
const packedItem = itemCodec.pack(item);
const packedHeader = Uint32LE.pack(result.offset);
return {
header: concat(result.header, packedHeader),
body: concat(result.body, packedItem),
offset: result.offset + packedItem.byteLength,
body: concat(result.body, item),
offset: result.offset + item.byteLength,
};
},
{
Expand Down Expand Up @@ -209,13 +237,22 @@ export function dynvec<T extends BytesCodec>(itemCodec: T): ArrayCodec<T> {
});
}

/**
* General vector codec, if `itemCodec` is fixed size type, it will create a fixvec codec, otherwise a dynvec codec will be created.
* @param itemCodec
*/
export function vector<T extends BytesCodec>(itemCodec: T): ArrayCodec<T> {
if (isFixedCodec(itemCodec)) {
return fixvec(itemCodec);
}
return dynvec(itemCodec);
}

/**
* Table is a dynamic-size type. It can be considered as a dynvec but the length is fixed.
* @param shape The table shape, item codec can be dynamic size
* @param fields the shape's keys. Also provide an order for pack/unpack.
*/
export function table<T extends Record<string, BytesCodec>>(
shape: T,
fields: (keyof T)[]
Expand All @@ -224,13 +261,13 @@ export function table<T extends Record<string, BytesCodec>>(
return createBytesCodec({
pack(obj) {
const headerLength = 4 + fields.length * 4;
const objectCodec = createObjectCodec(shape);
const packedObj = objectCodec.pack(
obj as { [K in keyof T]: PackParam<T[K]> }
);
const packed = fields.reduce(
(result, field) => {
const itemCodec = shape[field];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const item = obj[field];
const packedItem = itemCodec.pack(item);
const packedItem = packedObj[field];
const packedOffset = Uint32LE.pack(result.offset);
return {
header: concat(result.header, packedOffset),
Expand Down Expand Up @@ -282,22 +319,37 @@ export function table<T extends Record<string, BytesCodec>>(
});
}

/**
* Union is a dynamic-size type.
* Serializing a union has two steps:
* - Serialize a item type id in bytes as a 32 bit unsigned integer in little-endian. The item type id is the index of the inner items, and it's starting at 0.
* - Serialize the inner item.
* @param itemCodec the union item record
* @param fields the list of itemCodec's keys. It's also provide an order for pack/unpack.
*/
export function union<T extends Record<string, BytesCodec>>(
itemCodec: T,
fields: (keyof T)[]
): UnionCodec<T> {
return createBytesCodec({
pack(obj) {
const type = obj.type;
const typeName = `Union(${fields.join(" | ")})`;

/* c8 ignore next */
if (typeof type !== "string") {
throw new Error(`Invalid type in union, type must be a string`);
throw new CodecBaseParseError(
`Invalid type in union, type must be a string`,
typeName
);
}

const fieldIndex = fields.indexOf(type);
if (fieldIndex === -1) {
throw new Error(`Unknown union type: ${String(obj.type)}`);
throw new CodecBaseParseError(
`Unknown union type: ${String(obj.type)}`,
typeName
);
}
const packedFieldIndex = Uint32LE.pack(fieldIndex);
const packedBody = itemCodec[type].pack(obj.value);
Expand All @@ -311,11 +363,19 @@ export function union<T extends Record<string, BytesCodec>>(
});
}

/**
* Option is a dynamic-size type.
* Serializing an option depends on whether it is empty or not:
* - if it's empty, there is zero bytes (the size is 0).
* - if it's not empty, just serialize the inner item (the size is same as the inner item's size).
* @param itemCodec
*/
export function option<T extends BytesCodec>(itemCodec: T): OptionCodec<T> {
return createBytesCodec({
pack(obj?) {
const nullableCodec = createNullableCodec(itemCodec);
if (obj !== undefined && obj !== null) {
return itemCodec.pack(obj);
return nullableCodec.pack(obj);
} else {
return Uint8Array.from([]);
}
Expand Down
Loading

0 comments on commit 08b7f8d

Please sign in to comment.