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

feat: AgentJs Candid Pinpointed Type Errors upgrade - PR #633

Merged
merged 3 commits into from Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/generated/changelog.html
Expand Up @@ -16,6 +16,7 @@ <h2>Version 0.14.0</h2>
Adds retry logic to HttpAgent. By default, retries three times before throwing an error,
to offer a more cohesive workflow
</li>
<li>Improves and truncates error messages in Candid</li>
<li>fixes flaky tests for syncTime</li>
<li>
Adds a top-level <code>fetchCandid()</code> function which retrieves the Candid interface
Expand Down
141 changes: 104 additions & 37 deletions packages/candid/src/idl.ts
Expand Up @@ -45,6 +45,7 @@ const enum IDLTypeIds {
}

const magicNumber = 'DIDL';
const toReadableString_max = 400; // will not display arguments after 400chars. Makes sure 2mb blobs don't get inside the error

function zipWith<TX, TY, TR>(xs: TX[], ys: TY[], f: (a: TX, b: TY) => TR): TR[] {
return xs.map((x, i) => f(x, ys[i]));
Expand Down Expand Up @@ -260,7 +261,7 @@ export class EmptyClass extends PrimitiveType<never> {
}

public covariant(x: any): x is never {
return false;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(): never {
Expand Down Expand Up @@ -301,7 +302,7 @@ export class UnknownClass extends Type {
}

public covariant(x: any): x is any {
return false;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(): never {
Expand Down Expand Up @@ -363,7 +364,8 @@ export class BoolClass extends PrimitiveType<boolean> {
}

public covariant(x: any): x is boolean {
return typeof x === 'boolean';
if (typeof x === 'boolean') return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: boolean): ArrayBuffer {
Expand Down Expand Up @@ -400,7 +402,8 @@ export class NullClass extends PrimitiveType<null> {
}

public covariant(x: any): x is null {
return x === null;
if (x === null) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue() {
Expand Down Expand Up @@ -462,7 +465,8 @@ export class TextClass extends PrimitiveType<string> {
}

public covariant(x: any): x is string {
return typeof x === 'string';
if (typeof x === 'string') return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: string) {
Expand Down Expand Up @@ -503,7 +507,8 @@ export class IntClass extends PrimitiveType<bigint> {
public covariant(x: any): x is bigint {
// We allow encoding of JavaScript plain numbers.
// But we will always decode to bigint.
return typeof x === 'bigint' || Number.isInteger(x);
if (typeof x === 'bigint' || Number.isInteger(x)) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: bigint | number) {
Expand Down Expand Up @@ -539,7 +544,8 @@ export class NatClass extends PrimitiveType<bigint> {
public covariant(x: any): x is bigint {
// We allow encoding of JavaScript plain numbers.
// But we will always decode to bigint.
return (typeof x === 'bigint' && x >= BigInt(0)) || (Number.isInteger(x) && x >= 0);
if ((typeof x === 'bigint' && x >= BigInt(0)) || (Number.isInteger(x) && x >= 0)) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: bigint | number) {
Expand Down Expand Up @@ -579,7 +585,8 @@ export class FloatClass extends PrimitiveType<number> {
}

public covariant(x: any): x is number {
return typeof x === 'number' || x instanceof Number;
if (typeof x === 'number' || x instanceof Number) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: number) {
Expand Down Expand Up @@ -633,14 +640,18 @@ export class FixedIntClass extends PrimitiveType<bigint | number> {
public covariant(x: any): x is bigint {
const min = iexp2(this._bits - 1) * BigInt(-1);
const max = iexp2(this._bits - 1) - BigInt(1);
let ok = false;
if (typeof x === 'bigint') {
return x >= min && x <= max;
ok = x >= min && x <= max;
} else if (Number.isInteger(x)) {
const v = BigInt(x);
return v >= min && v <= max;
ok = v >= min && v <= max;
} else {
return false;
ok = false;
}

if (ok) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: bigint | number) {
Expand Down Expand Up @@ -685,14 +696,17 @@ export class FixedNatClass extends PrimitiveType<bigint | number> {

public covariant(x: any): x is bigint {
const max = iexp2(this._bits);
let ok = false;
if (typeof x === 'bigint' && x >= BigInt(0)) {
return x < max;
ok = x < max;
} else if (Number.isInteger(x) && x >= 0) {
const v = BigInt(x);
return v < max;
ok = v < max;
} else {
return false;
ok = false;
}
if (ok) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: bigint | number) {
Expand Down Expand Up @@ -759,10 +773,21 @@ export class VecClass<T> extends ConstructType<T[]> {
: this._type instanceof FixedIntClass
? this._type._bits
: 0;
return (

if (
(ArrayBuffer.isView(x) && bits == (x as any).BYTES_PER_ELEMENT * 8) ||
(Array.isArray(x) && x.every(v => this._type.covariant(v)))
);
(Array.isArray(x) &&
x.every((v, idx) => {
try {
return this._type.covariant(v);
} catch (e: any) {
throw new Error(`Invalid ${this.display()} argument: \n\nindex ${idx} -> ${e.message}`);
}
}))
)
return true;

throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: T[]) {
Expand Down Expand Up @@ -862,7 +887,15 @@ export class OptClass<T> extends ConstructType<[T] | []> {
}

public covariant(x: any): x is [T] | [] {
return Array.isArray(x) && (x.length === 0 || (x.length === 1 && this._type.covariant(x[0])));
try {
if (Array.isArray(x) && (x.length === 0 || (x.length === 1 && this._type.covariant(x[0]))))
return true;
} catch (e: any) {
throw new Error(
`Invalid ${this.display()} argument: ${toReadableString(x)} \n\n-> ${e.message}`,
);
}
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: [T] | []) {
Expand Down Expand Up @@ -942,16 +975,23 @@ export class RecordClass extends ConstructType<Record<string, any>> {
}

public covariant(x: any): x is Record<string, any> {
return (
if (
typeof x === 'object' &&
this._fields.every(([k, t]) => {
// eslint-disable-next-line
if (!x.hasOwnProperty(k)) {
throw new Error(`Record is missing key "${k}".`);
}
return t.covariant(x[k]);
try {
return t.covariant(x[k]);
} catch (e: any) {
throw new Error(`Invalid ${this.display()} argument: \n\nfield ${k} -> ${e.message}`);
}
})
);
)
return true;

throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: Record<string, any>) {
Expand Down Expand Up @@ -1062,11 +1102,21 @@ export class TupleClass<T extends any[]> extends RecordClass {

public covariant(x: any): x is T {
// `>=` because tuples can be covariant when encoded.
return (

if (
Array.isArray(x) &&
x.length >= this._fields.length &&
this._components.every((t, i) => t.covariant(x[i]))
);
this._components.every((t, i) => {
try {
return t.covariant(x[i]);
} catch (e: any) {
throw new Error(`Invalid ${this.display()} argument: \n\nindex ${i} -> ${e.message}`);
}
})
)
return true;

throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: any[]) {
Expand Down Expand Up @@ -1122,14 +1172,21 @@ export class VariantClass extends ConstructType<Record<string, any>> {
}

public covariant(x: any): x is Record<string, any> {
return (
if (
typeof x === 'object' &&
Object.entries(x).length === 1 &&
this._fields.every(([k, v]) => {
// eslint-disable-next-line
return !x.hasOwnProperty(k) || v.covariant(x[k]);
try {
// eslint-disable-next-line
return !x.hasOwnProperty(k) || v.covariant(x[k]);
} catch (e: any) {
throw new Error(`Invalid ${this.display()} argument: \n\nvariant ${k} -> ${e.message}`);
}
})
);
)
return true;

throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: Record<string, any>) {
Expand Down Expand Up @@ -1230,7 +1287,8 @@ export class RecClass<T = any> extends ConstructType<T> {
}

public covariant(x: any): x is T {
return this._type ? this._type.covariant(x) : false;
if (this._type ? this._type.covariant(x) : false) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: T) {
Expand Down Expand Up @@ -1294,7 +1352,8 @@ export class PrincipalClass extends PrimitiveType<PrincipalId> {
}

public covariant(x: any): x is PrincipalId {
return x && x._isPrincipal;
if (x && x._isPrincipal) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: PrincipalId): ArrayBuffer {
Expand Down Expand Up @@ -1342,9 +1401,9 @@ export class FuncClass extends ConstructType<[PrincipalId, string]> {
return v.visitFunc(this, d);
}
public covariant(x: any): x is [PrincipalId, string] {
return (
Array.isArray(x) && x.length === 2 && x[0] && x[0]._isPrincipal && typeof x[1] === 'string'
);
if (Array.isArray(x) && x.length === 2 && x[0] && x[0]._isPrincipal && typeof x[1] === 'string')
return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue([principal, methodName]: [PrincipalId, string]) {
Expand Down Expand Up @@ -1426,7 +1485,8 @@ export class ServiceClass extends ConstructType<PrincipalId> {
return v.visitService(this, d);
}
public covariant(x: any): x is PrincipalId {
return x && x._isPrincipal;
if (x && x._isPrincipal) return true;
throw new Error(`Invalid ${this.display()} argument: ${toReadableString(x)}`);
}

public encodeValue(x: PrincipalId) {
Expand Down Expand Up @@ -1467,9 +1527,13 @@ export class ServiceClass extends ConstructType<PrincipalId> {
* @returns {string}
*/
function toReadableString(x: unknown): string {
return JSON.stringify(x, (_key, value) =>
const str = JSON.stringify(x, (_key, value) =>
typeof value === 'bigint' ? `BigInt(${value})` : value,
);

return str && str.length > toReadableString_max
? str.substring(0, toReadableString_max - 3) + '...'
: str;
}

/**
Expand All @@ -1492,8 +1556,11 @@ export function encode(argTypes: Array<Type<any>>, args: any[]): ArrayBuffer {
const typs = concat(...argTypes.map(t => t.encodeType(typeTable)));
const vals = concat(
...zipWith(argTypes, args, (t, x) => {
if (!t.covariant(x)) {
throw new Error(`Invalid ${t.display()} argument: ${toReadableString(x)}`);
try {
t.covariant(x);
} catch (e: any) {
const err = new Error(e.message + '\n\n');
throw err;
}

return t.encodeValue(x);
Expand Down