Skip to content

Commit

Permalink
feat(SPV-793): add function that allows parsing query params into new…
Browse files Browse the repository at this point in the history
… search params struct.
  • Loading branch information
dorzepowski committed Aug 23, 2024
1 parent 8b853de commit 10f94a4
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 7 deletions.
37 changes: 37 additions & 0 deletions internal/query/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package query

import (
"time"

"github.com/bitcoin-sv/spv-wallet/models/filter"
"github.com/gin-gonic/gin"
"github.com/mitchellh/mapstructure"
)

func ParseSearchParams[T any](c *gin.Context, _ T) (*filter.SearchParams[T], error) {
var params filter.SearchParams[T]

dicts, err := ShouldGetQueryNestedMap(c)
if err != nil {
return nil, err
}

config := mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339),
WeaklyTypedInput: true,
Result: &params,
TagName: "json", // Small hax to reuse json tags which we have already defined
}

decoder, err := mapstructure.NewDecoder(&config)
if err != nil {
return nil, err
}

err = decoder.Decode(dicts)
if err != nil {
return nil, err
}

return &params, nil
}
122 changes: 122 additions & 0 deletions internal/query/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package query

import (
"net/http"
"net/url"
"testing"
"time"

"github.com/bitcoin-sv/spv-wallet/models/filter"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func TestParseSearchParamsSuccessfully(t *testing.T) {
tests := map[string]struct {
url string
expectedResult filter.SearchParams[ExampleConditionsForTests]
}{
"empty query": {
url: "",
expectedResult: filter.SearchParams[ExampleConditionsForTests]{},
},
"query page": {
url: "?page=2&size=200&order=asc&sortBy=id",
expectedResult: filter.SearchParams[ExampleConditionsForTests]{
Page: filter.Page{
Number: 2,
Size: 200,
Order: "asc",
SortBy: "id",
},
},
},
"query conditions model filter": {
url: "?includeDeleted=true&createdRange[from]=2021-01-01T00:00:00Z&createdRange[to]=2021-01-02T00:00:00Z&updatedRange[from]=2021-02-01T00:00:00Z&updatedRange[to]=2021-02-02T00:00:00Z",
expectedResult: filter.SearchParams[ExampleConditionsForTests]{
Conditions: ExampleConditionsForTests{
ModelFilter: filter.ModelFilter{
IncludeDeleted: ptr(true),
CreatedRange: &filter.TimeRange{
From: ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
To: ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)),
},
UpdatedRange: &filter.TimeRange{
From: ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)),
To: ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)),
},
},
},
},
},
"query conditions without nested structs": {
url: "?xBoolean=true&xString=some%20string&xInt=5",
expectedResult: filter.SearchParams[ExampleConditionsForTests]{
Conditions: ExampleConditionsForTests{
XBoolean: ptr(true),
XString: ptr("some string"),
XInt: ptr(5),
},
},
},
"query conditions nested struct": {
url: "?nested[isNested]=true&nested[name]=some%20name&nested[number]=10",
expectedResult: filter.SearchParams[ExampleConditionsForTests]{
Conditions: ExampleConditionsForTests{
Nested: &ExampleNestedConditionsForTests{
IsNested: ptr(true),
Name: ptr("some name"),
Number: ptr(10),
},
},
},
},
"query metadata": {
url: "?metadata[key]=value1&metadata[key2][nested]=value2&metadata[key3][nested][]=value3&metadata[key3][nested][]=value4",
expectedResult: filter.SearchParams[ExampleConditionsForTests]{
Metadata: map[string]interface{}{
"key": "value1",
"key2": map[string]interface{}{
"nested": "value2",
},
"key3": map[string]interface{}{
"nested": []string{"value3", "value4"},
},
},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
u, err := url.Parse(test.url)
require.NoError(t, err)

c := &gin.Context{
Request: &http.Request{
URL: u,
},
}

params, err := ParseSearchParams(c, ExampleConditionsForTests{})
require.NoError(t, err)
require.EqualValues(t, test.expectedResult, *params)
})
}
}

type ExampleConditionsForTests struct {
filter.ModelFilter `json:",inline,squash"`
XBoolean *bool `json:"xBoolean,omitempty"`
XString *string `json:"xString,omitempty"`
XInt *int `json:"xInt,omitempty"`
Nested *ExampleNestedConditionsForTests `json:"nested,omitempty"`
}
type ExampleNestedConditionsForTests struct {
IsNested *bool `json:"isNested,omitempty"`
Name *string `json:"name,omitempty"`
Number *int `json:"number,omitempty"`
}

func ptr[T any](value T) *T {
return &value
}
File renamed without changes.
45 changes: 45 additions & 0 deletions mappings/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package mappings

import (
"github.com/bitcoin-sv/spv-wallet/engine/datastore"
"github.com/bitcoin-sv/spv-wallet/models/filter"
)

const (
defaultPage = 1
defaultPageSize = 50
defaultSortBy = "created_at"
defaultOrder = "desc"
)

// MapToDbQueryParams converts filter.QueryParams from models to matching datastore.QueryParams
func MapToDbQueryParams(model *filter.Page) *datastore.QueryParams {
if model == nil {
return &datastore.QueryParams{
Page: defaultPage,
PageSize: defaultPageSize,
OrderByField: defaultOrder,
SortDirection: defaultOrder,
}
}
return &datastore.QueryParams{
Page: getNumberOrDefault(model.Number, defaultPage),
PageSize: getNumberOrDefault(model.Size, defaultPageSize),
OrderByField: getStringOrDefalut(model.SortBy, defaultSortBy),
SortDirection: getStringOrDefalut(model.Order, defaultOrder),
}
}

func getNumberOrDefault(value int, defaultValue int) int {
if value == 0 {
return defaultValue
}
return value
}

func getStringOrDefalut(value string, defaultValue string) string {
if value == "" {
return defaultValue
}
return value
}
2 changes: 1 addition & 1 deletion models/filter/access_key_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package filter

// AccessKeyFilter is a struct for handling request parameters for destination search requests
type AccessKeyFilter struct {
ModelFilter `json:",inline"`
ModelFilter `json:",inline,squash"`

// RevokedRange specifies the time range when a record was revoked.
RevokedRange *TimeRange `json:"revokedRange,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion models/filter/contact_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package filter

// ContactFilter is a struct for handling request parameters for contact search requests
type ContactFilter struct {
ModelFilter `json:",inline"`
ModelFilter `json:",inline,squash"`
ID *string `json:"id" example:"ffdbe74e-0700-4710-aac5-611a1f877c7f"`
FullName *string `json:"fullName" example:"Alice"`
Paymail *string `json:"paymail" example:"alice@example.com"`
Expand Down
2 changes: 1 addition & 1 deletion models/filter/destination_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package filter

// DestinationFilter is a struct for handling request parameters for destination search requests
type DestinationFilter struct {
ModelFilter `json:",inline"`
ModelFilter `json:",inline,squash"`
LockingScript *string `json:"lockingScript,omitempty" example:"76a9147b05764a97f3b4b981471492aa703b188e45979b88ac"`
Address *string `json:"address,omitempty" example:"1CDUf7CKu8ocTTkhcYUbq75t14Ft168K65"`
DraftID *string `json:"draftId,omitempty" example:"b356f7fa00cd3f20cce6c21d704cd13e871d28d714a5ebd0532f5a0e0cde63f7"`
Expand Down
2 changes: 1 addition & 1 deletion models/filter/paymail_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package filter

// AdminPaymailFilter is a struct for handling request parameters for paymail_addresses search requests
type AdminPaymailFilter struct {
ModelFilter `json:",inline"`
ModelFilter `json:",inline,squash"`

ID *string `json:"id,omitempty" example:"ffb86c103d17d87c15aaf080aab6be5415c9fa885309a79b04c9910e39f2b542"`
XpubID *string `json:"xpubId,omitempty" example:"79f90a6bab0a44402fc64828af820e9465645658aea2d138c5205b88e6dabd00"`
Expand Down
14 changes: 14 additions & 0 deletions models/filter/search_params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package filter

type Page struct {
Number int `json:"page,omitempty"`
Size int `json:"size,omitempty"`
Order string `json:"order,omitempty"`
SortBy string `json:"sortBy,omitempty"`
}

type SearchParams[T any] struct {
Page Page `json:"paging,squash"`
Conditions T `json:"conditions,squash"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
2 changes: 1 addition & 1 deletion models/filter/transaction_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package filter

// TransactionFilter is a struct for handling request parameters for transactions search requests
type TransactionFilter struct {
ModelFilter `json:",inline"`
ModelFilter `json:",inline,squash"`
Hex *string `json:"hex,omitempty"`
BlockHash *string `json:"blockHash,omitempty" example:"0000000000000000031928c28075a82d7a00c2c90b489d1d66dc0afa3f8d26f8"`
BlockHeight *uint64 `json:"blockHeight,omitempty" example:"839376"`
Expand Down
2 changes: 1 addition & 1 deletion models/filter/utxo_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package filter

// UtxoFilter is a struct for handling request parameters for utxo search requests
type UtxoFilter struct {
ModelFilter `json:",inline"`
ModelFilter `json:",inline,squash"`

TransactionID *string `json:"transactionId,omitempty" example:"5e17858ea0ca4155827754ba82bdcfcce108d5bb5b47fbb3aa54bd14540683c6"`
OutputIndex *uint32 `json:"outputIndex,omitempty" example:"0"`
Expand Down
2 changes: 1 addition & 1 deletion models/filter/xpub_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package filter

// XpubFilter is a struct for handling request parameters for utxo search requests
type XpubFilter struct {
ModelFilter `json:",inline"`
ModelFilter `json:",inline,squash"`

ID *string `json:"id,omitempty" example:"00b953624f78004a4c727cd28557475d5233c15f17aef545106639f4d71b712d"`
CurrentBalance *uint64 `json:"currentBalance,omitempty" example:"1000"`
Expand Down

0 comments on commit 10f94a4

Please sign in to comment.