From f50e6e228c67116922ea0d205485699fcdb57d35 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Thu, 20 Apr 2023 18:01:36 +0000 Subject: [PATCH] Module split --- .gitignore | 2 + .golangci.yaml | 38 ++++ equal.go | 20 ++ equal_test.go | 464 ++++++++++++++++++++++++++++++++++++++++++ go.mod | 19 ++ go.sum | 33 +++ greater.go | 50 +++++ greater_test.go | 348 ++++++++++++++++++++++++++++++++ greaterequal.go | 51 +++++ greaterequal_test.go | 468 +++++++++++++++++++++++++++++++++++++++++++ hasprefix.go | 54 +++++ hasprefix_test.go | 152 ++++++++++++++ in.go | 5 + in_test.go | 348 ++++++++++++++++++++++++++++++++ justfile | 18 ++ less.go | 50 +++++ less_test.go | 348 ++++++++++++++++++++++++++++++++ lessequal.go | 51 +++++ lessequal_test.go | 468 +++++++++++++++++++++++++++++++++++++++++++ merge.go | 88 ++++++++ merge_test.go | 137 +++++++++++++ op.go | 56 ++++++ parse.go | 153 ++++++++++++++ parse_test.go | 42 ++++ path.go | 259 ++++++++++++++++++++++++ path_test.go | 141 +++++++++++++ pkg_test.go | 11 + slice.go | 27 +++ sort.go | 110 ++++++++++ sort_test.go | 282 ++++++++++++++++++++++++++ 30 files changed, 4293 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 equal.go create mode 100644 equal_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 greater.go create mode 100644 greater_test.go create mode 100644 greaterequal.go create mode 100644 greaterequal_test.go create mode 100644 hasprefix.go create mode 100644 hasprefix_test.go create mode 100644 in.go create mode 100644 in_test.go create mode 100644 justfile create mode 100644 less.go create mode 100644 less_test.go create mode 100644 lessequal.go create mode 100644 lessequal_test.go create mode 100644 merge.go create mode 100644 merge_test.go create mode 100644 op.go create mode 100644 parse.go create mode 100644 parse_test.go create mode 100644 path.go create mode 100644 path_test.go create mode 100644 pkg_test.go create mode 100644 slice.go create mode 100644 sort.go create mode 100644 sort_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6754c7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cover.out +cover.html diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..9aa3e2e --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,38 @@ +linters: + enable-all: true + disable: + # re-enable when working + - rowserrcheck + - wastedassign + # maybe enable these + - wrapcheck + # leave these disabled + - cyclop + - deadcode + - dupl + - exhaustivestruct + - exhaustruct + - forbidigo + - forcetypeassert + - funlen + - gochecknoglobals + - gocognit + - goconst + - godox + - golint + - gomnd + - ifshort + - interfacer + - lll + - maintidx + - maligned + - nilnil + - nestif + - nlreturn + - nolintlint + - nosnakecase + - scopelint + - structcheck + - thelper + - varcheck + - varnamelen diff --git a/equal.go b/equal.go new file mode 100644 index 0000000..dd63d99 --- /dev/null +++ b/equal.go @@ -0,0 +1,20 @@ +package path + +import "time" + +func Equal(obj any, path string, matchStr string) (bool, error) { + return op(obj, path, matchStr, equal) +} + +func equal(obj, match any, _ string) bool { + switch objt := obj.(type) { + case time.Time: + tm := match.(*timeVal) + + // TODO: Replace Truncate() with a timezone-aware version + return tm.time.Equal(objt.Truncate(tm.precision)) + + default: + return obj == match + } +} diff --git a/equal_test.go b/equal_test.go new file mode 100644 index 0000000..40592e9 --- /dev/null +++ b/equal_test.go @@ -0,0 +1,464 @@ +package path_test + +import ( + "testing" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +func TestEqualStruct(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType2{ + Tt1: testType1{ + Int: 2345, + }, + }, "tt1.int", "2345") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType2{ + Tt1p: &testType1{ + Int: 2345, + }, + }, "tt1p.int", "2345") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType2{}, "tt1p.int", "2345") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualPointer(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + match, err := path.Equal(&testType1{ + TimeP: &tm, + }, "timep", "2006-01-02T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + TimeP: &tm, + }, "timep", "2006-01-02T15:04:05+01:00") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualPointers(t *testing.T) { + t.Parallel() + + tm1, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + tm2, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-10T15:04:05Z") + require.NoError(t, err) + + match, err := path.Equal(&testType1{ + TimesP: []*time.Time{&tm1, nil, &tm2}, + }, "timesp", "2006-01-10T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + TimesP: []*time.Time{&tm1, &tm2}, + }, "timesp", "2006-01-02T15:04:05+01:00") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualInt(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Int: 1234, + }, "int", "1234") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Int: 1234, + }, "int", "1235") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualInt64(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Int64: 3456, + }, "int64", "3456") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Int64: 3456, + }, "int64", "3457") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualUInt(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + UInt: 4567, + }, "uint", "4567") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + UInt: 4567, + }, "uint", "4568") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualUInt64(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + UInt64: 5678, + }, "uint64", "5678") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + UInt64: 5678, + }, "uint64", "5679") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualFloat32(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Float32: 3.1415, + }, "float32", "3.1415") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Float32: 3.1415, + }, "float32", "3.1416") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualFloat64(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159265") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159266") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualString(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + String: "foo", + }, "string2", "foo") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + String: "foo", + }, "string2", "bar") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualBool(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Bool: true, + }, "bool2", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Bool: true, + }, "bool2", "false") + require.NoError(t, err) + require.False(t, match) + + boolp := true + + match, err = path.Equal(&testType1{ + BoolP: &boolp, + }, "boolp", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{}, "boolp", "false") + require.NoError(t, err) + require.True(t, match) +} + +func TestEqualInts(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "4") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "5") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualInt64s(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "4") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "5") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualUInts(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "4") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "5") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualUInt64s(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "4") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "5") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualFloat32s(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7182") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7183") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualFloat64s(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7182") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7183") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualStrings(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "foo") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "zig") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualBools(t *testing.T) { + t.Parallel() + + match, err := path.Equal(&testType1{ + Bools: []bool{true, false}, + }, "bools", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Bools: []bool{false, false}, + }, "bools", "true") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualTime(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + match, err := path.Equal(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:05+00:00") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:05+01:00") + require.NoError(t, err) + require.False(t, match) + + match, err = path.Equal(&testType1{ + Time: tm, + }, "time", "1136214245") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Time: tm, + }, "time", "1136214246") + require.NoError(t, err) + require.False(t, match) + + match, err = path.Equal(&testType1{ + Time: tm, + }, "time", "1136214245000") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Time: tm, + }, "time", "1136214245001") + require.NoError(t, err) + require.False(t, match) + + tm2, err := time.Parse("2006-01-02T15:04:05.999999999Z", "2006-01-02T15:04:05.500000000Z") + require.NoError(t, err) + + match, err = path.Equal(&testType1{ + Time: tm2, + }, "time", "1136214245") + require.NoError(t, err) + require.True(t, match) +} + +func TestEqualTimes(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + tm2, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-10T15:04:05Z") + require.NoError(t, err) + + match, err := path.Equal(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "1136214245000") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "1136214245001") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualDate(t *testing.T) { + t.Parallel() + + d, err := civil.ParseDate("2006-01-01") + require.NoError(t, err) + + match, err := path.Equal(&testType1{ + Date: d, + }, "date", "2006-01-01") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Date: d, + }, "date", "2006-01-02") + require.NoError(t, err) + require.False(t, match) +} + +func TestEqualDates(t *testing.T) { + t.Parallel() + + d1, err := civil.ParseDate("2006-01-01") + require.NoError(t, err) + + d2, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + match, err := path.Equal(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-02") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Equal(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-03") + require.NoError(t, err) + require.False(t, match) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b6ef70c --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/gopatchy/path + +go 1.19 + +require ( + cloud.google.com/go v0.110.0 + github.com/gopatchy/jsrest v0.0.0-20230420161234-12a6d6da8b7f + github.com/stretchr/testify v1.8.2 + go.uber.org/goleak v1.2.1 + golang.org/x/exp v0.0.0-20230420155640-133eef4313cb +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gopatchy/metadata v0.0.0-20230420053349-25837551c11d // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/vfaronov/httpheader v0.1.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2059568 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/gopatchy/jsrest v0.0.0-20230420161234-12a6d6da8b7f h1:1uGPJm9K0Fro1UEcZpuK6FNPU/U1XX3aS3x0/PdFS40= +github.com/gopatchy/jsrest v0.0.0-20230420161234-12a6d6da8b7f/go.mod h1:Ryi8LRBLFDhQsMQHuh+6VL7HcFWjBXOEiOy9Ip/Q+Ps= +github.com/gopatchy/metadata v0.0.0-20230420053349-25837551c11d h1:chunoM47vkWSanIvLx4uRSkLMG6chDZOy09L2tt/bv8= +github.com/gopatchy/metadata v0.0.0-20230420053349-25837551c11d/go.mod h1:VgD33raUShjDePCDBo55aj+eSXFtUEpMzs+Ie39g2zo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/vfaronov/httpheader v0.1.0 h1:VdzetvOKRoQVHjSrXcIOwCV6JG5BCAW9rjbVbFPBmb0= +github.com/vfaronov/httpheader v0.1.0/go.mod h1:ZBxgbYu6nbN5V9Ptd1yYUUan0voD0O8nZLXHyxLgoLE= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +golang.org/x/exp v0.0.0-20230420155640-133eef4313cb h1:rhjz/8Mbfa8xROFiH+MQphmAmgqRM0bOMnytznhWEXk= +golang.org/x/exp v0.0.0-20230420155640-133eef4313cb/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/greater.go b/greater.go new file mode 100644 index 0000000..8ebb4ba --- /dev/null +++ b/greater.go @@ -0,0 +1,50 @@ +package path + +import ( + "time" + + "cloud.google.com/go/civil" +) + +func Greater(obj any, path string, matchStr string) (bool, error) { + return op(obj, path, matchStr, greater) +} + +func greater(obj, match any, _ string) bool { + switch objt := obj.(type) { + case int: + return objt > match.(int) + + case int64: + return objt > match.(int64) + + case uint: + return objt > match.(uint) + + case uint64: + return objt > match.(uint64) + + case float32: + return objt > match.(float32) + + case float64: + return objt > match.(float64) + + case string: + return objt > match.(string) + + case bool: + return objt && !match.(bool) + + case time.Time: + tm := match.(*timeVal) + + return objt.Truncate(tm.precision).After(tm.time) + + case civil.Date: + return objt.After(match.(civil.Date)) + + default: + panic(obj) + } +} diff --git a/greater_test.go b/greater_test.go new file mode 100644 index 0000000..1d17663 --- /dev/null +++ b/greater_test.go @@ -0,0 +1,348 @@ +package path_test + +import ( + "testing" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +func TestGreaterInt(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Int: 1234, + }, "int", "1233") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Int: 1234, + }, "int", "1235") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterInt64(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Int64: 3456, + }, "int64", "3455") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Int64: 3456, + }, "int64", "3457") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterUInt(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + UInt: 4567, + }, "uint", "4566") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + UInt: 4567, + }, "uint", "4568") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterUInt64(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + UInt64: 5678, + }, "uint64", "5677") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + UInt64: 5678, + }, "uint64", "5679") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterFloat32(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Float32: 3.1415, + }, "float32", "3.1414") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Float32: 3.1415, + }, "float32", "3.1416") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterFloat64(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159264") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159266") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterString(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + String: "foo", + }, "string2", "bar") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + String: "foo", + }, "string2", "zig") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterBool(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Bool: true, + }, "bool2", "false") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Bool: false, + }, "bool2", "true") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterInts(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "8") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterInt64s(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "8") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterUInts(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "8") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterUInt64s(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "8") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterFloat32s(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7181") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "3.1416") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterFloat64s(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7181") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "3.1416") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterStrings(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "baz") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "zig") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterBools(t *testing.T) { + t.Parallel() + + match, err := path.Greater(&testType1{ + Bools: []bool{true, false}, + }, "bools", "false") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Bools: []bool{true, false}, + }, "bools", "true") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterTime(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + match, err := path.Greater(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:04Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:06Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterTimes(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + tm2, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-10T15:04:05Z") + require.NoError(t, err) + + match, err := path.Greater(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-05T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-11T15:04:05Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterDate(t *testing.T) { + t.Parallel() + + d, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + match, err := path.Greater(&testType1{ + Date: d, + }, "date", "2006-01-01") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Date: d, + }, "date", "2006-01-03") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterDates(t *testing.T) { + t.Parallel() + + d1, err := civil.ParseDate("2006-01-01") + require.NoError(t, err) + + d2, err := civil.ParseDate("2006-01-03") + require.NoError(t, err) + + match, err := path.Greater(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-02") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Greater(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-04") + require.NoError(t, err) + require.False(t, match) +} diff --git a/greaterequal.go b/greaterequal.go new file mode 100644 index 0000000..bc57b20 --- /dev/null +++ b/greaterequal.go @@ -0,0 +1,51 @@ +package path + +import ( + "time" + + "cloud.google.com/go/civil" +) + +func GreaterEqual(obj any, path string, matchStr string) (bool, error) { + return op(obj, path, matchStr, greaterEqual) +} + +func greaterEqual(obj, match any, _ string) bool { + switch objt := obj.(type) { + case int: + return objt >= match.(int) + + case int64: + return objt >= match.(int64) + + case uint: + return objt >= match.(uint) + + case uint64: + return objt >= match.(uint64) + + case float32: + return objt >= match.(float32) + + case float64: + return objt >= match.(float64) + + case string: + return objt >= match.(string) + + case bool: + return objt || objt == match.(bool) + + case time.Time: + tm := match.(*timeVal) + trunc := objt.Truncate(tm.precision) + + return trunc.Equal(tm.time) || trunc.After(tm.time) + + case civil.Date: + return objt == match.(civil.Date) || objt.After(match.(civil.Date)) + + default: + panic(obj) + } +} diff --git a/greaterequal_test.go b/greaterequal_test.go new file mode 100644 index 0000000..ea603d0 --- /dev/null +++ b/greaterequal_test.go @@ -0,0 +1,468 @@ +package path_test + +import ( + "testing" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +func TestGreaterEqualInt(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Int: 1234, + }, "int", "1233") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Int: 1234, + }, "int", "1234") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Int: 1234, + }, "int", "1235") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualInt64(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Int64: 3456, + }, "int64", "3455") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Int64: 3456, + }, "int64", "3456") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Int64: 3456, + }, "int64", "3457") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualUInt(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + UInt: 4567, + }, "uint", "4566") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + UInt: 4567, + }, "uint", "4567") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + UInt: 4567, + }, "uint", "4568") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualUInt64(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + UInt64: 5678, + }, "uint64", "5677") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + UInt64: 5678, + }, "uint64", "5678") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + UInt64: 5678, + }, "uint64", "5679") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualFloat32(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Float32: 3.1415, + }, "float32", "3.1414") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Float32: 3.1415, + }, "float32", "3.1415") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Float32: 3.1415, + }, "float32", "3.1416") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualFloat64(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159264") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159265") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159266") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualString(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + String: "foo", + }, "string2", "bar") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + String: "foo", + }, "string2", "foo") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + String: "foo", + }, "string2", "zig") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualBool(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Bool: true, + }, "bool2", "false") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Bool: true, + }, "bool2", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Bool: false, + }, "bool2", "true") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualInts(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "7") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "8") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualInt64s(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "7") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "8") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualUInts(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "7") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "8") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualUInt64s(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "7") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "8") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualFloat32s(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7181") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "3.1415") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "3.1416") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualFloat64s(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7181") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "3.1415") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "3.1416") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualStrings(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "baz") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "foo") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "zig") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualBools(t *testing.T) { + t.Parallel() + + match, err := path.GreaterEqual(&testType1{ + Bools: []bool{true, false}, + }, "bools", "false") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Bools: []bool{true, false}, + }, "bools", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Bools: []bool{false, false}, + }, "bools", "true") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualTime(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + match, err := path.GreaterEqual(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:04Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:06Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualTimes(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + tm2, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-10T15:04:05Z") + require.NoError(t, err) + + match, err := path.GreaterEqual(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-05T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-10T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-11T15:04:05Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualDate(t *testing.T) { + t.Parallel() + + d, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + match, err := path.GreaterEqual(&testType1{ + Date: d, + }, "date", "2006-01-01") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Date: d, + }, "date", "2006-01-02") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Date: d, + }, "date", "2006-01-03") + require.NoError(t, err) + require.False(t, match) +} + +func TestGreaterEqualDates(t *testing.T) { + t.Parallel() + + d1, err := civil.ParseDate("2006-01-01") + require.NoError(t, err) + + d2, err := civil.ParseDate("2006-01-03") + require.NoError(t, err) + + match, err := path.GreaterEqual(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-02") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-03") + require.NoError(t, err) + require.True(t, match) + + match, err = path.GreaterEqual(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-04") + require.NoError(t, err) + require.False(t, match) +} diff --git a/hasprefix.go b/hasprefix.go new file mode 100644 index 0000000..fd0fa89 --- /dev/null +++ b/hasprefix.go @@ -0,0 +1,54 @@ +package path + +import ( + "strconv" + "strings" + "time" + + "cloud.google.com/go/civil" +) + +func HasPrefix(obj any, path string, matchStr string) (bool, error) { + return op(obj, path, matchStr, hasPrefix) +} + +func hasPrefix(obj, match any, matchStr string) bool { + var objStr string + + switch objt := obj.(type) { + case int: + objStr = strconv.FormatInt(int64(objt), 10) + + case int64: + objStr = strconv.FormatInt(objt, 10) + + case uint: + objStr = strconv.FormatUint(uint64(objt), 10) + + case uint64: + objStr = strconv.FormatUint(objt, 10) + + case float32: + objStr = strconv.FormatFloat(float64(objt), 'f', -1, 32) + + case float64: + objStr = strconv.FormatFloat(objt, 'f', -1, 64) + + case string: + objStr = objt + + case bool: + objStr = strconv.FormatBool(objt) + + case time.Time: + objStr = objt.String() + + case civil.Date: + objStr = objt.String() + + default: + panic(obj) + } + + return strings.HasPrefix(objStr, matchStr) +} diff --git a/hasprefix_test.go b/hasprefix_test.go new file mode 100644 index 0000000..15ac04f --- /dev/null +++ b/hasprefix_test.go @@ -0,0 +1,152 @@ +package path_test + +import ( + "testing" + + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +func TestHasPrefixInt(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + Int: -1234, + }, "int", "-12") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + Int: -1234, + }, "int", "23") + require.NoError(t, err) + require.False(t, match) +} + +func TestHasPrefixInt64(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + Int64: 3456, + }, "int64", "34") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + Int64: 3456, + }, "int64", "45") + require.NoError(t, err) + require.False(t, match) +} + +func TestHasPrefixUInt(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + UInt: 4567, + }, "uint", "45") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + UInt: 4567, + }, "uint", "457") + require.NoError(t, err) + require.False(t, match) +} + +func TestHasPrefixUInt64(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + UInt64: 5678, + }, "uint64", "567") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + UInt64: 5678, + }, "uint64", "56789") + require.NoError(t, err) + require.False(t, match) +} + +func TestHasPrefixFloat32(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + Float32: -3.1415, + }, "float32", "-3.14") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + Float32: -3.1415, + }, "float32", "3.14") + require.NoError(t, err) + require.False(t, match) +} + +func TestHasPrefixFloat64(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + Float64: 3.14159265, + }, "float64", "3.1415926") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + Float64: 3.14159265, + }, "float64", "3.141592651") + require.NoError(t, err) + require.False(t, match) +} + +func TestHasPrefixString(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + String: "foobar", + }, "string2", "foo") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + String: "foobar", + }, "string2", "bar") + require.NoError(t, err) + require.False(t, match) +} + +func TestHasPrefixBool(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + Bool: true, + }, "bool2", "t") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + Bool: true, + }, "bool2", "f") + require.NoError(t, err) + require.False(t, match) +} + +func TestHasPrefixStrings(t *testing.T) { + t.Parallel() + + match, err := path.HasPrefix(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "f") + require.NoError(t, err) + require.True(t, match) + + match, err = path.HasPrefix(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "z") + require.NoError(t, err) + require.False(t, match) +} diff --git a/in.go b/in.go new file mode 100644 index 0000000..281afdd --- /dev/null +++ b/in.go @@ -0,0 +1,5 @@ +package path + +func In(obj any, path string, matchStr string) (bool, error) { + return opList(obj, path, matchStr, equal) +} diff --git a/in_test.go b/in_test.go new file mode 100644 index 0000000..058e44c --- /dev/null +++ b/in_test.go @@ -0,0 +1,348 @@ +package path_test + +import ( + "testing" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +func TestInInt(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Int: 1234, + }, "int", "1233,1234,1235") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Int: 1234, + }, "int", "1233,1235") + require.NoError(t, err) + require.False(t, match) +} + +func TestInInt64(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Int64: 3456, + }, "int64", "3455,3456,3457") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Int64: 3456, + }, "int64", "3455,3457") + require.NoError(t, err) + require.False(t, match) +} + +func TestInUInt(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + UInt: 4567, + }, "uint", "4566,4567,4568") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + UInt: 4567, + }, "uint", "4566,4568") + require.NoError(t, err) + require.False(t, match) +} + +func TestInUInt64(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + UInt64: 5678, + }, "uint64", "5677,5678,5679") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + UInt64: 5678, + }, "uint64", "5677,5679") + require.NoError(t, err) + require.False(t, match) +} + +func TestInFloat32(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Float32: 3.1415, + }, "float32", "3.1414,3.1415,3.1416") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Float32: 3.1415, + }, "float32", "3.1414,3.1416") + require.NoError(t, err) + require.False(t, match) +} + +func TestInFloat64(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159264,3.14159265,3.14159266") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159264,3.14159266") + require.NoError(t, err) + require.False(t, match) +} + +func TestInString(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + String: "foo", + }, "string2", "zig,foo,bar") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + String: "foo", + }, "string2", "zig,bar") + require.NoError(t, err) + require.False(t, match) +} + +func TestInBool(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Bool: true, + }, "bool2", "true,false") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Bool: true, + }, "bool2", "false,false") + require.NoError(t, err) + require.False(t, match) +} + +func TestInInts(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "3,4,5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "3,5") + require.NoError(t, err) + require.False(t, match) +} + +func TestInInt64s(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "3,4,5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "3,5") + require.NoError(t, err) + require.False(t, match) +} + +func TestInUInts(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "3,4,5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "3,5") + require.NoError(t, err) + require.False(t, match) +} + +func TestInUInt64s(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "3,4,5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "3,5") + require.NoError(t, err) + require.False(t, match) +} + +func TestInFloat32s(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7181,2.7182,2.7183") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7181,2.7183") + require.NoError(t, err) + require.False(t, match) +} + +func TestInFloat64s(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7181,2.7182,2.7183") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7181,2.7183") + require.NoError(t, err) + require.False(t, match) +} + +func TestInStrings(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "baz,foo,zig") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "baz,zig") + require.NoError(t, err) + require.False(t, match) +} + +func TestInBools(t *testing.T) { + t.Parallel() + + match, err := path.In(&testType1{ + Bools: []bool{true, false}, + }, "bools", "true,false") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Bools: []bool{false, false}, + }, "bools", "true,true") + require.NoError(t, err) + require.False(t, match) +} + +func TestInTime(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + match, err := path.In(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:04Z,2006-01-02T15:04:05Z,2006-01-02T15:04:06Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:04Z,2006-01-02T15:04:06Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestInTimes(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + tm2, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-10T15:04:05Z") + require.NoError(t, err) + + match, err := path.In(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-02T15:04:04Z,2006-01-02T15:04:05Z,2006-01-02T15:04:06Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-02T15:04:04Z,2006-01-02T15:04:06Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestInDate(t *testing.T) { + t.Parallel() + + d, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + match, err := path.In(&testType1{ + Date: d, + }, "date", "2006-01-01,2006-01-02,2006-01-03") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Date: d, + }, "date", "2006-01-01,2006-01-03") + require.NoError(t, err) + require.False(t, match) +} + +func TestInDates(t *testing.T) { + t.Parallel() + + d1, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + d2, err := civil.ParseDate("2006-01-05") + require.NoError(t, err) + + match, err := path.In(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-01,2006-01-02,2006-01-03") + require.NoError(t, err) + require.True(t, match) + + match, err = path.In(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-01,2006-01-03") + require.NoError(t, err) + require.False(t, match) +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..d607f93 --- /dev/null +++ b/justfile @@ -0,0 +1,18 @@ +go := env_var_or_default('GOCMD', 'go') + +default: tidy test + +tidy: + {{go}} mod tidy + goimports -l -w . + gofumpt -l -w . + {{go}} fmt ./... + +test: + {{go}} vet ./... + golangci-lint run ./... + {{go}} test -race -coverprofile=cover.out -timeout=60s -parallel=10 ./... + {{go}} tool cover -html=cover.out -o=cover.html + +todo: + -git grep -e TODO --and --not -e ignoretodo diff --git a/less.go b/less.go new file mode 100644 index 0000000..87b585b --- /dev/null +++ b/less.go @@ -0,0 +1,50 @@ +package path + +import ( + "time" + + "cloud.google.com/go/civil" +) + +func Less(obj any, path string, matchStr string) (bool, error) { + return op(obj, path, matchStr, less) +} + +func less(obj, match any, _ string) bool { + switch objt := obj.(type) { + case int: + return objt < match.(int) + + case int64: + return objt < match.(int64) + + case uint: + return objt < match.(uint) + + case uint64: + return objt < match.(uint64) + + case float32: + return objt < match.(float32) + + case float64: + return objt < match.(float64) + + case string: + return objt < match.(string) + + case bool: + return !objt && match.(bool) + + case time.Time: + tm := match.(*timeVal) + + return objt.Truncate(tm.precision).Before(tm.time) + + case civil.Date: + return objt.Before(match.(civil.Date)) + + default: + panic(obj) + } +} diff --git a/less_test.go b/less_test.go new file mode 100644 index 0000000..fcb3f58 --- /dev/null +++ b/less_test.go @@ -0,0 +1,348 @@ +package path_test + +import ( + "testing" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +func TestLessInt(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Int: 1234, + }, "int", "1235") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Int: 1234, + }, "int", "1233") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessInt64(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Int64: 3456, + }, "int64", "3457") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Int64: 3456, + }, "int64", "3455") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessUInt(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + UInt: 4567, + }, "uint", "4568") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + UInt: 4567, + }, "uint", "4566") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessUInt64(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + UInt64: 5678, + }, "uint64", "5679") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + UInt64: 5678, + }, "uint64", "5677") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessFloat32(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Float32: 3.1415, + }, "float32", "3.1416") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Float32: 3.1415, + }, "float32", "3.1414") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessFloat64(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159266") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159264") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessString(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + String: "foo", + }, "string2", "zig") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + String: "foo", + }, "string2", "bar") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessBool(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Bool: false, + }, "bool2", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Bool: true, + }, "bool2", "false") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessInts(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "3") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "1") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessInt64s(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "3") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "1") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessUInts(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "3") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "1") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessUInt64s(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "3") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "1") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessFloat32s(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "3.1414") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7181") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessFloat64s(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "3.1414") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7181") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessStrings(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "baz") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "adv") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessBools(t *testing.T) { + t.Parallel() + + match, err := path.Less(&testType1{ + Bools: []bool{true, false}, + }, "bools", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Bools: []bool{true, false}, + }, "bools", "false") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessTime(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + match, err := path.Less(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:06Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:04Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessTimes(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + tm2, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-10T15:04:05Z") + require.NoError(t, err) + + match, err := path.Less(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-05T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-01T15:04:05Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessDate(t *testing.T) { + t.Parallel() + + d, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + match, err := path.Less(&testType1{ + Date: d, + }, "date", "2006-01-03") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Date: d, + }, "date", "2006-01-01") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessDates(t *testing.T) { + t.Parallel() + + d1, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + d2, err := civil.ParseDate("2006-01-04") + require.NoError(t, err) + + match, err := path.Less(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-03") + require.NoError(t, err) + require.True(t, match) + + match, err = path.Less(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-01") + require.NoError(t, err) + require.False(t, match) +} diff --git a/lessequal.go b/lessequal.go new file mode 100644 index 0000000..ca66e84 --- /dev/null +++ b/lessequal.go @@ -0,0 +1,51 @@ +package path + +import ( + "time" + + "cloud.google.com/go/civil" +) + +func LessEqual(obj any, path string, matchStr string) (bool, error) { + return op(obj, path, matchStr, lessEqual) +} + +func lessEqual(obj, match any, _ string) bool { + switch objt := obj.(type) { + case int: + return objt <= match.(int) + + case int64: + return objt <= match.(int64) + + case uint: + return objt <= match.(uint) + + case uint64: + return objt <= match.(uint64) + + case float32: + return objt <= match.(float32) + + case float64: + return objt <= match.(float64) + + case string: + return objt <= match.(string) + + case bool: + return !objt || objt == match.(bool) + + case time.Time: + tm := match.(*timeVal) + trunc := objt.Truncate(tm.precision) + + return trunc.Equal(tm.time) || trunc.Before(tm.time) + + case civil.Date: + return objt == match.(civil.Date) || objt.Before(match.(civil.Date)) + + default: + panic(obj) + } +} diff --git a/lessequal_test.go b/lessequal_test.go new file mode 100644 index 0000000..5d894ef --- /dev/null +++ b/lessequal_test.go @@ -0,0 +1,468 @@ +package path_test + +import ( + "testing" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +func TestLessEqualInt(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Int: 1234, + }, "int", "1235") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Int: 1234, + }, "int", "1234") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Int: 1234, + }, "int", "1233") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualInt64(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Int64: 3456, + }, "int64", "3457") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Int64: 3456, + }, "int64", "3456") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Int64: 3456, + }, "int64", "3455") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualUInt(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + UInt: 4567, + }, "uint", "4568") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + UInt: 4567, + }, "uint", "4567") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + UInt: 4567, + }, "uint", "4566") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualUInt64(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + UInt64: 5678, + }, "uint64", "5679") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + UInt64: 5678, + }, "uint64", "5678") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + UInt64: 5678, + }, "uint64", "5677") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualFloat32(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Float32: 3.1415, + }, "float32", "3.1416") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Float32: 3.1415, + }, "float32", "3.1415") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Float32: 3.1415, + }, "float32", "3.1414") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualFloat64(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159266") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159265") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Float64: 3.14159265, + }, "float64", "3.14159264") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualString(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + String: "foo", + }, "string2", "zig") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + String: "foo", + }, "string2", "foo") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + String: "foo", + }, "string2", "bar") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualBool(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Bool: false, + }, "bool2", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Bool: true, + }, "bool2", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Bool: true, + }, "bool2", "false") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualInts(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "2") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Ints: []int{2, 4, 7}, + }, "ints", "1") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualInt64s(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "2") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Int64s: []int64{2, 4, 7}, + }, "int64s", "1") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualUInts(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "2") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + UInts: []uint{2, 4, 7}, + }, "uints", "1") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualUInt64s(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "5") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "2") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + UInt64s: []uint64{2, 4, 7}, + }, "uint64s", "1") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualFloat32s(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "3.1414") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7182") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Float32s: []float32{3.1415, 2.7182}, + }, "float32s", "2.7181") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualFloat64s(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "3.1414") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7182") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Float64s: []float64{3.1415, 2.7182}, + }, "float64s", "2.7181") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualStrings(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "baz") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "bar") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Strings: []string{"foo", "bar"}, + }, "strings", "adv") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualBools(t *testing.T) { + t.Parallel() + + match, err := path.LessEqual(&testType1{ + Bools: []bool{true, false}, + }, "bools", "false") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Bools: []bool{true, false}, + }, "bools", "true") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Bools: []bool{true, true}, + }, "bools", "false") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualTime(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + match, err := path.LessEqual(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:06Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Time: tm, + }, "time", "2006-01-02T15:04:04Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualTimes(t *testing.T) { + t.Parallel() + + tm, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + + tm2, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-10T15:04:05Z") + require.NoError(t, err) + + match, err := path.LessEqual(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-05T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-02T15:04:05Z") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Times: []time.Time{tm, tm2}, + }, "times", "2006-01-01T15:04:05Z") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualDate(t *testing.T) { + t.Parallel() + + d, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + match, err := path.LessEqual(&testType1{ + Date: d, + }, "date", "2006-01-03") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Date: d, + }, "date", "2006-01-02") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Date: d, + }, "date", "2006-01-01") + require.NoError(t, err) + require.False(t, match) +} + +func TestLessEqualDates(t *testing.T) { + t.Parallel() + + d1, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + + d2, err := civil.ParseDate("2006-01-04") + require.NoError(t, err) + + match, err := path.LessEqual(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-03") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-02") + require.NoError(t, err) + require.True(t, match) + + match, err = path.LessEqual(&testType1{ + Dates: []civil.Date{d1, d2}, + }, "dates", "2006-01-01") + require.NoError(t, err) + require.False(t, match) +} diff --git a/merge.go b/merge.go new file mode 100644 index 0000000..07ffe38 --- /dev/null +++ b/merge.go @@ -0,0 +1,88 @@ +package path + +import ( + "encoding/json" + "reflect" +) + +func Merge(to, from any) { + MergeValue(reflect.ValueOf(to), reflect.ValueOf(from)) +} + +func MergeValue(to, from reflect.Value) { + to = reflect.Indirect(to) + from = reflect.Indirect(from) + + for i := 0; i < to.NumField(); i++ { + toField := to.Field(i) + fromField := from.Field(i) + + if fromField.IsZero() { + continue + } + + if reflect.Indirect(fromField).Kind() == reflect.Struct { + MergeValue(toField, fromField) + continue + } + + toField.Set(fromField) + } +} + +func MergeMap(to any, from map[string]any) error { + m, err := ToMap(to) + if err != nil { + // TODO: Wrap error + return err + } + + MergeMaps(m, from) + + return FromMap(to, m) +} + +func MergeMaps(to map[string]any, from map[string]any) { + for k, v := range from { + if vMap, isMap := v.(map[string]any); isMap { + if _, ok := to[k].(map[string]any); !ok { + // Either key doesn't exist or it's a different type + // If different type, error will happen during json decode + to[k] = map[string]any{} + } + + MergeMaps(to[k].(map[string]any), vMap) + } else { + to[k] = v + } + } +} + +func ToMap(from any) (map[string]any, error) { + js, err := json.Marshal(from) + if err != nil { + // TODO: Wrap error + return nil, err + } + + ret := map[string]any{} + + err = json.Unmarshal(js, &ret) + if err != nil { + // TODO: Wrap error + return nil, err + } + + return ret, nil +} + +func FromMap(to any, from map[string]any) error { + js, err := json.Marshal(from) + if err != nil { + // TODO: Wrap error + return err + } + + // TODO: Wrap error + return json.Unmarshal(js, to) +} diff --git a/merge_test.go b/merge_test.go new file mode 100644 index 0000000..2b1ad70 --- /dev/null +++ b/merge_test.go @@ -0,0 +1,137 @@ +package path_test + +import ( + "encoding/json" + "testing" + + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +type mergeTestType struct { + A string + B int + C []string + D nestedType + E *nestedType + H int +} + +type nestedType struct { + F []int + G string +} + +func TestMergeString(t *testing.T) { + t.Parallel() + + to := &mergeTestType{ + A: "foo", + B: 42, + } + + path.Merge(to, &mergeTestType{ + A: "bar", + }) + + require.Equal(t, "bar", to.A) + require.Equal(t, 42, to.B) +} + +func TestMergeSlice(t *testing.T) { + t.Parallel() + + to := &mergeTestType{ + B: 42, + C: []string{"foo", "bar"}, + } + + path.Merge(to, &mergeTestType{ + C: []string{"zig", "zag"}, + }) + + require.Equal(t, 42, to.B) + require.Equal(t, []string{"zig", "zag"}, to.C) +} + +func TestMergeNested(t *testing.T) { + t.Parallel() + + to := &mergeTestType{ + B: 42, + D: nestedType{ + F: []int{42, 43}, + G: "bar", + }, + } + + path.Merge(to, &mergeTestType{ + D: nestedType{ + F: []int{44, 45}, + }, + }) + + require.Equal(t, 42, to.B) + require.Equal(t, []int{44, 45}, to.D.F) + require.Equal(t, "bar", to.D.G) +} + +func TestMergeNestedPointer(t *testing.T) { + t.Parallel() + + to := &mergeTestType{ + B: 42, + E: &nestedType{ + F: []int{42, 43}, + G: "bar", + }, + } + + path.Merge(to, &mergeTestType{ + E: &nestedType{ + F: []int{49, 50}, + }, + }) + + require.Equal(t, 42, to.B) + require.Equal(t, []int{49, 50}, to.E.F) + require.Equal(t, "bar", to.E.G) +} + +func TestMergeMap(t *testing.T) { + t.Parallel() + + to := &mergeTestType{ + A: "foo", + B: 42, + D: nestedType{ + F: []int{42, 43}, + G: "bar", + }, + H: 5, + } + + from := map[string]any{} + + err := json.Unmarshal( + []byte(` +{ + "B": 45, + "D": { + "F": [46, 47] + }, + "H": 0 +}`), + &from, + ) + require.NoError(t, err) + + err = path.MergeMap(to, from) + require.NoError(t, err) + + require.Equal(t, "foo", to.A) + require.Equal(t, 45, to.B) + require.Equal(t, []int{46, 47}, to.D.F) + require.Equal(t, "bar", to.D.G) + require.Equal(t, 0, to.H) +} diff --git a/op.go b/op.go new file mode 100644 index 0000000..38c4080 --- /dev/null +++ b/op.go @@ -0,0 +1,56 @@ +package path + +import ( + "strings" +) + +func op(obj any, path string, matchStr string, cb func(any, any, string) bool) (bool, error) { + objVal, err := Get(obj, path) + if err != nil { + return false, err + } + + matchVal, err := parse(matchStr, objVal) + if err != nil { + return false, err + } + + if isSlice(objVal) { + return anyTrue(objVal, func(x any, _ int) bool { return cb(x, matchVal, matchStr) }), nil + } + + return cb(objVal, matchVal, matchStr), nil +} + +func opList(obj any, path string, matchStr string, cb func(any, any, string) bool) (bool, error) { + objVal, err := Get(obj, path) + if err != nil { + return false, err + } + + if objVal == nil { + return false, nil + } + + matchVal := []any{} + matchParts := strings.Split(matchStr, ",") + + for _, matchPart := range matchParts { + matchTmp, err := parse(matchPart, objVal) + if err != nil { + return false, err + } + + matchVal = append(matchVal, matchTmp) + } + + return anyTrue(matchVal, func(y any, i int) bool { + str := matchParts[i] + + if isSlice(objVal) { + return anyTrue(objVal, func(x any, _ int) bool { return cb(x, y, str) }) + } + + return cb(objVal, y, str) + }), nil +} diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..0eb6321 --- /dev/null +++ b/parse.go @@ -0,0 +1,153 @@ +package path + +import ( + "errors" + "reflect" + "strconv" + "strings" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/jsrest" +) + +type timeVal struct { + time time.Time + precision time.Duration +} + +var ( + ErrUnsupportedType = errors.New("unsupported type") + ErrUnknownTimeFormat = errors.New("unknown time format") +) + +func parse(str string, t any) (any, error) { + typ := reflect.TypeOf(t) + + if typ.Kind() == reflect.Slice { + typ = typ.Elem() + } + + if typ.Kind() == reflect.Pointer { + typ = typ.Elem() + } + + // TODO: Consider attempting to convert to string in default case + switch typ.Kind() { //nolint:exhaustive + case reflect.Int: + return parseInt(str) + + case reflect.Int64: + return strconv.ParseInt(str, 10, 64) + + case reflect.Uint: + return parseUint(str) + + case reflect.Uint64: + return strconv.ParseUint(str, 10, 64) + + case reflect.Float32: + return parseFloat32(str) + + case reflect.Float64: + return strconv.ParseFloat(str, 64) + + case reflect.String: + return str, nil + + case reflect.Bool: + return strconv.ParseBool(str) + + case reflect.Struct: + switch typ { + case reflect.TypeOf(time.Time{}): + return parseTime(str) + + case reflect.TypeOf(civil.Date{}): + return civil.ParseDate(str) + } + } + + return nil, jsrest.Errorf(jsrest.ErrBadRequest, "%T (%w)", t, ErrUnsupportedType) +} + +func parseInt(str string) (int, error) { + val, err := strconv.ParseInt(str, 10, strconv.IntSize) + + return int(val), err +} + +func parseUint(str string) (uint, error) { + val, err := strconv.ParseUint(str, 10, strconv.IntSize) + + return uint(val), err +} + +func parseFloat32(str string) (float32, error) { + val, err := strconv.ParseFloat(str, 32) + + return float32(val), err +} + +type timeFormat struct { + format string + precision time.Duration +} + +var timeFormats = []timeFormat{ + { + format: "2006-01-02-07:00", + precision: 24 * time.Hour, + // TODO: Support field annotation to change start vs end of day + // TODO: Support timezone context passed down to allow naked date + }, + { + format: "2006-01-02T15:04:05Z", + precision: 1 * time.Second, + }, + { + format: "2006-01-02T15:04:05-07:00", + precision: 1 * time.Second, + }, +} + +func parseTime(str string) (*timeVal, error) { + if strings.ToLower(str) == "now" { + return &timeVal{ + time: time.Now(), + precision: 1 * time.Nanosecond, + }, nil + } + + for _, format := range timeFormats { + tm, err := time.Parse(format.format, str) + if err != nil { + continue + } + + return &timeVal{ + time: tm, + precision: format.precision, + }, nil + } + + i, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return nil, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", str, ErrUnknownTimeFormat) + } + + // UNIX Seconds: 2969-05-03 + // UNIX Millis: 1971-01-01 + // Intended to give us a wide range of useful values in both schemes + if i > 31536000000 { + return &timeVal{ + time: time.UnixMilli(i), + precision: 1 * time.Millisecond, + }, nil + } + + return &timeVal{ + time: time.Unix(i, 0), + precision: 1 * time.Second, + }, nil +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..07f7f6e --- /dev/null +++ b/parse_test.go @@ -0,0 +1,42 @@ +//nolint:testpackage +package path + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestParseTimeNow(t *testing.T) { + t.Parallel() + + start := time.Now() + + now, err := parse("now", time.Time{}) + require.NoError(t, err) + + end := time.Now() + + require.WithinRange(t, now.(*timeVal).time, start, end) +} + +func TestParseTimeDate(t *testing.T) { + t.Parallel() + + parsed, err := parse("2022-11-01-08:00", time.Time{}) + require.NoError(t, err) + + tv := parsed.(*timeVal) + require.Equal(t, tv.precision, 24*time.Hour) +} + +func TestParseTimeSecond(t *testing.T) { + t.Parallel() + + parsed, err := parse("2022-11-01T05:06:07Z", time.Time{}) + require.NoError(t, err) + + tv := parsed.(*timeVal) + require.Equal(t, tv.precision, 1*time.Second) +} diff --git a/path.go b/path.go new file mode 100644 index 0000000..5599cf2 --- /dev/null +++ b/path.go @@ -0,0 +1,259 @@ +package path + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strings" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/jsrest" + "golang.org/x/exp/slices" +) + +type WalkCallback func(string, []string, reflect.StructField) + +var ( + TimeTimeType = reflect.TypeOf(time.Time{}) + CivilDateType = reflect.TypeOf(civil.Date{}) + + ErrNotAStruct = errors.New("not a struct") + ErrUnknownFieldName = errors.New("unknown field name") +) + +func Get(obj any, path string) (any, error) { + v, err := GetValue(reflect.ValueOf(obj), path) + if err != nil { + return nil, err + } + + return v.Interface(), nil +} + +func GetValue(v reflect.Value, path string) (reflect.Value, error) { + parts := strings.Split(path, ".") + return getRecursive(v, parts, []string{}) +} + +func getRecursive(v reflect.Value, parts []string, prev []string) (reflect.Value, error) { + if v.Kind() == reflect.Pointer { + if v.IsNil() { + v = reflect.Zero(v.Type().Elem()) + } else { + v = reflect.Indirect(v) + } + } + + if len(parts) == 0 { + return v, nil + } + + if v.Kind() != reflect.Struct { + return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct) + } + + part := parts[0] + + sub, found := getField(v, part) + if !found { + return reflect.Value{}, jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName) + } + + newPrev := []string{} + newPrev = append(newPrev, prev...) + newPrev = append(newPrev, part) + + return getRecursive(sub, parts[1:], newPrev) +} + +func getField(v reflect.Value, name string) (reflect.Value, bool) { + field, found := getStructField(v.Type(), name) + if !found { + return reflect.Value{}, false + } + + return v.FieldByName(field.Name), true +} + +func Set(obj any, path string, val string) error { + return SetValue(reflect.ValueOf(obj), path, val) +} + +func SetValue(v reflect.Value, path string, val string) error { + parts := strings.Split(path, ".") + return setRecursive(v, parts, []string{}, val) +} + +func setRecursive(v reflect.Value, parts []string, prev []string, val string) error { + if v.Kind() == reflect.Pointer { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + + v = reflect.Indirect(v) + } + + if len(parts) == 0 { + n, err := parse(val, v.Interface()) + if err != nil { + return err + } + + if _, ok := n.(*timeVal); ok { + n = n.(*timeVal).time + } + + v.Set(reflect.ValueOf(n)) + + return nil + } + + if v.Kind() != reflect.Struct { + return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", strings.Join(prev, "."), ErrNotAStruct) + } + + part := parts[0] + + sub, found := getField(v, part) + if !found { + return jsrest.Errorf(jsrest.ErrBadRequest, "%s (%w)", errorPath(prev, part), ErrUnknownFieldName) + } + + newPrev := []string{} + newPrev = append(newPrev, prev...) + newPrev = append(newPrev, part) + + return setRecursive(sub, parts[1:], newPrev, val) +} + +func List(obj any) []string { + return ListType(reflect.TypeOf(obj)) +} + +func ListType(t reflect.Type) []string { + list := []string{} + + WalkType(t, func(path string, _ []string, field reflect.StructField) { + t := MaybeIndirectType(field.Type) + if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType { + return + } + + list = append(list, path) + }) + + sort.Strings(list) + + return list +} + +func GetFieldType(t reflect.Type, path string) reflect.Type { + parts := strings.Split(path, ".") + + for _, part := range parts { + field, found := getStructField(MaybeIndirectType(t), part) + if !found { + return nil + } + + t = field.Type + } + + return t +} + +func FindTagValueType(t reflect.Type, key, value string) (string, bool) { + ret := "" + + WalkType(t, func(path string, _ []string, field reflect.StructField) { + tag, found := field.Tag.Lookup(key) + if !found { + return + } + + parts := strings.Split(tag, ",") + + if slices.Contains(parts, value) { + ret = path + } + }) + + return ret, ret != "" +} + +func Walk(obj any, cb WalkCallback) { + WalkType(reflect.TypeOf(obj), cb) +} + +func WalkType(t reflect.Type, cb WalkCallback) { + walkRecursive(MaybeIndirectType(t), cb, []string{}) +} + +func walkRecursive(t reflect.Type, cb WalkCallback, prev []string) { + for i := 0; i < t.NumField(); i++ { + sub := t.Field(i) + + newPrev := []string{} + newPrev = append(newPrev, prev...) + + if !sub.Anonymous { + newPrev = append(newPrev, FieldName(sub)) + } + + t := MaybeIndirectType(sub.Type) + + if len(newPrev) > 0 { + cb(strings.Join(newPrev, "."), newPrev, sub) + } + + if t.Kind() == reflect.Struct && t != TimeTimeType && t != CivilDateType { + walkRecursive(t, cb, newPrev) + } + } +} + +func getStructField(t reflect.Type, name string) (reflect.StructField, bool) { + name = strings.ToLower(name) + + return t.FieldByNameFunc(func(iterName string) bool { + iterField, iterOK := t.FieldByName(iterName) + if !iterOK { + panic(iterName) + } + + return strings.ToLower(FieldName(iterField)) == name + }) +} + +func errorPath(prev []string, part string) string { + if len(prev) == 0 { + return part + } + + return fmt.Sprintf("%s.%s", strings.Join(prev, "."), part) +} + +func FieldName(field reflect.StructField) string { + tag := field.Tag.Get("json") + if tag != "" { + if tag == "-" { + return "" + } + + parts := strings.SplitN(tag, ",", 2) + + return parts[0] + } + + return field.Name +} + +func MaybeIndirectType(t reflect.Type) reflect.Type { + if t.Kind() == reflect.Pointer { + return t.Elem() + } + + return t +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..fa8ad8b --- /dev/null +++ b/path_test.go @@ -0,0 +1,141 @@ +package path_test + +import ( + "reflect" + "testing" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +type testType1 struct { + Int int + Int64 int64 + UInt uint + UInt64 uint64 + Float32 float32 + Float64 float64 + String string `json:"string2,omitempty"` + Bool bool `json:"bool2"` + BoolP *bool + + Ints []int + Int64s []int64 + UInts []uint + UInt64s []uint64 + Float32s []float32 + Float64s []float64 + Strings []string + Bools []bool + + Time time.Time + Times []time.Time + Date civil.Date + Dates []civil.Date + + TimeP *time.Time + TimesP []*time.Time +} + +type testType2 struct { + Tt1 testType1 + Tt1p *testType1 +} + +type testType3 struct { + testType1 +} + +type testType4 struct { + Foo *testType5 `json:"foo"` + testType5 +} + +type testType5 struct { + String string `json:"string2,omitempty"` + Bool bool `json:"bool2"` + UInt uint `xtest:"foo,bar,zig"` +} + +func TestSet(t *testing.T) { + t.Parallel() + + tt1 := &testType1{} + err := path.Set(tt1, "int64", "1234") + require.NoError(t, err) + require.Equal(t, int64(1234), tt1.Int64) + + get, err := path.Get(tt1, "int64") + require.NoError(t, err) + require.Equal(t, int64(1234), get) + + err = path.Set(tt1, "time", "2022-11-01-08:00") + require.NoError(t, err) + require.Equal(t, int64(1667289600), tt1.Time.Unix()) + + tt2 := &testType2{} + err = path.Set(tt2, "tt1p.bool2", "true") + require.NoError(t, err) + require.Equal(t, true, tt2.Tt1p.Bool) + + err = path.Set(tt2, "tt1p.string2", "foo") + require.NoError(t, err) + require.Equal(t, "foo", tt2.Tt1p.String) + + err = path.Set(tt2, "tt1.boolp", "true") + require.NoError(t, err) + require.Equal(t, true, *tt2.Tt1.BoolP) +} + +func TestEmbed(t *testing.T) { + t.Parallel() + + tt3 := &testType3{} + err := path.Set(tt3, "int", "1234") + require.NoError(t, err) + require.Equal(t, 1234, tt3.Int) +} + +func TestList(t *testing.T) { + t.Parallel() + + list := path.List(&testType4{}) + require.Equal(t, []string{ + "UInt", + "bool2", + "foo.UInt", + "foo.bool2", + "foo.string2", + "string2", + }, list) +} + +func TestGetFieldType(t *testing.T) { + t.Parallel() + + typ := reflect.TypeOf(&testType4{}) + + typ2 := path.GetFieldType(typ, "bool2") + require.NotNil(t, typ2) + require.Equal(t, reflect.TypeOf(true), typ2) + + typ2 = path.GetFieldType(typ, "foo.UInt") + require.NotNil(t, typ2) + require.Equal(t, reflect.TypeOf(uint(1)), typ2) +} + +func TestFindTagValueType(t *testing.T) { + t.Parallel() + + typ := reflect.TypeOf(&testType4{}) + + p, ok := path.FindTagValueType(typ, "xtest", "foo") + require.True(t, ok) + require.Equal(t, "UInt", p) + + p, ok = path.FindTagValueType(typ, "xtest", "zag") + require.False(t, ok) + require.Empty(t, p) +} diff --git a/pkg_test.go b/pkg_test.go new file mode 100644 index 0000000..e678bad --- /dev/null +++ b/pkg_test.go @@ -0,0 +1,11 @@ +package path_test + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/slice.go b/slice.go new file mode 100644 index 0000000..f13424f --- /dev/null +++ b/slice.go @@ -0,0 +1,27 @@ +package path + +import "reflect" + +func isSlice(v any) bool { + return reflect.TypeOf(v).Kind() == reflect.Slice +} + +func anyTrue(v any, cb func(any, int) bool) bool { + val := reflect.ValueOf(v) + + for i := 0; i < val.Len(); i++ { + sub := val.Index(i) + + if sub.Kind() == reflect.Pointer && sub.IsNil() { + continue + } + + sub = reflect.Indirect(sub) + + if cb(sub.Interface(), i) { + return true + } + } + + return false +} diff --git a/sort.go b/sort.go new file mode 100644 index 0000000..5715247 --- /dev/null +++ b/sort.go @@ -0,0 +1,110 @@ +package path + +import ( + "errors" + "reflect" + "sort" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/jsrest" +) + +func Sort(objs any, path string) error { + as := newAnySlice(objs, path) + sort.Stable(as) + + return as.err +} + +func SortReverse(objs any, path string) error { + as := newAnySlice(objs, path) + sort.Stable(sort.Reverse(as)) + + return as.err +} + +type anySlice struct { + path string + slice reflect.Value + swapper func(i, j int) + err error +} + +var ErrUnsupportedSortType = errors.New("unsupported _sort type") + +func newAnySlice(objs any, path string) *anySlice { + return &anySlice{ + path: path, + slice: reflect.ValueOf(objs), + swapper: reflect.Swapper(objs), + } +} + +func (as *anySlice) Len() int { + return as.slice.Len() +} + +func (as *anySlice) Less(i, j int) bool { + v1, err := Get(as.slice.Index(i).Interface(), as.path) + if err != nil { + as.err = err + // We have to obey the Less() contract even in error cases + return i < j + } + + v2, err := Get(as.slice.Index(j).Interface(), as.path) + if err != nil { + as.err = err + return i < j + } + + switch { + case v1 == nil && v2 == nil: + return false + case v1 == nil: + return true + case v2 == nil: + return false + } + + switch t1 := v1.(type) { + case int: + return t1 < v2.(int) + + case int64: + return t1 < v2.(int64) + + case uint: + return t1 < v2.(uint) + + case uint64: + return t1 < v2.(uint64) + + case float32: + return t1 < v2.(float32) + + case float64: + return t1 < v2.(float64) + + case string: + return t1 < v2.(string) + + case bool: + return !t1 && v2.(bool) + + case time.Time: + return t1.Before(v2.(time.Time)) + + case civil.Date: + return t1.Before(v2.(civil.Date)) + + default: + as.err = jsrest.Errorf(jsrest.ErrBadRequest, "%s: %T (%w)", as.path, t1, ErrUnsupportedSortType) + return i < j + } +} + +func (as *anySlice) Swap(i, j int) { + as.swapper(i, j) +} diff --git a/sort_test.go b/sort_test.go new file mode 100644 index 0000000..73f6ca0 --- /dev/null +++ b/sort_test.go @@ -0,0 +1,282 @@ +package path_test + +import ( + "testing" + "time" + + "cloud.google.com/go/civil" + "github.com/gopatchy/path" + "github.com/stretchr/testify/require" +) + +func TestSortStruct(t *testing.T) { + t.Parallel() + + objs := []*testType2{ + { + Tt1: testType1{ + Int: 2, + }, + }, + { + Tt1: testType1{ + Int: 1, + }, + }, + { + Tt1: testType1{ + Int: 3, + }, + }, + } + + err := path.Sort(objs, "tt1.int") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []int{1, 2, 3}, []int{objs[0].Tt1.Int, objs[1].Tt1.Int, objs[2].Tt1.Int}) +} + +func TestSortReverse(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + Int: 3, + }, + { + Int: 1, + }, + { + Int: 2, + }, + } + + err := path.SortReverse(objs, "int") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []int{3, 2, 1}, []int{objs[0].Int, objs[1].Int, objs[2].Int}) +} + +func TestSortInt(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + Int: 3, + }, + { + Int: 1, + }, + { + Int: 2, + }, + } + + err := path.Sort(objs, "int") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []int{1, 2, 3}, []int{objs[0].Int, objs[1].Int, objs[2].Int}) +} + +func TestSortInt64(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + Int64: 3, + }, + { + Int64: 1, + }, + { + Int64: 2, + }, + } + + err := path.Sort(objs, "int64") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []int64{1, 2, 3}, []int64{objs[0].Int64, objs[1].Int64, objs[2].Int64}) +} + +func TestSortUint(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + UInt: 3, + }, + { + UInt: 1, + }, + { + UInt: 2, + }, + } + + err := path.Sort(objs, "uint") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []uint{1, 2, 3}, []uint{objs[0].UInt, objs[1].UInt, objs[2].UInt}) +} + +func TestSortUint64(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + UInt64: 3, + }, + { + UInt64: 1, + }, + { + UInt64: 2, + }, + } + + err := path.Sort(objs, "uint64") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []uint64{1, 2, 3}, []uint64{objs[0].UInt64, objs[1].UInt64, objs[2].UInt64}) +} + +func TestSortFloat32(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + Float32: 3.3, + }, + { + Float32: 1.1, + }, + { + Float32: 2.2, + }, + } + + err := path.Sort(objs, "float32") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []float32{1.1, 2.2, 3.3}, []float32{objs[0].Float32, objs[1].Float32, objs[2].Float32}) +} + +func TestSortFloat64(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + Float64: 3.3, + }, + { + Float64: 1.1, + }, + { + Float64: 2.2, + }, + } + + err := path.Sort(objs, "float64") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []float64{1.1, 2.2, 3.3}, []float64{objs[0].Float64, objs[1].Float64, objs[2].Float64}) +} + +func TestSortString(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + String: "zig", + }, + { + String: "bar", + }, + { + String: "foo", + }, + } + + err := path.Sort(objs, "string2") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []string{"bar", "foo", "zig"}, []string{objs[0].String, objs[1].String, objs[2].String}) +} + +func TestSortBool(t *testing.T) { + t.Parallel() + + objs := []*testType1{ + { + Bool: true, + }, + { + Bool: false, + }, + { + Bool: true, + }, + } + + err := path.Sort(objs, "bool2") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []bool{false, true, true}, []bool{objs[0].Bool, objs[1].Bool, objs[2].Bool}) +} + +func TestSortTime(t *testing.T) { + t.Parallel() + + t1, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-01T15:04:05Z") + require.NoError(t, err) + t2, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-02T15:04:05Z") + require.NoError(t, err) + t3, err := time.Parse("2006-01-02T15:04:05Z", "2006-01-03T15:04:05Z") + require.NoError(t, err) + + objs := []*testType1{ + { + Time: t3, + }, + { + Time: t1, + }, + { + Time: t2, + }, + } + + err = path.Sort(objs, "time") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []time.Time{t1, t2, t3}, []time.Time{objs[0].Time, objs[1].Time, objs[2].Time}) +} + +func TestSortDate(t *testing.T) { + t.Parallel() + + d1, err := civil.ParseDate("2006-01-01") + require.NoError(t, err) + d2, err := civil.ParseDate("2006-01-02") + require.NoError(t, err) + d3, err := civil.ParseDate("2006-01-03") + require.NoError(t, err) + + objs := []*testType1{ + { + Date: d3, + }, + { + Date: d1, + }, + { + Date: d2, + }, + } + + err = path.Sort(objs, "date") + require.NoError(t, err) + require.Len(t, objs, 3) + require.Equal(t, []civil.Date{d1, d2, d3}, []civil.Date{objs[0].Date, objs[1].Date, objs[2].Date}) +}