Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit 500c831

Browse files
authored
Merge pull request #5376 from trufflesuite/stringy-code
Enhancement: Add `allowJson` and `strictBooleans` options to encoder
2 parents 945912c + 429331b commit 500c831

File tree

9 files changed

+404
-49
lines changed

9 files changed

+404
-49
lines changed

packages/codec/lib/wrap/bool.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const boolCasesBasic: Case<
1414
Format.Values.BoolValue,
1515
never
1616
>[] = [
17+
boolFromBoolean, //needed due to strictBooleans mode
1718
boolFromString,
1819
boolFromBoxedPrimitive,
1920
boolFromCodecBoolValue,
@@ -27,10 +28,30 @@ export const boolCases: Case<
2728
Format.Types.BoolType,
2829
Format.Values.BoolValue,
2930
never
30-
>[] = [
31-
boolFromTypeValueInput,
32-
...boolCasesBasic
33-
];
31+
>[] = [boolFromTypeValueInput, ...boolCasesBasic];
32+
33+
function* boolFromBoolean(
34+
dataType: Format.Types.BoolType,
35+
input: unknown,
36+
wrapOptions: WrapOptions
37+
): Generator<never, Format.Values.BoolValue, WrapResponse> {
38+
if (typeof input !== "boolean") {
39+
throw new TypeMismatchError(
40+
dataType,
41+
input,
42+
wrapOptions.name,
43+
1,
44+
"Input was not a boolean"
45+
);
46+
}
47+
return {
48+
type: dataType,
49+
kind: "value" as const,
50+
value: {
51+
asBoolean: input
52+
}
53+
};
54+
}
3455

3556
function* boolFromString(
3657
dataType: Format.Types.BoolType,
@@ -46,8 +67,21 @@ function* boolFromString(
4667
"Input was not a string"
4768
);
4869
}
49-
//strings are true unless they're falsy or the case-insensitive string "false"
50-
const asBoolean = Boolean(input) && input.toLowerCase() !== "false";
70+
const lowerCasedInput = input.toLowerCase();
71+
if (
72+
wrapOptions.strictBooleans &&
73+
!["true", "false", "1", "0"].includes(lowerCasedInput)
74+
) {
75+
throw new TypeMismatchError(
76+
dataType,
77+
input,
78+
wrapOptions.name,
79+
5,
80+
"Input was not 'true', 'false', '1', or '0'"
81+
);
82+
}
83+
//strings are true unless they're falsy or the case-insensitive strings "false" or "0"
84+
const asBoolean = Boolean(input) && !["false", "0"].includes(lowerCasedInput);
5185
return {
5286
type: dataType,
5387
kind: "value" as const,
@@ -72,7 +106,12 @@ function* boolFromBoxedPrimitive(
72106
);
73107
}
74108
//unbox and try again
75-
return yield* wrapWithCases(dataType, input.valueOf(), wrapOptions, boolCases);
109+
return yield* wrapWithCases(
110+
dataType,
111+
input.valueOf(),
112+
wrapOptions,
113+
boolCases
114+
);
76115
}
77116

78117
function* boolFromCodecBoolValue(
@@ -215,9 +254,7 @@ function* boolFromCodecUdvtValue(
215254
"Input was not a wrapped result"
216255
);
217256
}
218-
if (
219-
input.type.typeClass !== "userDefinedValueType"
220-
) {
257+
if (input.type.typeClass !== "userDefinedValueType") {
221258
throw new TypeMismatchError(
222259
dataType,
223260
input,
@@ -252,9 +289,7 @@ function* boolFromCodecUdvtError(
252289
"Input was not a wrapped result"
253290
);
254291
}
255-
if (
256-
input.type.typeClass !== "userDefinedValueType"
257-
) {
292+
if (input.type.typeClass !== "userDefinedValueType") {
258293
throw new TypeMismatchError(
259294
dataType,
260295
input,
@@ -282,7 +317,11 @@ function* boolFromCodecUdvtError(
282317
Messages.errorResultMessage
283318
);
284319
}
285-
return yield* boolFromCodecBoolError(dataType, input.error.error, wrapOptions);
320+
return yield* boolFromCodecBoolError(
321+
dataType,
322+
input.error.error,
323+
wrapOptions
324+
);
286325
}
287326

288327
function* boolFromOther(
@@ -315,6 +354,16 @@ function* boolFromOther(
315354
"Input was a type/value pair"
316355
);
317356
}
357+
//...and also we don't do this case if strictBooleans is turned on
358+
if (wrapOptions.strictBooleans) {
359+
throw new TypeMismatchError(
360+
dataType,
361+
input,
362+
wrapOptions.name,
363+
2,
364+
"Input was neither a boolean nor a boolean string"
365+
);
366+
}
318367
const asBoolean = Boolean(input);
319368
return {
320369
type: dataType,

packages/codec/lib/wrap/index.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function wrappingToResolution(
8484
function* wrapForMethodRaw(
8585
method: Method,
8686
inputs: unknown[],
87-
{ userDefinedTypes, allowOptions }: ResolveOptions,
87+
{ userDefinedTypes, allowOptions, allowJson, strictBooleans }: ResolveOptions,
8888
loose: boolean = false
8989
): Generator<WrapRequest, Format.Values.Value[], WrapResponse> {
9090
debug("wrapping for method");
@@ -95,7 +95,9 @@ function* wrapForMethodRaw(
9595
userDefinedTypes,
9696
oldOptionsBehavior: true, //HACK
9797
loose,
98-
name: "<arguments>"
98+
name: "<arguments>",
99+
allowJson,
100+
strictBooleans
99101
});
100102
} else if (allowOptions && method.inputs.length === inputs.length - 1) {
101103
//options case
@@ -108,7 +110,9 @@ function* wrapForMethodRaw(
108110
userDefinedTypes,
109111
oldOptionsBehavior: true, //HACK
110112
loose,
111-
name: "<arguments>"
113+
name: "<arguments>",
114+
allowJson,
115+
strictBooleans
112116
});
113117
} else {
114118
//invalid length case
@@ -128,7 +132,7 @@ function* wrapForMethodRaw(
128132
export function* resolveAndWrap(
129133
methods: Method[],
130134
inputs: unknown[],
131-
{ userDefinedTypes, allowOptions }: ResolveOptions
135+
{ userDefinedTypes, allowOptions, allowJson, strictBooleans }: ResolveOptions
132136
): Generator<WrapRequest, Resolution, WrapResponse> {
133137
//despite us having a good system for overload resolution, we want to
134138
//use it as little as possible! That's because using it means we don't
@@ -140,7 +144,9 @@ export function* resolveAndWrap(
140144
//this is important for good error messages in this case
141145
return yield* wrapForMethod(methods[0], inputs, {
142146
userDefinedTypes,
143-
allowOptions
147+
allowOptions,
148+
allowJson,
149+
strictBooleans
144150
});
145151
}
146152
//OK, so, there are multiple possibilities then. let's try to filter things down by length.
@@ -162,7 +168,9 @@ export function* resolveAndWrap(
162168
name: "<options>",
163169
loose: true,
164170
oldOptionsBehavior: true, //HACK
165-
userDefinedTypes
171+
userDefinedTypes,
172+
allowJson,
173+
strictBooleans
166174
})
167175
);
168176
possibleOptions = wrappedOptions.value;
@@ -192,7 +200,9 @@ export function* resolveAndWrap(
192200
arguments: yield* wrapMultiple(method.inputs, inputs, {
193201
userDefinedTypes,
194202
loose: true,
195-
name: "<arguments>"
203+
name: "<arguments>",
204+
allowJson,
205+
strictBooleans
196206
}),
197207
options: {}
198208
};
@@ -209,7 +219,9 @@ export function* resolveAndWrap(
209219
arguments: yield* wrapMultiple(method.inputs, inputs, {
210220
userDefinedTypes,
211221
loose: true,
212-
name: "<arguments>"
222+
name: "<arguments>",
223+
allowJson,
224+
strictBooleans
213225
}),
214226
options: possibleOptions
215227
};
@@ -233,7 +245,9 @@ export function* resolveAndWrap(
233245
//although yes this means options will be re-wrapped, oh well
234246
wrapped = yield* wrapForMethodRaw(method, inputs, {
235247
userDefinedTypes,
236-
allowOptions
248+
allowOptions,
249+
allowJson,
250+
strictBooleans
237251
});
238252
} catch (error) {
239253
//if there's an error, don't add it

packages/codec/lib/wrap/types.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type * as Format from "@truffle/codec/format";
22
import type * as Abi from "@truffle/abi-utils";
33
import type * as Common from "@truffle/codec/common";
4-
import type { WrapRequest, WrapResponse } from "../types";
4+
import type { WrapResponse } from "../types";
55

66
/**
77
* @Category Interfaces
@@ -43,16 +43,22 @@ export interface WrapOptions {
4343
loose?: boolean;
4444
oldOptionsBehavior?: boolean; //to not break Truffle Contract
4545
specificityFloor?: number; //raise all specificities to at least this much... should *not* propagate!
46+
allowJson?: boolean;
47+
strictBooleans?: boolean;
4648
}
4749

4850
export interface ResolveOptions {
4951
userDefinedTypes?: Format.Types.TypesById;
5052
allowOptions?: boolean;
53+
allowJson?: boolean;
54+
strictBooleans?: boolean;
5155
}
5256

53-
export type Case<TypeType, ValueType, RequestType> =
54-
(dataType: TypeType, input: unknown, options: WrapOptions)
55-
=> Generator<RequestType, ValueType, WrapResponse>;
57+
export type Case<TypeType, ValueType, RequestType> = (
58+
dataType: TypeType,
59+
input: unknown,
60+
options: WrapOptions
61+
) => Generator<RequestType, ValueType, WrapResponse>;
5662

5763
export interface ContractInput {
5864
address: string;

packages/codec/lib/wrap/wrap.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ const arrayCasesBasic: Case<
2727
Format.Types.ArrayType,
2828
Format.Values.ArrayValue,
2929
WrapRequest
30-
>[] = [arrayFromArray, arrayFromCodecArrayValue, arrayFailureCase];
30+
>[] = [
31+
arrayFromArray,
32+
arrayFromCodecArrayValue,
33+
arrayFromJson,
34+
arrayFailureCase
35+
];
3136

3237
export const arrayCases: Case<
3338
Format.Types.ArrayType,
@@ -39,6 +44,7 @@ const tupleCasesBasic: Case<TupleLikeType, TupleLikeValue, WrapRequest>[] = [
3944
tupleFromArray,
4045
tupleFromCodecTupleLikeValue,
4146
tupleFromObject,
47+
tupleFromJson,
4248
tupleFailureCase
4349
];
4450

@@ -231,6 +237,44 @@ function* arrayFromCodecArrayValue(
231237
return yield* arrayFromArray(dataType, value, wrapOptions);
232238
}
233239

240+
function* arrayFromJson(
241+
dataType: Format.Types.ArrayType,
242+
input: unknown,
243+
wrapOptions: WrapOptions
244+
): Generator<WrapRequest, Format.Values.ArrayValue, WrapResponse> {
245+
if (!wrapOptions.allowJson) {
246+
throw new TypeMismatchError(
247+
dataType,
248+
input,
249+
wrapOptions.name,
250+
1,
251+
"JSON input must be explicitly enabled"
252+
);
253+
}
254+
if (typeof input !== "string") {
255+
throw new TypeMismatchError(
256+
dataType,
257+
input,
258+
wrapOptions.name,
259+
1,
260+
"Input was not a string"
261+
);
262+
}
263+
let parsedInput: unknown;
264+
try {
265+
parsedInput = JSON.parse(input);
266+
} catch (error) {
267+
throw new TypeMismatchError(
268+
dataType,
269+
input,
270+
wrapOptions.name,
271+
5,
272+
`Input was not valid JSON: ${error.message}`
273+
);
274+
}
275+
return yield* arrayFromArray(dataType, parsedInput, wrapOptions);
276+
}
277+
234278
function* arrayFromTypeValueInput(
235279
dataType: Format.Types.ArrayType,
236280
input: unknown,
@@ -432,6 +476,49 @@ function* tupleFromObject(
432476
};
433477
}
434478

479+
function* tupleFromJson(
480+
dataType: TupleLikeType,
481+
input: unknown,
482+
wrapOptions: WrapOptions
483+
): Generator<WrapRequest, TupleLikeValue, WrapResponse> {
484+
if (!wrapOptions.allowJson) {
485+
throw new TypeMismatchError(
486+
dataType,
487+
input,
488+
wrapOptions.name,
489+
1,
490+
"JSON input must be explicitly enabled"
491+
);
492+
}
493+
if (typeof input !== "string") {
494+
throw new TypeMismatchError(
495+
dataType,
496+
input,
497+
wrapOptions.name,
498+
1,
499+
"Input was not a string"
500+
);
501+
}
502+
let parsedInput: unknown;
503+
try {
504+
parsedInput = JSON.parse(input);
505+
} catch (error) {
506+
throw new TypeMismatchError(
507+
dataType,
508+
input,
509+
wrapOptions.name,
510+
5,
511+
`Input was not valid JSON: ${error.message}`
512+
);
513+
}
514+
debug("input is JSON");
515+
debug("parses to: %O", parsedInput);
516+
return yield* wrapWithCases(dataType, parsedInput, wrapOptions, [
517+
tupleFromObject,
518+
tupleFromArray
519+
]);
520+
}
521+
435522
function* tupleFromCodecTupleLikeValue(
436523
dataType: TupleLikeType,
437524
input: unknown,

0 commit comments

Comments
 (0)