From 832bfc12882086635ae5dcd0b4229538b5c4fd4c Mon Sep 17 00:00:00 2001 From: Ariel Alba Date: Thu, 14 Jul 2022 16:17:42 -0400 Subject: [PATCH] feat(core): refactor --- .golangci.yml | 7 ++- agtime/time.go | 7 +++ cache/redis/redis.go | 57 ++++++++++++++++++++++++ cache/service.go | 12 +++++ group_by.go => groupby/group_by.go | 45 +++++++++---------- handlers/handler.go | 14 ------ log/log.go | 2 +- redis/redis.go | 31 ------------- security/hash.go | 18 ++++++++ sql/{ => db}/pgu.go | 8 ++-- sql/db/service.go | 43 ++++++++++++++++++ sql/{query_reader.go => query/reader.go} | 22 ++++----- sql/service.go | 38 ---------------- sql/{simple_types.go => type.go} | 6 +-- {types => typing}/type.go | 6 ++- web/handler.go | 14 ++++++ 16 files changed, 197 insertions(+), 133 deletions(-) create mode 100644 cache/redis/redis.go create mode 100644 cache/service.go rename group_by.go => groupby/group_by.go (75%) delete mode 100644 handlers/handler.go delete mode 100644 redis/redis.go create mode 100644 security/hash.go rename sql/{ => db}/pgu.go (96%) create mode 100644 sql/db/service.go rename sql/{query_reader.go => query/reader.go} (67%) delete mode 100644 sql/service.go rename sql/{simple_types.go => type.go} (93%) rename {types => typing}/type.go (58%) create mode 100644 web/handler.go diff --git a/.golangci.yml b/.golangci.yml index a014048..cefa8c8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -39,7 +39,6 @@ linters-settings: gocritic: disabled-checks: - unnamedResult - - hugeParam - whyNoLint enabled-tags: - performance @@ -53,7 +52,7 @@ linters: enable-all: false disable-all: true enable: - # - lll + - lll - misspell - goconst - gochecknoinits @@ -68,14 +67,14 @@ linters: - gofumpt - gocritic - vet - # - revive + - revive - bodyclose - deadcode - errcheck - gosec - structcheck - unconvert - # - dupl + - dupl - varcheck - unparam - staticcheck diff --git a/agtime/time.go b/agtime/time.go index ce043aa..695057b 100644 --- a/agtime/time.go +++ b/agtime/time.go @@ -120,10 +120,12 @@ func (t *NullableDate) UnmarshalJSON(data []byte) error { return nil } +// TruncateMonth returns a nullable date with truncated month func (t *NullableDate) TruncateMonth() NullableDate { return NewNullDate(TruncateMonth(t.Time)) } +// After checks if nullable date `t` is after time `a` func (t *NullableDate) After(a time.Time) bool { if t.Valid { return t.Time.After(a) @@ -131,6 +133,7 @@ func (t *NullableDate) After(a time.Time) bool { return false } +// Before checks if nullable date `t` is before time `a` func (t *NullableDate) Before(a time.Time) bool { if t.Valid { return t.Time.Before(a) @@ -138,6 +141,7 @@ func (t *NullableDate) Before(a time.Time) bool { return false } +// Equal checks if nullable date `t` is equal time `a` func (t *NullableDate) Equal(a time.Time) bool { if t.Valid { return t.Time.Equal(a) @@ -145,6 +149,7 @@ func (t *NullableDate) Equal(a time.Time) bool { return false } +// AftEq checks if nullable date `t` is after or equal the time `a` func (t *NullableDate) AftEq(a time.Time) bool { if t.Valid { return t.After(a) || t.Equal(a) @@ -152,6 +157,7 @@ func (t *NullableDate) AftEq(a time.Time) bool { return false } +// BfEq checks if nullable date `t` is before or equal the time `a` func (t *NullableDate) BfEq(a time.Time) bool { if t.Valid { return t.Before(a) || t.Equal(a) @@ -159,6 +165,7 @@ func (t *NullableDate) BfEq(a time.Time) bool { return false } +// Between checks if nullable date `t` is between the times `a` and `b` func (t *NullableDate) Between(a, b time.Time) bool { return t.AftEq(a) && t.BfEq(b) } diff --git a/cache/redis/redis.go b/cache/redis/redis.go new file mode 100644 index 0000000..b4668f0 --- /dev/null +++ b/cache/redis/redis.go @@ -0,0 +1,57 @@ +package redis + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" + + "github.com/agflow/tools/log" +) + +// Client is a wrapper of a redis client +type Client struct { + Redis *redis.Client +} + +// NewClient returns a verified redis client +func NewClient(address, password string) *redis.Client { + rdb := redis.NewClient(&redis.Options{ + Addr: address, + Password: password, + DB: 0, // use default DB + }) + if rdb == nil { + log.Warnf("failed to initialize redis client") + return rdb + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + if err := rdb.Ping(ctx).Err(); err != nil { + log.Warnf("can't ping redis, %v", err) + } + return rdb +} + +// New returns a new redis client +func New(address, password string) *Client { + redisClient := NewClient(address, password) + return &Client{Redis: redisClient} +} + +// Get gets the value stored on `key` +func (c *Client) Get(ctx context.Context, key string) ([]byte, error) { + return c.Redis.Get(ctx, key).Bytes() +} + +// Set sets `value` on `key` with a `timeout`` +func (c *Client) Set( + ctx context.Context, + key string, + value interface{}, + timeout time.Duration, +) error { + return c.Redis.Set(ctx, key, value, timeout).Err() +} diff --git a/cache/service.go b/cache/service.go new file mode 100644 index 0000000..a8d3104 --- /dev/null +++ b/cache/service.go @@ -0,0 +1,12 @@ +package cache + +import ( + "context" + "time" +) + +// Service declares the interface of a cache service +type Service interface { + Get(context.Context, string) ([]byte, error) + Set(context.Context, string, interface{}, time.Duration) error +} diff --git a/group_by.go b/groupby/group_by.go similarity index 75% rename from group_by.go rename to groupby/group_by.go index a91810f..50dd552 100644 --- a/group_by.go +++ b/groupby/group_by.go @@ -1,42 +1,31 @@ -package main +package groupby import ( - "crypto/sha512" - "encoding/json" "errors" - "fmt" "reflect" "sync" "time" "github.com/agflow/tools/log" - "github.com/agflow/tools/types" + "github.com/agflow/tools/security" + "github.com/agflow/tools/typing" ) -func Hash(vs interface{}) (string, error) { - h := sha512.New() - r, err := json.Marshal(vs) - if err != nil { - return "", err - } - h.Write(r) - return fmt.Sprintf("%x", h.Sum(nil)), nil -} - func getGroupHash(v reflect.Value, cols []string) (string, error) { grouped := make([]interface{}, len(cols)) for j := range cols { grouped[j] = v.FieldByName(cols[j]).Interface() } - return Hash(grouped) + return security.Hash(grouped) } +// AggrFunc is an aggregation function type AggrFunc func([]interface{}) interface{} var wg sync.WaitGroup //nolint: gochecknoglobals -// GroupBy groups a slice of structs with an aggregation function -func GroupBy(on, dest interface{}, cols []string, funcs map[string]AggrFunc) error { //nolint: deadcode +// Agg groups by a slice of structs with an aggregation function +func Agg(on, dest interface{}, cols []string, funcs map[string]AggrFunc) error { groupMap := make(map[interface{}]chan interface{}) if reflect.TypeOf(on).Kind() != reflect.Slice { return errors.New("on needs to be slice") @@ -45,12 +34,12 @@ func GroupBy(on, dest interface{}, cols []string, funcs map[string]AggrFunc) err destVal := reflect.ValueOf(dest) direct := reflect.Indirect(destVal) - sliceType, err := types.BaseType(destVal.Type(), reflect.Slice) + sliceType, err := typing.Base(destVal.Type(), reflect.Slice) if err != nil { return err } - baseType := types.DeRef(sliceType.Elem()) + baseType := typing.DeRef(sliceType.Elem()) s := reflect.ValueOf(on) finishChan := make(chan bool) @@ -94,7 +83,12 @@ func GroupBy(on, dest interface{}, cols []string, funcs map[string]AggrFunc) err return nil } -func processFun(v chan interface{}, funcs map[string]AggrFunc, dest reflect.Value, finish chan bool) { +func processFun( + v chan interface{}, + funcs map[string]AggrFunc, + dest reflect.Value, + finish chan bool, +) { defer wg.Done() var isFinished bool grouped := make([]interface{}, 0) @@ -114,10 +108,11 @@ func processFun(v chan interface{}, funcs map[string]AggrFunc, dest reflect.Valu } } +// FoldFunc is a fold function type FoldFunc func(interface{}, interface{}) interface{} -// GroupByFold groups a slice of structs with a fold function -func GroupByFold(on, dest interface{}, cols []string, funcs map[string]FoldFunc) error { //nolint: deadcode +// Fold groups by a slice of structs with a fold function +func Fold(on, dest interface{}, cols []string, funcs map[string]FoldFunc) error { groupMap := make(map[interface{}]int) if reflect.TypeOf(on).Kind() != reflect.Slice { return errors.New("on needs to be slice") @@ -126,12 +121,12 @@ func GroupByFold(on, dest interface{}, cols []string, funcs map[string]FoldFunc) destVal := reflect.ValueOf(dest) direct := reflect.Indirect(destVal) - sliceType, err := types.BaseType(destVal.Type(), reflect.Slice) + sliceType, err := typing.Base(destVal.Type(), reflect.Slice) if err != nil { return err } - baseType := types.DeRef(sliceType.Elem()) + baseType := typing.DeRef(sliceType.Elem()) s := reflect.ValueOf(on) for i := 0; i < s.Len(); i++ { diff --git a/handlers/handler.go b/handlers/handler.go deleted file mode 100644 index 01d3d09..0000000 --- a/handlers/handler.go +++ /dev/null @@ -1,14 +0,0 @@ -package handler - -import ( - "github.com/go-redis/redis/v8" - - "github.com/agflow/tools/agtime" - "github.com/agflow/tools/sql" -) - -type Handlers struct { - T agtime.ClockTime - DBSvc *sql.DBService - CacheSvc *redis.Client -} diff --git a/log/log.go b/log/log.go index 99c7c07..4ee544c 100644 --- a/log/log.go +++ b/log/log.go @@ -16,7 +16,7 @@ const ( timeFormat = "2006/01/02 15:04:05" ) -//nolint: gochecknoinits +// nolint: gochecknoinits func init() { log.SetFlags(0) } diff --git a/redis/redis.go b/redis/redis.go deleted file mode 100644 index 19df787..0000000 --- a/redis/redis.go +++ /dev/null @@ -1,31 +0,0 @@ -package redis - -import ( - "context" - "time" - - "github.com/go-redis/redis/v8" - - "github.com/agflow/tools/log" -) - -// NewClient returns a verified redis client -func NewClient(address, password string) *redis.Client { - rdb := redis.NewClient(&redis.Options{ - Addr: address, - Password: password, - DB: 0, // use default DB - }) - if rdb == nil { - log.Warnf("failed to initialize redis client") - return rdb - } - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - if err := rdb.Ping(ctx).Err(); err != nil { - log.Warnf("can't ping redis, %v", err) - } - return rdb -} diff --git a/security/hash.go b/security/hash.go new file mode 100644 index 0000000..00d6d9f --- /dev/null +++ b/security/hash.go @@ -0,0 +1,18 @@ +package security + +import ( + "crypto/sha512" + "encoding/json" + "fmt" +) + +// Hash hashes `vs` +func Hash(vs interface{}) (string, error) { + h := sha512.New() + r, err := json.Marshal(vs) + if err != nil { + return "", err + } + h.Write(r) + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/sql/pgu.go b/sql/db/pgu.go similarity index 96% rename from sql/pgu.go rename to sql/db/pgu.go index 9eb0fbb..200e8a4 100644 --- a/sql/pgu.go +++ b/sql/db/pgu.go @@ -1,4 +1,4 @@ -package sql +package db import ( "context" @@ -8,7 +8,7 @@ import ( "reflect" "github.com/agflow/tools/log" - "github.com/agflow/tools/types" + "github.com/agflow/tools/typing" ) // Select runs query on database with arguments and saves result on dest variable @@ -112,13 +112,13 @@ func scanAll(rows *sql.Rows, dest interface{}, structOnly bool) error { } direct := reflect.Indirect(value) - slice, err := types.BaseType(value.Type(), reflect.Slice) + slice, err := typing.Base(value.Type(), reflect.Slice) if err != nil { return err } isPtr := slice.Elem().Kind() == reflect.Ptr - base := types.DeRef(slice.Elem()) + base := typing.DeRef(slice.Elem()) scannable := isScannable(base) if structOnly { diff --git a/sql/db/service.go b/sql/db/service.go new file mode 100644 index 0000000..45b4f39 --- /dev/null +++ b/sql/db/service.go @@ -0,0 +1,43 @@ +package db + +import ( + "database/sql" + "log" +) + +// Client is a wrapper of a sql.DB client +type Client struct { + DB *sql.DB +} + +// Service is an interface od db.Service +type Service interface { + Select(interface{}, string, ...interface{}) error +} + +// Select selects from `client`` using the `query` and `args` +func (c *Client) Select(dest interface{}, query string, args ...interface{}) error { + return Select(c.DB, dest, query, args...) +} + +// New return a new db.Client +func New(url string) (*Client, error) { + db, err := sql.Open("postgres", url) + if err != nil { + return nil, err + } + // check that the connection is valid + if err := db.Ping(); err != nil { + return nil, err + } + return &Client{DB: db}, nil +} + +// MustNew return a new db.Client without an error +func MustNew(url string) *Client { + dbSvc, err := New(url) + if err != nil { + log.Fatal(err) + } + return dbSvc +} diff --git a/sql/query_reader.go b/sql/query/reader.go similarity index 67% rename from sql/query_reader.go rename to sql/query/reader.go index f23dc61..8b5b36a 100644 --- a/sql/query_reader.go +++ b/sql/query/reader.go @@ -1,4 +1,4 @@ -package sql +package query import ( "embed" @@ -12,10 +12,10 @@ import ( "github.com/agflow/tools/log" ) -// mustReadQueries is an utility method for MustReadSQL for sql files in queries subdirectory +// mustRead is an utility method for MustReadSQL for sql files in queries subdirectory // Reads given folder and expects a SQL file named for each field of destination. // Then sets the content of file into that field. -func mustReadQueries(v reflect.Value, queriesFS embed.FS, subfolder string, base ...string) { +func mustRead(v reflect.Value, queriesFS embed.FS, subfolder string, base ...string) { dir := subfolder if len(base) > 0 { dir = filepath.Join(base[0], subfolder) @@ -39,10 +39,10 @@ func mustReadQueries(v reflect.Value, queriesFS embed.FS, subfolder string, base for i := 0; i < v.NumField(); i++ { names[i] = v.Type().Field(i).Name } - setQueries(v, queriesFS, dir) + set(v, queriesFS, dir) } -func setQueries(v reflect.Value, queriesFS embed.FS, dir string) { +func set(v reflect.Value, queriesFS embed.FS, dir string) { for i := 0; i < v.NumField(); i++ { field := v.Type().Field(i) f := filepath.Join(dir, field.Name+".sql") @@ -54,25 +54,25 @@ func setQueries(v reflect.Value, queriesFS embed.FS, dir string) { } } -// mustReadSQLFiles fills queries into Query variable. -func mustReadSQLFiles(root string, queriesFS embed.FS, query interface{}) { +// mustReadFiles fills queries into Query variable. +func mustReadFiles(root string, queriesFS embed.FS, query interface{}) { q := reflect.ValueOf(query).Elem() v := reflect.Indirect(reflect.ValueOf(query)) for i := 0; i < v.NumField(); i++ { f := v.Type().Field(i) - mustReadQueries(q.FieldByName(f.Name), queriesFS, + mustRead(q.FieldByName(f.Name), queriesFS, filepath.Join(root, f.Tag.Get("sql"))) } } -// MustLoadSQLQueries enables pgu to use queries +// MustLoad loads queries // that are located in the default sql directory -func MustLoadSQLQueries(queriesFS embed.FS, query interface{}) { +func MustLoad(queriesFS embed.FS, query interface{}) { f, err := fs.ReadDir(queriesFS, ".") if err != nil { log.Fatal(err) } for _, r := range f { - mustReadSQLFiles(r.Name(), queriesFS, query) + mustReadFiles(r.Name(), queriesFS, query) } } diff --git a/sql/service.go b/sql/service.go deleted file mode 100644 index 06469a2..0000000 --- a/sql/service.go +++ /dev/null @@ -1,38 +0,0 @@ -package sql - -import ( - "database/sql" - "log" -) - -type DBService struct { - DB *sql.DB -} - -type API interface { - Select(interface{}, string, ...interface{}) error -} - -func (db *DBService) Select(dest interface{}, query string, args ...interface{}) error { - return Select(db.DB, dest, query, args...) -} - -func New(url string) (*DBService, error) { - db, err := sql.Open("postgres", url) - if err != nil { - return nil, err - } - // check that the connection is valid - if err := db.Ping(); err != nil { - return nil, err - } - return &DBService{DB: db}, nil -} - -func MustNew(url string) *DBService { - dbSvc, err := New(url) - if err != nil { - log.Fatal(err) - } - return dbSvc -} diff --git a/sql/simple_types.go b/sql/type.go similarity index 93% rename from sql/simple_types.go rename to sql/type.go index eae8621..66ef7d8 100644 --- a/sql/simple_types.go +++ b/sql/type.go @@ -26,7 +26,7 @@ func (v NullableString) MarshalJSON() ([]byte, error) { } // UnmarshalJSON unmarshals NullableString -func (v *NullableString) UnmarshalJSON(data []byte) error { +func (v *NullableString) UnmarshalJSON(data []byte) error { // nolint: dupl // Unmarshalling into a pointer will let us detect null var x *string if err := json.Unmarshal(data, &x); err != nil { @@ -69,7 +69,7 @@ func (v NullableInt) MarshalJSON() ([]byte, error) { } // UnmarshalJSON unmarshal NullableInt -func (v *NullableInt) UnmarshalJSON(data []byte) error { +func (v *NullableInt) UnmarshalJSON(data []byte) error { // nolint: dupl // Unmarshalling into a pointer will let us detect null var x *int64 if err := json.Unmarshal(data, &x); err != nil { @@ -107,7 +107,7 @@ func (v NullableFloat) MarshalJSON() ([]byte, error) { } // UnmarshalJSON unmarshal NullableFloat -func (v *NullableFloat) UnmarshalJSON(data []byte) error { +func (v *NullableFloat) UnmarshalJSON(data []byte) error { // nolint: dupl // Unmarshalling into a pointer will let us detect null var x *float64 if err := json.Unmarshal(data, &x); err != nil { diff --git a/types/type.go b/typing/type.go similarity index 58% rename from types/type.go rename to typing/type.go index c270913..9bf9018 100644 --- a/types/type.go +++ b/typing/type.go @@ -1,11 +1,12 @@ -package types +package typing import ( "fmt" "reflect" ) -func BaseType(t reflect.Type, expected reflect.Kind) (reflect.Type, error) { +// Base gets base type of `t` and compares to `expected` +func Base(t reflect.Type, expected reflect.Kind) (reflect.Type, error) { t = DeRef(t) if t.Kind() != expected { return nil, fmt.Errorf("expected %s but got %s", expected, t.Kind()) @@ -13,6 +14,7 @@ func BaseType(t reflect.Type, expected reflect.Kind) (reflect.Type, error) { return t, nil } +// DeRef derefences `t` to get its type func DeRef(t reflect.Type) reflect.Type { if t.Kind() == reflect.Ptr { t = t.Elem() diff --git a/web/handler.go b/web/handler.go new file mode 100644 index 0000000..3fdcae0 --- /dev/null +++ b/web/handler.go @@ -0,0 +1,14 @@ +package web + +import ( + "github.com/agflow/tools/agtime" + "github.com/agflow/tools/cache" + "github.com/agflow/tools/sql/db" +) + +// Handler is a struct that packs all the necessary services for a web handler +type Handler struct { + T agtime.ClockTime + DBSvc *db.Service + CacheSvc *cache.Service +}