Skip to content

Commit

Permalink
BE-636-domain-packages | Add number package, introduce http helper (#572
Browse files Browse the repository at this point in the history
)

To address cyclic import issues we move several utility functions to
more focused packages.
  • Loading branch information
deividaspetraitis authored Nov 29, 2024
1 parent 0500a54 commit 022c635
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 59 deletions.
23 changes: 23 additions & 0 deletions delivery/http/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package http

import (
"strconv"

"github.com/labstack/echo/v4"
)

// ParseBooleanQueryParam parses a boolean query parameter.
// Returns false if the parameter is not present.
// Errors if the value is not a valid boolean.
// Returns the boolean value and an error if any.
func ParseBooleanQueryParam(c echo.Context, paramName string) (paramValue bool, err error) {
paramValueStr := c.QueryParam(paramName)
if paramValueStr != "" {
paramValue, err = strconv.ParseBool(paramValueStr)
if err != nil {
return false, err
}
}

return paramValue, nil
}
47 changes: 47 additions & 0 deletions delivery/http/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package http

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
)

func TestParseBooleanQueryParam(t *testing.T) {
tests := []struct {
name string
queryParam string
expectedValue bool
expectedError bool
}{
{"True value", "param=true", true, false},
{"False value", "param=false", false, false},
{"1 as true", "param=1", true, false},
{"0 as false", "param=0", false, false},
{"Empty value", "", false, false},
{"Invalid value", "param=invalid", false, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/?"+tt.queryParam, nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Test
value, err := ParseBooleanQueryParam(c, "param")

// Assert
if tt.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedValue, value)
}
})
}
}
51 changes: 51 additions & 0 deletions domain/number/number.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Package number provides utility functions for working with numbers.
package number

import (
"strconv"
"strings"
)

// ParseNumbers parses a comma-separated list of numbers into a slice of unit64.
func ParseNumbers(numbersParam string) ([]uint64, error) {
var numbers []uint64
numStrings := splitAndTrim(numbersParam, ",")

for _, numStr := range numStrings {
num, err := strconv.ParseUint(numStr, 10, 64)
if err != nil {
return nil, err
}
numbers = append(numbers, num)
}

return numbers, nil
}

// ParseNumberType parses a comma-separated list of numbers into a slice of the specified type.
func ParseNumberType[T any](numbersParam string, parseFn func(s string) (T, error)) ([]T, error) {
numStrings := splitAndTrim(numbersParam, ",")

var numbers []T
for _, numStr := range numStrings {
num, err := parseFn(numStr)
if err != nil {
return nil, err
}
numbers = append(numbers, num)
}

return numbers, nil
}

// splitAndTrim splits a string by a separator and trims the resulting strings.
func splitAndTrim(s, sep string) []string {
var result []string
for _, val := range strings.Split(s, sep) {
trimmed := strings.TrimSpace(val)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
159 changes: 159 additions & 0 deletions domain/number/number_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package number

import (
"strconv"
"testing"

"github.com/stretchr/testify/require"
)

func TestParseNumbers(t *testing.T) {
tests := []struct {
name string
input string
want []uint64
wantErr bool
}{
{
name: "valid numbers",
input: "1, 2, 3, 4, 5",
want: []uint64{1, 2, 3, 4, 5},
},
{
name: "single number",
input: "42",
want: []uint64{42},
},
{
name: "large numbers",
input: "1000000, 2000000, 3000000",
want: []uint64{1000000, 2000000, 3000000},
},
{
name: "invalid number",
input: "1, 2, 3, abc, 5",
wantErr: true,
},
{
name: "numbers with extra spaces",
input: " 1 , 2 , 3 ",
want: []uint64{1, 2, 3},
},
{
name: "empty input",
input: "",
want: nil,
},
{
name: "only commas",
input: ",,,",
want: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseNumbers(tt.input)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got)
}
})
}
}

func TestParseNumberType(t *testing.T) {
tests := []struct {
name string
input string
parseFunc func(string) (any, error)
expected any
expectError bool
}{
{
name: "Parse uint64 numbers",
input: "1, 2, 3",
parseFunc: func(s string) (any, error) {
return strconv.ParseUint(s, 10, 64)
},
expected: []uint64{1, 2, 3},
expectError: false,
},
{
name: "Parse int numbers",
input: "-1, 0, 1",
parseFunc: func(s string) (any, error) {
return strconv.Atoi(s)
},
expected: []int{-1, 0, 1},
expectError: false,
},
{
name: "Parse float64 numbers",
input: "1.1, 2.2, 3.3",
parseFunc: func(s string) (any, error) {
return strconv.ParseFloat(s, 64)
},
expected: []float64{1.1, 2.2, 3.3},
expectError: false,
},
{
name: "Invalid input for uint64",
input: "1, 2, -3",
parseFunc: func(s string) (any, error) {
return strconv.ParseUint(s, 10, 64)
},
expected: []uint64{},
expectError: true,
},
{
name: "Empty input",
input: "",
parseFunc: func(s string) (any, error) {
return strconv.Atoi(s)
},
expected: nil,
expectError: false,
},
{
name: "Input with spaces",
input: " 1 , 2 , 3 ",
parseFunc: func(s string) (any, error) {
return strconv.Atoi(s)
},
expected: []int{1, 2, 3},
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result any
var err error

switch tt.expected.(type) {
case []uint64:
result, err = ParseNumberType(tt.input, func(s string) (uint64, error) {
return strconv.ParseUint(s, 10, 64)
})
case []int:
result, err = ParseNumberType(tt.input, func(s string) (int, error) {
return strconv.Atoi(s)
})
case []float64:
result, err = ParseNumberType(tt.input, func(s string) (float64, error) {
return strconv.ParseFloat(s, 64)
})
}

if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, result)
}
})
}
}
4 changes: 2 additions & 2 deletions domain/router_handlet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"reflect"
"testing"

"github.com/osmosis-labs/sqs/domain"
"github.com/osmosis-labs/sqs/domain/number"
"github.com/stretchr/testify/require"
)

Expand All @@ -25,7 +25,7 @@ func TestParseNumbers(t *testing.T) {
}

for _, testCase := range testCases {
actualNumbers, actualError := domain.ParseNumbers(testCase.input)
actualNumbers, actualError := number.ParseNumbers(testCase.input)

if testCase.expectedError {
require.Error(t, actualError)
Expand Down
51 changes: 0 additions & 51 deletions domain/util.go
Original file line number Diff line number Diff line change
@@ -1,44 +1,5 @@
package domain

import (
"strconv"
"strings"

"github.com/labstack/echo/v4"
)

// ParseNumbers parses a comma-separated list of numbers into a slice of unit64.
func ParseNumbers(numbersParam string) ([]uint64, error) {
var numbers []uint64
numStrings := splitAndTrim(numbersParam, ",")

for _, numStr := range numStrings {
num, err := strconv.ParseUint(numStr, 10, 64)
if err != nil {
return nil, err
}
numbers = append(numbers, num)
}

return numbers, nil
}

// ParseBooleanQueryParam parses a boolean query parameter.
// Returns false if the parameter is not present.
// Errors if the value is not a valid boolean.
// Returns the boolean value and an error if any.
func ParseBooleanQueryParam(c echo.Context, paramName string) (paramValue bool, err error) {
paramValueStr := c.QueryParam(paramName)
if paramValueStr != "" {
paramValue, err = strconv.ParseBool(paramValueStr)
if err != nil {
return false, err
}
}

return paramValue, nil
}

// ValidateInputDenoms returns nil of two denoms are valid, otherwise an error.
// This is to be used as a parameter validation for queries.
// For example, token in denom must not equal token out denom for quotes.
Expand All @@ -53,18 +14,6 @@ func ValidateInputDenoms(denomA, denomB string) error {
return nil
}

// splitAndTrim splits a string by a separator and trims the resulting strings.
func splitAndTrim(s, sep string) []string {
var result []string
for _, val := range strings.Split(s, sep) {
trimmed := strings.TrimSpace(val)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}

// Generic function to extract keys from any map.
func KeysFromMap[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m)) // Pre-allocate slice with capacity equal to map size
Expand Down
6 changes: 4 additions & 2 deletions pools/delivery/http/pools_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"github.com/labstack/echo/v4"
"github.com/sirupsen/logrus"

deliveryhttp "github.com/osmosis-labs/sqs/delivery/http"
"github.com/osmosis-labs/sqs/domain"
"github.com/osmosis-labs/sqs/domain/mvc"
"github.com/osmosis-labs/sqs/domain/number"
"github.com/osmosis-labs/sqs/sqsdomain"

"github.com/osmosis-labs/osmosis/osmomath"
Expand Down Expand Up @@ -75,7 +77,7 @@ func (a *PoolsHandler) GetPools(c echo.Context) error {
// Get pool ID parameters as strings.
poolIDsStr := c.QueryParam("IDs")
minLiquidityCapStr := c.QueryParam("min_liquidity_cap")
withMarketIncentives, err := domain.ParseBooleanQueryParam(c, "with_market_incentives")
withMarketIncentives, err := deliveryhttp.ParseBooleanQueryParam(c, "with_market_incentives")
if err != nil {
return c.JSON(http.StatusBadRequest, ResponseError{Message: err.Error()})
}
Expand All @@ -85,7 +87,7 @@ func (a *PoolsHandler) GetPools(c echo.Context) error {
)

// Parse numbers
poolIDs, err := domain.ParseNumbers(poolIDsStr)
poolIDs, err := number.ParseNumbers(poolIDsStr)
if err != nil {
return c.JSON(http.StatusBadRequest, ResponseError{Message: err.Error()})
}
Expand Down
Loading

0 comments on commit 022c635

Please sign in to comment.