Skip to content

Commit 13bd7c4

Browse files
committed
test(jsonrpc): add tests for positional decoding of parameters
Tests covers all structs declared in `params.go`, comparing positional decoding against equivalent named decoding.
1 parent a0d7e0a commit 13bd7c4

4 files changed

Lines changed: 208 additions & 73 deletions

File tree

internal/jsonrpc/api/params.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bytes"
88
"encoding/json"
99
"fmt"
10+
"reflect"
1011
)
1112

1213
type StringOrList []string
@@ -221,3 +222,49 @@ type GetWithdrawalParams struct {
221222
Application string `json:"application"`
222223
AccountIndex string `json:"account_index"`
223224
}
225+
226+
// UnmarshalParams supports both by-name (object) and by-position (array) parameter structures.
227+
// If params is an object, it simply does json.Unmarshal; if it's an array, it will attempt
228+
// to unmarshal each positional parameter into the target struct field in declaration order.
229+
func UnmarshalParams(data json.RawMessage, target any) error {
230+
data = bytes.TrimSpace(data)
231+
if len(data) > 0 && data[0] == '[' {
232+
// Unmarshal positional parameters into a slice of json.RawMessage.
233+
var rawParams []json.RawMessage
234+
if err := json.Unmarshal(data, &rawParams); err != nil {
235+
return err
236+
}
237+
// Use reflection to set values in the target struct in the order they appear.
238+
val := reflect.ValueOf(target)
239+
if val.Kind() != reflect.Pointer || val.IsNil() {
240+
return fmt.Errorf("error unmarshalling positional parameters target must be a non-nil pointer to a struct")
241+
}
242+
val = val.Elem()
243+
if val.Kind() != reflect.Struct {
244+
return fmt.Errorf("error unmarshalling positional parameters target must point to a struct")
245+
}
246+
typ := val.Type()
247+
if len(rawParams) > typ.NumField() {
248+
return fmt.Errorf("error unmarshalling positional parameters, expected %d params, got %d",
249+
typ.NumField(), len(rawParams))
250+
}
251+
// For each field in the struct, if a positional parameter exists, unmarshal that parameter.
252+
for i := 0; i < typ.NumField() && i < len(rawParams); i++ {
253+
sf := typ.Field(i)
254+
if sf.Tag.Get("json") == "-" {
255+
continue
256+
}
257+
field := val.Field(i)
258+
if !field.CanSet() {
259+
return fmt.Errorf("error unmarshalling positional parameter field %q is not settable", typ.Field(i).Name)
260+
}
261+
// Unmarshal the corresponding raw parameter into the field.
262+
if err := json.Unmarshal(rawParams[i], field.Addr().Interface()); err != nil {
263+
return fmt.Errorf("error unmarshalling positional parameter %d for field %s: %w", i, typ.Field(i).Name, err)
264+
}
265+
}
266+
return nil
267+
}
268+
// Otherwise, assume by-name structure.
269+
return json.Unmarshal(data, target)
270+
}

internal/jsonrpc/api/params_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,139 @@ func TestListOutputsParamsExecutedIsOptional(t *testing.T) {
8383
require.NotNil(t, pending.Executed)
8484
require.False(t, *pending.Executed)
8585
}
86+
87+
func TestPositionalParamsDeclarationOrder(t *testing.T) {
88+
tests := map[string]struct {
89+
newTarget func() any
90+
positional string
91+
named string
92+
}{
93+
"ListApplicationsParams": {
94+
func() any { return &ListApplicationsParams{} },
95+
`[25,3,true]`,
96+
`{"limit":25,"offset":3,"descending":true}`,
97+
},
98+
"GetApplicationParams": {
99+
func() any { return &GetApplicationParams{} },
100+
`["app"]`,
101+
`{"application":"app"}`,
102+
},
103+
"ListEpochsParams": {
104+
func() any { return &ListEpochsParams{} },
105+
`["app",["OPEN","CLOSED"],25,3,true,"0x2","0x9"]`,
106+
`{"application":"app","status":["OPEN","CLOSED"],"limit":25,"offset":3,"descending":true,"from":"0x2","to":"0x9"}`,
107+
},
108+
"GetEpochParams": {
109+
func() any { return &GetEpochParams{} },
110+
`["app","0x4"]`,
111+
`{"application":"app","epoch_index":"0x4"}`,
112+
},
113+
"GetEpochByVirtualIndexParams": {
114+
func() any { return &GetEpochByVirtualIndexParams{} },
115+
`["app","0x7"]`,
116+
`{"application":"app","virtual_index":"0x7"}`,
117+
},
118+
"GetLastAcceptedEpochIndexParams": {
119+
func() any { return &GetLastAcceptedEpochIndexParams{} },
120+
`["app"]`,
121+
`{"application":"app"}`,
122+
},
123+
"ListInputsParams": {
124+
func() any { return &ListInputsParams{} },
125+
`["app","0x4","sender","transaction-hash",25,3,true,"0x2","0x9"]`,
126+
`{"application":"app","epoch_index":"0x4","sender":"sender","transaction_hash":"transaction-hash","limit":25,"offset":3,"descending":true,"from":"0x2","to":"0x9"}`,
127+
},
128+
"GetInputParams": {
129+
func() any { return &GetInputParams{} },
130+
`["app","0x5"]`,
131+
`{"application":"app","input_index":"0x5"}`,
132+
},
133+
"GetProcessedInputCountParams": {
134+
func() any { return &GetProcessedInputCountParams{} },
135+
`["app"]`,
136+
`{"application":"app"}`,
137+
},
138+
"ListOutputsParams": {
139+
func() any { return &ListOutputsParams{} },
140+
`["app","0x4","0x5",["0x237a816f","0x10321e8b"],"voucher",25,3,true,"0x2","0x9",true]`,
141+
`{"application":"app","epoch_index":"0x4","input_index":"0x5","output_type":["0x237a816f","0x10321e8b"],"voucher_address":"voucher","limit":25,"offset":3,"descending":true,"from":"0x2","to":"0x9","executed":true}`,
142+
},
143+
"GetOutputParams": {
144+
func() any { return &GetOutputParams{} },
145+
`["app","0x6"]`,
146+
`{"application":"app","output_index":"0x6"}`,
147+
},
148+
"ListReportsParams": {
149+
func() any { return &ListReportsParams{} },
150+
`["app","0x4","0x5",25,3,true,"0x2","0x9"]`,
151+
`{"application":"app","epoch_index":"0x4","input_index":"0x5","limit":25,"offset":3,"descending":true,"from":"0x2","to":"0x9"}`,
152+
},
153+
"GetReportParams": {
154+
func() any { return &GetReportParams{} },
155+
`["app","0x7"]`,
156+
`{"application":"app","report_index":"0x7"}`,
157+
},
158+
"ListTournamentsParams": {
159+
func() any { return &ListTournamentsParams{} },
160+
`["app","0x4","0x2","parent-tournament","parent-match",25,3,true]`,
161+
`{"application":"app","epoch_index":"0x4","level":"0x2","parent_tournament_address":"parent-tournament","parent_match_id_hash":"parent-match","limit":25,"offset":3,"descending":true}`,
162+
},
163+
"GetTournamentParams": {
164+
func() any { return &GetTournamentParams{} },
165+
`["app","tournament"]`,
166+
`{"application":"app","address":"tournament"}`,
167+
},
168+
"ListCommitmentsParams": {
169+
func() any { return &ListCommitmentsParams{} },
170+
`["app","0x4","tournament",25,3,true]`,
171+
`{"application":"app","epoch_index":"0x4","tournament_address":"tournament","limit":25,"offset":3,"descending":true}`,
172+
},
173+
"GetCommitmentParams": {
174+
func() any { return &GetCommitmentParams{} },
175+
`["app","0x4","tournament","commitment"]`,
176+
`{"application":"app","epoch_index":"0x4","tournament_address":"tournament","commitment":"commitment"}`,
177+
},
178+
"ListMatchesParams": {
179+
func() any { return &ListMatchesParams{} },
180+
`["app","0x4","tournament",25,3,true]`,
181+
`{"application":"app","epoch_index":"0x4","tournament_address":"tournament","limit":25,"offset":3,"descending":true}`,
182+
},
183+
"GetMatchParams": {
184+
func() any { return &GetMatchParams{} },
185+
`["app","0x4","tournament","id-hash"]`,
186+
`{"application":"app","epoch_index":"0x4","tournament_address":"tournament","id_hash":"id-hash"}`,
187+
},
188+
"ListMatchAdvancesParams": {
189+
func() any { return &ListMatchAdvancesParams{} },
190+
`["app","0x4","tournament","id-hash",25,3,true]`,
191+
`{"application":"app","epoch_index":"0x4","tournament_address":"tournament","id_hash":"id-hash","limit":25,"offset":3,"descending":true}`,
192+
},
193+
"GetMatchAdvanceParams": {
194+
func() any { return &GetMatchAdvanceParams{} },
195+
`["app","0x4","tournament","id-hash","parent"]`,
196+
`{"application":"app","epoch_index":"0x4","tournament_address":"tournament","id_hash":"id-hash","parent":"parent"}`,
197+
},
198+
"ListWithdrawalsParams": {
199+
func() any { return &ListWithdrawalsParams{} },
200+
`["app","0x8",25,3,true]`,
201+
`{"application":"app","account_index":"0x8","limit":25,"offset":3,"descending":true}`,
202+
},
203+
"GetWithdrawalParams": {
204+
func() any { return &GetWithdrawalParams{} },
205+
`["app","0x8"]`,
206+
`{"application":"app","account_index":"0x8"}`,
207+
},
208+
}
209+
210+
for name, test := range tests {
211+
t.Run(name, func(t *testing.T) {
212+
expected := test.newTarget()
213+
require.NoError(t, json.Unmarshal([]byte(test.named), expected))
214+
215+
actual := test.newTarget()
216+
require.NoError(t, UnmarshalParams(json.RawMessage(test.positional), actual))
217+
218+
require.Equal(t, expected, actual)
219+
})
220+
}
221+
}

0 commit comments

Comments
 (0)