diff --git a/api/application/rows.go b/api/application/rows.go deleted file mode 100644 index 98017c6..0000000 --- a/api/application/rows.go +++ /dev/null @@ -1,44 +0,0 @@ -package application - -import ( - "context" - - "cloud.google.com/go/bigtable" - "github.com/takashabe/btcli/api/domain" - "github.com/takashabe/btcli/api/domain/repository" -) - -// RowsInteractor provide rows data -type RowsInteractor struct { - repository repository.Bigtable -} - -// NewRowsInteractor returns initialized RowsInteractor -func NewRowsInteractor(r repository.Bigtable) *RowsInteractor { - return &RowsInteractor{ - repository: r, - } -} - -// GetRow returns a single row -func (t *RowsInteractor) GetRow(ctx context.Context, table, key string, opts ...bigtable.ReadOption) (*domain.Row, error) { - tbl, err := t.repository.Get(ctx, table, key, opts...) - if err != nil { - return nil, err - } - return tbl.Rows[0], nil -} - -// GetRows returns rows -func (t *RowsInteractor) GetRows(ctx context.Context, table string, rr bigtable.RowRange, opts ...bigtable.ReadOption) ([]*domain.Row, error) { - tbl, err := t.repository.GetRows(ctx, table, rr, opts...) - if err != nil { - return nil, err - } - return tbl.Rows, nil -} - -// GetRowCount returns number of the table -func (t *RowsInteractor) GetRowCount(ctx context.Context, table string) (int, error) { - return t.repository.Count(ctx, table) -} diff --git a/api/application/tables.go b/api/application/tables.go deleted file mode 100644 index 724d8a8..0000000 --- a/api/application/tables.go +++ /dev/null @@ -1,24 +0,0 @@ -package application - -import ( - "context" - - "github.com/takashabe/btcli/api/domain/repository" -) - -// TableInteractor provide table data -type TableInteractor struct { - repository repository.Bigtable -} - -// NewTableInteractor returns initialized TableInteractor -func NewTableInteractor(r repository.Bigtable) *TableInteractor { - return &TableInteractor{ - repository: r, - } -} - -// GetTables returns list table -func (t *TableInteractor) GetTables(ctx context.Context) ([]string, error) { - return t.repository.Tables(ctx) -} diff --git a/api/domain/bigtable.go b/api/domain/bigtable.go deleted file mode 100644 index ac1490e..0000000 --- a/api/domain/bigtable.go +++ /dev/null @@ -1,23 +0,0 @@ -package domain - -import "time" - -// Bigtable entity of the bigtable -type Bigtable struct { - Table string - Rows []*Row -} - -// Row represent a row of the table -type Row struct { - Key string - Columns []*Column -} - -// Column represent a column of the row -type Column struct { - Family string - Qualifier string - Value []byte - Version time.Time -} diff --git a/api/domain/repository/bigtable.go b/api/domain/repository/bigtable.go deleted file mode 100644 index 6b9de5b..0000000 --- a/api/domain/repository/bigtable.go +++ /dev/null @@ -1,20 +0,0 @@ -package repository - -//go:generate mockgen --package=repository -source=bigtable.go -destination=bigtable_mock.go - -import ( - "context" - - "cloud.google.com/go/bigtable" - "github.com/takashabe/btcli/api/domain" -) - -// Bigtable represent repository of the bigtable -type Bigtable interface { - Get(ctx context.Context, table, key string, opts ...bigtable.ReadOption) (*domain.Bigtable, error) - GetRows(ctx context.Context, table string, rr bigtable.RowRange, opts ...bigtable.ReadOption) (*domain.Bigtable, error) - Count(ctx context.Context, table string) (int, error) - - // TODO: Isolation data management client and table management client - Tables(ctx context.Context) ([]string, error) -} diff --git a/api/domain/repository/bigtable_mock.go b/api/domain/repository/bigtable_mock.go deleted file mode 100644 index a608d79..0000000 --- a/api/domain/repository/bigtable_mock.go +++ /dev/null @@ -1,116 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: bigtable.go - -// Package repository is a generated GoMock package. -package repository - -import ( - bigtable "cloud.google.com/go/bigtable" - context "context" - gomock "github.com/golang/mock/gomock" - domain "github.com/takashabe/btcli/api/domain" - reflect "reflect" -) - -// MockBigtable is a mock of Bigtable interface -type MockBigtable struct { - ctrl *gomock.Controller - recorder *MockBigtableMockRecorder -} - -// MockBigtableMockRecorder is the mock recorder for MockBigtable -type MockBigtableMockRecorder struct { - mock *MockBigtable -} - -// NewMockBigtable creates a new mock instance -func NewMockBigtable(ctrl *gomock.Controller) *MockBigtable { - mock := &MockBigtable{ctrl: ctrl} - mock.recorder = &MockBigtableMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockBigtable) EXPECT() *MockBigtableMockRecorder { - return m.recorder -} - -// Get mocks base method -func (m *MockBigtable) Get(ctx context.Context, table, key string, opts ...bigtable.ReadOption) (*domain.Bigtable, error) { - varargs := []interface{}{ctx, table, key} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Get", varargs...) - ret0, _ := ret[0].(*domain.Bigtable) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get -func (mr *MockBigtableMockRecorder) Get(ctx, table, key interface{}, opts ...interface{}) *gomock.Call { - varargs := append([]interface{}{ctx, table, key}, opts...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockBigtable)(nil).Get), varargs...) -} - -// GetRows mocks base method -func (m *MockBigtable) GetRows(ctx context.Context, table string, rr bigtable.RowRange, opts ...bigtable.ReadOption) (*domain.Bigtable, error) { - varargs := []interface{}{ctx, table, rr} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetRows", varargs...) - ret0, _ := ret[0].(*domain.Bigtable) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRows indicates an expected call of GetRows -func (mr *MockBigtableMockRecorder) GetRows(ctx, table, rr interface{}, opts ...interface{}) *gomock.Call { - varargs := append([]interface{}{ctx, table, rr}, opts...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRows", reflect.TypeOf((*MockBigtable)(nil).GetRows), varargs...) -} - -// GetRowsWithPrefix mocks base method -func (m *MockBigtable) GetRowsWithPrefix(ctx context.Context, table, key string, opts ...bigtable.ReadOption) (*domain.Bigtable, error) { - varargs := []interface{}{ctx, table, key} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetRowsWithPrefix", varargs...) - ret0, _ := ret[0].(*domain.Bigtable) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRowsWithPrefix indicates an expected call of GetRowsWithPrefix -func (mr *MockBigtableMockRecorder) GetRowsWithPrefix(ctx, table, key interface{}, opts ...interface{}) *gomock.Call { - varargs := append([]interface{}{ctx, table, key}, opts...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRowsWithPrefix", reflect.TypeOf((*MockBigtable)(nil).GetRowsWithPrefix), varargs...) -} - -// Count mocks base method -func (m *MockBigtable) Count(ctx context.Context, table string) (int, error) { - ret := m.ctrl.Call(m, "Count", ctx, table) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Count indicates an expected call of Count -func (mr *MockBigtableMockRecorder) Count(ctx, table interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockBigtable)(nil).Count), ctx, table) -} - -// Tables mocks base method -func (m *MockBigtable) Tables(ctx context.Context) ([]string, error) { - ret := m.ctrl.Call(m, "Tables", ctx) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Tables indicates an expected call of Tables -func (mr *MockBigtableMockRecorder) Tables(ctx interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tables", reflect.TypeOf((*MockBigtable)(nil).Tables), ctx) -} diff --git a/api/infrastructure/bigtable/bigtable.go b/api/infrastructure/bigtable/bigtable.go deleted file mode 100644 index 08022d9..0000000 --- a/api/infrastructure/bigtable/bigtable.go +++ /dev/null @@ -1,127 +0,0 @@ -package bigtable - -import ( - "context" - "sort" - "time" - - "cloud.google.com/go/bigtable" - "github.com/takashabe/btcli/api/domain" - "github.com/takashabe/btcli/api/domain/repository" -) - -type bigtableRepository struct { - client *bigtable.Client - adminClient *bigtable.AdminClient -} - -// NewBigtableRepository returns initialized bigtableRepository -func NewBigtableRepository(project, instance string) (repository.Bigtable, error) { - client, err := getClient(project, instance) - if err != nil { - return nil, err - } - adminClient, err := getAdminClient(project, instance) - if err != nil { - return nil, err - } - return &bigtableRepository{ - client: client, - adminClient: adminClient, - }, nil -} - -func getClient(project, instance string) (*bigtable.Client, error) { - // TODO: Support options - return bigtable.NewClient(context.Background(), project, instance) -} - -func getAdminClient(project, instance string) (*bigtable.AdminClient, error) { - // TODO: Support options - return bigtable.NewAdminClient(context.Background(), project, instance) -} - -func (b *bigtableRepository) Get(ctx context.Context, table, key string, opts ...bigtable.ReadOption) (*domain.Bigtable, error) { - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - - tbl := b.client.Open(table) - row, err := tbl.ReadRow(ctx, key, opts...) - if err != nil { - return nil, err - } - return &domain.Bigtable{ - Table: table, - Rows: []*domain.Row{ - readRow(row), - }, - }, nil -} - -func (b *bigtableRepository) GetRows(ctx context.Context, table string, rr bigtable.RowRange, opts ...bigtable.ReadOption) (*domain.Bigtable, error) { - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - - tbl := b.client.Open(table) - rows := []*domain.Row{} - err := tbl.ReadRows(ctx, rr, func(row bigtable.Row) bool { - rows = append(rows, readRow(row)) - return true - }, opts...) - if err != nil { - return nil, err - } - return &domain.Bigtable{ - Table: table, - Rows: rows, - }, nil -} - -func (b *bigtableRepository) Count(ctx context.Context, table string) (int, error) { - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - - tbl := b.client.Open(table) - cnt := 0 - err := tbl.ReadRows(ctx, bigtable.InfiniteRange(""), func(_ bigtable.Row) bool { - cnt++ - return true - }, bigtable.RowFilter(bigtable.StripValueFilter())) - return cnt, err -} - -func readRow(r bigtable.Row) *domain.Row { - ret := &domain.Row{ - Key: r.Key(), - Columns: make([]*domain.Column, 0, len(r)), - } - for fam := range r { - ris := r[fam] - for _, ri := range ris { - c := &domain.Column{ - Family: fam, - Qualifier: ri.Column, - Value: ri.Value, - Version: ri.Timestamp.Time(), - } - ret.Columns = append(ret.Columns, c) - } - } - - sort.Slice(ret.Columns, func(i, j int) bool { - return ret.Columns[i].Family > ret.Columns[j].Family - }) - return ret -} - -func (b *bigtableRepository) Tables(ctx context.Context) ([]string, error) { - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - - tbls, err := b.adminClient.Tables(ctx) - if err != nil { - return []string{}, err - } - sort.Strings(tbls) - return tbls, nil -} diff --git a/api/interfaces/executor.go b/api/interfaces/executor.go deleted file mode 100644 index ca963ad..0000000 --- a/api/interfaces/executor.go +++ /dev/null @@ -1,324 +0,0 @@ -package interfaces - -import ( - "context" - "fmt" - "io" - "os" - "strconv" - "strings" - "time" - - "cloud.google.com/go/bigtable" - "github.com/takashabe/btcli/api/application" -) - -// Avoid to circular dependencies -var ( - doHelpFn func(context.Context, *Executor, ...string) -) - -func doHelp(ctx context.Context, e *Executor, args ...string) { - doHelpFn(ctx, e, args...) -} - -func init() { - doHelpFn = lazyDoHelp -} - -// Executor provides exec command handler -type Executor struct { - outStream io.Writer - errStream io.Writer - history io.Writer - - tableInteractor *application.TableInteractor - rowsInteractor *application.RowsInteractor -} - -// Do provides execute command -func (e *Executor) Do(s string) { - s = strings.TrimSpace(s) - if s == "" { - return - } - - ctx := context.Background() - args := strings.Split(s, " ") - cmd := args[0] - - for _, c := range commands { - if cmd == c.Name { - if e.history != nil { - fmt.Fprintln(e.history, strings.Join(args, " ")) - } - - // TODO: extract args[0] - c.Runner(ctx, e, args...) - return - } - } - fmt.Fprintf(e.errStream, "Unknown command: %s\n", cmd) -} - -func doExit(ctx context.Context, e *Executor, args ...string) { - fmt.Fprintln(e.outStream, "Bye!") - os.Exit(0) -} - -func lazyDoHelp(ctx context.Context, e *Executor, args ...string) { - if len(args) == 1 { - usage(e.outStream) - return - } - cmd := args[1] - for _, c := range commands { - if c.Name == cmd { - fmt.Fprintln(e.outStream, c.Usage) - return - } - } - fmt.Fprintf(e.errStream, "Unknown command: %s\n", cmd) -} - -func doLS(ctx context.Context, e *Executor, args ...string) { - tables, err := e.tableInteractor.GetTables(ctx) - if err != nil { - fmt.Fprintf(e.errStream, "%v", err) - return - } - for _, tbl := range tables { - fmt.Fprintln(e.outStream, tbl) - } -} - -func doCount(ctx context.Context, e *Executor, args ...string) { - if len(args) < 2 { - fmt.Fprintln(e.errStream, "Invalid args: count ") - return - } - table := args[1] - cnt, err := e.rowsInteractor.GetRowCount(ctx, table) - if err != nil { - fmt.Fprintf(e.errStream, "%v", err) - return - } - fmt.Fprintln(e.outStream, cnt) -} - -func doLookup(ctx context.Context, e *Executor, args ...string) { - if len(args) < 3 { - fmt.Fprintln(e.errStream, "Invalid args: lookup
") - return - } - table := args[1] - key := args[2] - e.lookupWithOptions(table, key, args[3:]...) -} - -func doRead(ctx context.Context, e *Executor, args ...string) { - if len(args) < 2 { - fmt.Fprintln(e.errStream, "Invalid args: read
[args ...]") - return - } - table := args[1] - e.readWithOptions(table, args[2:]...) -} - -func (e *Executor) lookupWithOptions(table, key string, args ...string) { - parsed := make(map[string]string) - for _, arg := range args { - i := strings.Index(arg, "=") - if i < 0 { - fmt.Fprintf(e.errStream, "Invalid args: %v\n", arg) - return - } - // TODO: Improve parsing args - k, v := arg[:i], arg[i+1:] - switch k { - default: - fmt.Fprintf(e.errStream, "Unknown arg: %v\n", arg) - return - case "decode", "decode_columns": - parsed[k] = v - case "version": - parsed[k] = v - } - } - - ro, err := readOption(parsed) - if err != nil { - fmt.Fprintf(e.errStream, "Invalid options: %v\n", err) - return - } - - ctx := context.Background() - row, err := e.rowsInteractor.GetRow(ctx, table, key, ro...) - if err != nil { - fmt.Fprintf(e.errStream, "%v", err) - return - } - - // decode options - p := &Printer{ - outStream: e.outStream, - errStream: e.errStream, - - decodeType: decodeGlobalOption(parsed), - decodeColumnType: decodeColumnOption(parsed), - } - p.printRow(row) -} - -func (e *Executor) readWithOptions(table string, args ...string) { - parsed := make(map[string]string) - for _, arg := range args { - i := strings.Index(arg, "=") - if i < 0 { - fmt.Fprintf(os.Stderr, "Invalid args: %v\n", arg) - return - } - // TODO: Improve parsing args - key, val := arg[:i], arg[i+1:] - switch key { - default: - fmt.Fprintf(os.Stderr, "Unknown arg: %v\n", arg) - return - case "decode", "decode_columns": - parsed[key] = val - case "count", "start", "end", "prefix", "version", "family", "value", "from", "to": - parsed[key] = val - } - } - - if (parsed["start"] != "" || parsed["end"] != "") && parsed["prefix"] != "" { - fmt.Fprintf(e.errStream, `"start"/"end" may not be mixed with "prefix"`) - return - } - - rr, err := rowRange(parsed) - if err != nil { - fmt.Fprintf(e.errStream, "Invlaid range: %v\n", err) - return - } - ro, err := readOption(parsed) - if err != nil { - fmt.Fprintf(e.errStream, "Invalid options: %v\n", err) - return - } - - ctx := context.Background() - rows, err := e.rowsInteractor.GetRows(ctx, table, rr, ro...) - if err != nil { - fmt.Fprintf(e.errStream, "%v\n", err) - return - } - - // decode options - p := &Printer{ - outStream: e.outStream, - errStream: e.errStream, - - decodeType: decodeGlobalOption(parsed), - decodeColumnType: decodeColumnOption(parsed), - } - p.printRows(rows) -} - -func rowRange(parsedArgs map[string]string) (bigtable.RowRange, error) { - var rr bigtable.RowRange - if start, end := parsedArgs["start"], parsedArgs["end"]; end != "" { - rr = bigtable.NewRange(start, end) - } else if start != "" { - rr = bigtable.InfiniteRange(start) - } - if prefix := parsedArgs["prefix"]; prefix != "" { - rr = bigtable.PrefixRange(prefix) - } - - return rr, nil -} - -func readOption(parsedArgs map[string]string) ([]bigtable.ReadOption, error) { - var ( - opts []bigtable.ReadOption - fils []bigtable.Filter - ) - - // filters - if regex := parsedArgs["regex"]; regex != "" { - fils = append(fils, bigtable.RowKeyFilter(regex)) - } - if family := parsedArgs["family"]; family != "" { - fils = append(fils, bigtable.FamilyFilter(fmt.Sprintf("^%s$", family))) - } - if version := parsedArgs["version"]; version != "" { - n, err := strconv.ParseInt(version, 0, 64) - if err != nil { - return nil, err - } - fils = append(fils, bigtable.LatestNFilter(int(n))) - } - var startTime, endTime time.Time - if from := parsedArgs["from"]; from != "" { - t, err := strconv.ParseInt(from, 0, 64) - if err != nil { - return nil, err - } - startTime = time.Unix(t, 0) - } - if to := parsedArgs["to"]; to != "" { - t, err := strconv.ParseInt(to, 0, 64) - if err != nil { - return nil, err - } - endTime = time.Unix(t, 0) - } - if !startTime.IsZero() || !endTime.IsZero() { - fils = append(fils, bigtable.TimestampRangeFilter(startTime, endTime)) - } - if value := parsedArgs["value"]; value != "" { - fils = append(fils, bigtable.ValueFilter(fmt.Sprintf("%s", value))) - } - - if len(fils) == 1 { - opts = append(opts, bigtable.RowFilter(fils[0])) - } else if len(fils) > 1 { - opts = append(opts, bigtable.RowFilter(bigtable.ChainFilters(fils...))) - } - - // isolated readOption - if count := parsedArgs["count"]; count != "" { - n, err := strconv.ParseInt(count, 0, 64) - if err != nil { - return nil, err - } - opts = append(opts, bigtable.LimitRows(n)) - } - return opts, nil -} - -func decodeGlobalOption(parsedArgs map[string]string) string { - if d := parsedArgs["decode"]; d != "" { - return d - } - return os.Getenv("BTCLI_DECODE_TYPE") -} - -func decodeColumnOption(parsedArgs map[string]string) map[string]string { - arg := parsedArgs["decode_columns"] - if len(arg) == 0 { - return map[string]string{} - } - - ds := strings.Split(arg, ",") - ret := map[string]string{} - for _, d := range ds { - ct := strings.SplitN(d, ":", 2) - if len(ct) != 2 { - continue - } - ret[ct[0]] = ct[1] - } - return ret -} diff --git a/api/interfaces/printer.go b/api/interfaces/printer.go deleted file mode 100644 index 8cb73c4..0000000 --- a/api/interfaces/printer.go +++ /dev/null @@ -1,86 +0,0 @@ -package interfaces - -import ( - "encoding/binary" - "fmt" - "io" - "math" - "strings" - - "github.com/takashabe/btcli/api/domain" -) - -const ( - decodeTypeString = "string" - decodeTypeInt = "int" - decodeTypeFloat = "float" -) - -// Printer print the bigtable items to stream -type Printer struct { - outStream io.Writer - errStream io.Writer - - decodeType string - decodeColumnType map[string]string -} - -func (w *Printer) printRows(rs []*domain.Row) { - for _, r := range rs { - w.printRow(r) - } -} - -func (w *Printer) printRow(r *domain.Row) { - fmt.Fprintln(w.outStream, strings.Repeat("-", 40)) - fmt.Fprintln(w.outStream, r.Key) - - for _, c := range r.Columns { - fmt.Fprintf(w.outStream, " %-40s @ %s\n", c.Qualifier, c.Version.Format("2006/01/02-15:04:05.000000")) - w.printValue(c.Qualifier, c.Value) - } -} - -func (w *Printer) printValue(q string, v []byte) { - // extract columnName in a qualifier - // qualifier format: "columnFamily:columnName" - q = q[strings.Index(q, ":")+1:] - - // retrieve decode each columns - // decodeColumns format "column1:type1,column2:type2,..." - for column, decode := range w.decodeColumnType { - if q == column { - w.doPrint(decode, v) - return - } - } - - // invoke print with a general decodeType - w.doPrint(w.decodeType, v) -} - -func (w *Printer) doPrint(decode string, v []byte) { - if len(v) != 8 { - fmt.Fprintf(w.outStream, " %q\n", v) - return - } - - switch decode { - case decodeTypeInt: - fmt.Fprintf(w.outStream, " %d\n", w.byte2Int(v)) - case decodeTypeFloat: - fmt.Fprintf(w.outStream, " %f\n", w.byte2Float(v)) - case decodeTypeString: - default: - fmt.Fprintf(w.outStream, " %q\n", v) - } -} - -func (*Printer) byte2Int(b []byte) int64 { - return (int64)(binary.BigEndian.Uint64(b)) -} - -func (*Printer) byte2Float(b []byte) float64 { - bits := binary.BigEndian.Uint64(b) - return math.Float64frombits(bits) -} diff --git a/cmd/btcli/btcli.go b/cmd/btcli/btcli.go index 15c223b..baf1484 100644 --- a/cmd/btcli/btcli.go +++ b/cmd/btcli/btcli.go @@ -3,7 +3,7 @@ package main import ( "os" - "github.com/takashabe/btcli/api/interfaces" + "github.com/takashabe/btcli/pkg/cmd/interactive" ) // App version @@ -13,7 +13,7 @@ var ( ) func main() { - cli := &interfaces.CLI{ + cli := &interactive.CLI{ OutStream: os.Stdout, ErrStream: os.Stderr, Version: Version, diff --git a/docker-compose.yml b/docker-compose.yml index 6707e1a..994aa45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "2" +version: "3" services: bigtable: diff --git a/pkg/bigtable/bigtable.go b/pkg/bigtable/bigtable.go new file mode 100644 index 0000000..dc468fc --- /dev/null +++ b/pkg/bigtable/bigtable.go @@ -0,0 +1,193 @@ +package bigtable + +import ( + "context" + "io" + "os" + "sort" + "time" + + "cloud.google.com/go/bigtable" +) + +//go:generate mockgen --package=bigtable -source=bigtable.go -destination=bigtable_mock.go + +// Bigtable entity of the bigtable +type Bigtable struct { + Table string + Rows []*Row +} + +// Row represent a row of the table +type Row struct { + Key string + Columns []*Column +} + +// Column represent a column of the row +type Column struct { + Family string + Qualifier string + Value []byte + Version time.Time +} + +// Client represent repository of the bigtable +type Client interface { + OutStream() io.Writer + ErrStream() io.Writer + + Get(ctx context.Context, table, key string, opts ...bigtable.ReadOption) (*Bigtable, error) + GetRows(ctx context.Context, table string, rr bigtable.RowRange, opts ...bigtable.ReadOption) (*Bigtable, error) + Count(ctx context.Context, table string) (int, error) + Tables(ctx context.Context) ([]string, error) +} + +type client struct { + client *bigtable.Client + adminClient *bigtable.AdminClient + outStream io.Writer + errStream io.Writer +} + +// Option functional option pattern for the client. +type Option func(*client) + +// NewClient returns initialized client. +func NewClient(project, instance string, opts ...Option) (Client, error) { + cli, err := getClient(project, instance) + if err != nil { + return nil, err + } + adminClient, err := getAdminClient(project, instance) + if err != nil { + return nil, err + } + return &client{ + client: cli, + adminClient: adminClient, + }, nil +} + +// WithOutStream settings outStream +func WithOutStream(w io.Writer) Option { + return func(c *client) { + c.outStream = w + } +} + +// WithErrStream settings errStream +func WithErrStream(w io.Writer) Option { + return func(c *client) { + c.errStream = w + } +} + +func getClient(project, instance string) (*bigtable.Client, error) { + // TODO: Support options + return bigtable.NewClient(context.Background(), project, instance) +} + +func getAdminClient(project, instance string) (*bigtable.AdminClient, error) { + // TODO: Support options + return bigtable.NewAdminClient(context.Background(), project, instance) +} + +func (c *client) OutStream() io.Writer { + if c.outStream == nil { + return os.Stdout + } + return c.outStream +} + +func (c *client) ErrStream() io.Writer { + if c.errStream == nil { + return os.Stderr + } + return c.errStream +} + +func (c *client) Get(ctx context.Context, table, key string, opts ...bigtable.ReadOption) (*Bigtable, error) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + tbl := c.client.Open(table) + row, err := tbl.ReadRow(ctx, key, opts...) + if err != nil { + return nil, err + } + return &Bigtable{ + Table: table, + Rows: []*Row{ + readRow(row), + }, + }, nil +} + +func (c *client) GetRows(ctx context.Context, table string, rr bigtable.RowRange, opts ...bigtable.ReadOption) (*Bigtable, error) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + tbl := c.client.Open(table) + rows := []*Row{} + err := tbl.ReadRows(ctx, rr, func(row bigtable.Row) bool { + rows = append(rows, readRow(row)) + return true + }, opts...) + if err != nil { + return nil, err + } + return &Bigtable{ + Table: table, + Rows: rows, + }, nil +} + +func (c *client) Count(ctx context.Context, table string) (int, error) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + tbl := c.client.Open(table) + cnt := 0 + err := tbl.ReadRows(ctx, bigtable.InfiniteRange(""), func(_ bigtable.Row) bool { + cnt++ + return true + }, bigtable.RowFilter(bigtable.StripValueFilter())) + return cnt, err +} + +func readRow(r bigtable.Row) *Row { + ret := &Row{ + Key: r.Key(), + Columns: make([]*Column, 0, len(r)), + } + for fam := range r { + ris := r[fam] + for _, ri := range ris { + c := &Column{ + Family: fam, + Qualifier: ri.Column, + Value: ri.Value, + Version: ri.Timestamp.Time(), + } + ret.Columns = append(ret.Columns, c) + } + } + + sort.Slice(ret.Columns, func(i, j int) bool { + return ret.Columns[i].Family > ret.Columns[j].Family + }) + return ret +} + +func (c *client) Tables(ctx context.Context) ([]string, error) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + tbls, err := c.adminClient.Tables(ctx) + if err != nil { + return []string{}, err + } + sort.Strings(tbls) + return tbls, nil +} diff --git a/pkg/bigtable/bigtable_mock.go b/pkg/bigtable/bigtable_mock.go new file mode 100644 index 0000000..4eede32 --- /dev/null +++ b/pkg/bigtable/bigtable_mock.go @@ -0,0 +1,134 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: bigtable.go + +// Package bigtable is a generated GoMock package. +package bigtable + +import ( + bigtable "cloud.google.com/go/bigtable" + context "context" + gomock "github.com/golang/mock/gomock" + io "io" + reflect "reflect" +) + +// MockClient is a mock of Client interface +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// OutStream mocks base method +func (m *MockClient) OutStream() io.Writer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OutStream") + ret0, _ := ret[0].(io.Writer) + return ret0 +} + +// OutStream indicates an expected call of OutStream +func (mr *MockClientMockRecorder) OutStream() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OutStream", reflect.TypeOf((*MockClient)(nil).OutStream)) +} + +// ErrStream mocks base method +func (m *MockClient) ErrStream() io.Writer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ErrStream") + ret0, _ := ret[0].(io.Writer) + return ret0 +} + +// ErrStream indicates an expected call of ErrStream +func (mr *MockClientMockRecorder) ErrStream() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ErrStream", reflect.TypeOf((*MockClient)(nil).ErrStream)) +} + +// Get mocks base method +func (m *MockClient) Get(ctx context.Context, table, key string, opts ...bigtable.ReadOption) (*Bigtable, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, table, key} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(*Bigtable) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get +func (mr *MockClientMockRecorder) Get(ctx, table, key interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, table, key}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), varargs...) +} + +// GetRows mocks base method +func (m *MockClient) GetRows(ctx context.Context, table string, rr bigtable.RowRange, opts ...bigtable.ReadOption) (*Bigtable, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, table, rr} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetRows", varargs...) + ret0, _ := ret[0].(*Bigtable) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRows indicates an expected call of GetRows +func (mr *MockClientMockRecorder) GetRows(ctx, table, rr interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, table, rr}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRows", reflect.TypeOf((*MockClient)(nil).GetRows), varargs...) +} + +// Count mocks base method +func (m *MockClient) Count(ctx context.Context, table string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Count", ctx, table) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Count indicates an expected call of Count +func (mr *MockClientMockRecorder) Count(ctx, table interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockClient)(nil).Count), ctx, table) +} + +// Tables mocks base method +func (m *MockClient) Tables(ctx context.Context) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Tables", ctx) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Tables indicates an expected call of Tables +func (mr *MockClientMockRecorder) Tables(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tables", reflect.TypeOf((*MockClient)(nil).Tables), ctx) +} diff --git a/api/infrastructure/bigtable/bigtable_test.go b/pkg/bigtable/bigtable_test.go similarity index 85% rename from api/infrastructure/bigtable/bigtable_test.go rename to pkg/bigtable/bigtable_test.go index 2f9666f..f11df6b 100644 --- a/api/infrastructure/bigtable/bigtable_test.go +++ b/pkg/bigtable/bigtable_test.go @@ -7,7 +7,6 @@ import ( "cloud.google.com/go/bigtable" "github.com/stretchr/testify/assert" - "github.com/takashabe/btcli/api/domain" ) func TestGet(t *testing.T) { @@ -18,14 +17,14 @@ func TestGet(t *testing.T) { cases := []struct { table string key string - expect *domain.Row + expect *Row }{ { "users", "1", - &domain.Row{ + &Row{ Key: "1", - Columns: []*domain.Column{ + Columns: []*Column{ { Family: "d", Qualifier: "d:row", @@ -38,9 +37,9 @@ func TestGet(t *testing.T) { { "articles", "1##1", - &domain.Row{ + &Row{ Key: "1##1", - Columns: []*domain.Column{ + Columns: []*Column{ { Family: "d", Qualifier: "d:content", @@ -58,7 +57,7 @@ func TestGet(t *testing.T) { }, } for _, c := range cases { - r, err := NewBigtableRepository("test-project", "test-instance") + r, err := NewClient("test-project", "test-instance") assert.NoError(t, err) bt, err := r.Get(context.Background(), c.table, c.key) @@ -84,16 +83,16 @@ func TestGetRows(t *testing.T) { table string rr bigtable.RowRange opts []bigtable.ReadOption - expect []*domain.Row + expect []*Row }{ { "users", bigtable.PrefixRange("1"), []bigtable.ReadOption{}, - []*domain.Row{ + []*Row{ { Key: "1", - Columns: []*domain.Column{ + Columns: []*Column{ { Family: "d", Qualifier: "d:row", @@ -104,7 +103,7 @@ func TestGetRows(t *testing.T) { }, { Key: "10", - Columns: []*domain.Column{ + Columns: []*Column{ { Family: "d'", Qualifier: "d':row", @@ -126,10 +125,10 @@ func TestGetRows(t *testing.T) { ), ), }, - []*domain.Row{ + []*Row{ { Key: "4", - Columns: []*domain.Column{ + Columns: []*Column{ { Family: "d", Qualifier: "d:row", @@ -144,10 +143,10 @@ func TestGetRows(t *testing.T) { "articles", bigtable.PrefixRange("3"), []bigtable.ReadOption{}, - []*domain.Row{ + []*Row{ { Key: "3##1", - Columns: []*domain.Column{ + Columns: []*Column{ { Family: "d", Qualifier: "d:content", @@ -166,7 +165,7 @@ func TestGetRows(t *testing.T) { }, } for _, c := range cases { - r, err := NewBigtableRepository("test-project", "test-instance") + r, err := NewClient("test-project", "test-instance") assert.NoError(t, err) bt, err := r.GetRows(context.Background(), c.table, c.rr, c.opts...) @@ -187,7 +186,7 @@ func TestCount(t *testing.T) { {"users", 5}, } for _, c := range cases { - r, err := NewBigtableRepository("test-project", "test-instance") + r, err := NewClient("test-project", "test-instance") assert.NoError(t, err) cnt, err := r.Count(context.Background(), c.table) @@ -212,7 +211,7 @@ func TestTables(t *testing.T) { }, } for _, c := range cases { - r, err := NewBigtableRepository("test-project", "test-instance") + r, err := NewClient("test-project", "test-instance") assert.NoError(t, err) tbls, err := r.Tables(context.Background()) diff --git a/api/infrastructure/bigtable/main_test.go b/pkg/bigtable/main_test.go similarity index 100% rename from api/infrastructure/bigtable/main_test.go rename to pkg/bigtable/main_test.go diff --git a/api/infrastructure/bigtable/testdata/articles.yaml b/pkg/bigtable/testdata/articles.yaml similarity index 100% rename from api/infrastructure/bigtable/testdata/articles.yaml rename to pkg/bigtable/testdata/articles.yaml diff --git a/api/infrastructure/bigtable/testdata/users.yaml b/pkg/bigtable/testdata/users.yaml similarity index 100% rename from api/infrastructure/bigtable/testdata/users.yaml rename to pkg/bigtable/testdata/users.yaml diff --git a/api/interfaces/command.go b/pkg/cmd/interactive/command.go similarity index 88% rename from api/interfaces/command.go rename to pkg/cmd/interactive/command.go index d2d9fc8..52a2020 100644 --- a/api/interfaces/command.go +++ b/pkg/cmd/interactive/command.go @@ -1,9 +1,11 @@ -package interfaces +package interactive import ( "context" prompt "github.com/c-bata/go-prompt" + "github.com/takashabe/btcli/pkg/bigtable" + "github.com/takashabe/btcli/pkg/evaluator/cbt" ) // Command defines command describe and runner @@ -11,7 +13,7 @@ type Command struct { Name string Description string Usage string - Runner func(context.Context, *Executor, ...string) + Runner func(context.Context, bigtable.Client, ...string) } var commands = []Command{ @@ -25,13 +27,13 @@ var commands = []Command{ Name: "ls", Description: "List tables", Usage: "ls", - Runner: doLS, + Runner: cbt.DoLS, }, { Name: "count", Description: "Count table rows", Usage: "count
", - Runner: doCount, + Runner: cbt.DoCount, }, { Name: "lookup", @@ -40,7 +42,7 @@ var commands = []Command{ version Read only latest columns decode Decode big-endian value decode-columns Decode big-endian value with columns. [,]`, - Runner: doLookup, + Runner: cbt.DoLookup, }, { Name: "read", @@ -56,7 +58,7 @@ var commands = []Command{ to Read older cells than this unittime decode Decode big-endian value decode-columns Decode big-endian value with columns. [,]`, - Runner: doRead, + Runner: cbt.DoRead, }, // btcli commands diff --git a/api/interfaces/completer.go b/pkg/cmd/interactive/completer.go similarity index 92% rename from api/interfaces/completer.go rename to pkg/cmd/interactive/completer.go index fd11ecf..766d2a4 100644 --- a/api/interfaces/completer.go +++ b/pkg/cmd/interactive/completer.go @@ -1,16 +1,16 @@ -package interfaces +package interactive import ( "context" "strings" prompt "github.com/c-bata/go-prompt" - "github.com/takashabe/btcli/api/application" + "github.com/takashabe/btcli/pkg/bigtable" ) // Completer provides completion command handler type Completer struct { - tableInteractor *application.TableInteractor + client bigtable.Client } // Do provide completion to prompt @@ -94,7 +94,7 @@ func filterDuplicateCommands(args []string, subcommands []prompt.Suggest) []prom } func (c *Completer) getTableSuggestions() []prompt.Suggest { - tbls, err := c.tableInteractor.GetTables(context.Background()) + tbls, err := c.client.Tables(context.Background()) if err != nil { return []prompt.Suggest{} } diff --git a/api/interfaces/completer_test.go b/pkg/cmd/interactive/completer_test.go similarity index 96% rename from api/interfaces/completer_test.go rename to pkg/cmd/interactive/completer_test.go index 61caebf..1c309c6 100644 --- a/api/interfaces/completer_test.go +++ b/pkg/cmd/interactive/completer_test.go @@ -1,4 +1,4 @@ -package interfaces +package interactive import ( "testing" diff --git a/pkg/cmd/interactive/executor.go b/pkg/cmd/interactive/executor.go new file mode 100644 index 0000000..ee9fe3f --- /dev/null +++ b/pkg/cmd/interactive/executor.go @@ -0,0 +1,74 @@ +package interactive + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/takashabe/btcli/pkg/bigtable" +) + +// Avoid to circular dependencies +var ( + doHelpFn func(context.Context, bigtable.Client, ...string) +) + +func doHelp(ctx context.Context, client bigtable.Client, args ...string) { + doHelpFn(ctx, client, args...) +} + +func init() { + doHelpFn = lazyDoHelp +} + +// Executor provides exec command handler +type Executor struct { + client bigtable.Client + history io.Writer +} + +// Do provides execute command +func (e *Executor) Do(s string) { + s = strings.TrimSpace(s) + if s == "" { + return + } + + ctx := context.Background() + args := strings.Split(s, " ") + cmd := args[0] + + for _, c := range commands { + if cmd == c.Name { + if e.history != nil { + fmt.Fprintln(e.history, strings.Join(args, " ")) + } + + c.Runner(ctx, e.client, args[1:]...) + return + } + } + fmt.Fprintf(e.client.ErrStream(), "Unknown command: %s\n", cmd) +} + +func doExit(ctx context.Context, client bigtable.Client, args ...string) { + fmt.Fprintln(client.OutStream(), "Bye!") + os.Exit(0) +} + +func lazyDoHelp(ctx context.Context, client bigtable.Client, args ...string) { + if len(args) == 1 { + usage(client.OutStream()) + return + } + cmd := args[1] + for _, c := range commands { + if c.Name == cmd { + fmt.Fprintln(client.OutStream(), c.Usage) + return + } + } + fmt.Fprintf(client.ErrStream(), "Unknown command: %s\n", cmd) +} diff --git a/pkg/cmd/interactive/executor_test.go b/pkg/cmd/interactive/executor_test.go new file mode 100644 index 0000000..6ca15cc --- /dev/null +++ b/pkg/cmd/interactive/executor_test.go @@ -0,0 +1 @@ +package interactive diff --git a/api/interfaces/cli.go b/pkg/cmd/interactive/interactive.go similarity index 82% rename from api/interfaces/cli.go rename to pkg/cmd/interactive/interactive.go index 1d4ba58..26ab38b 100644 --- a/api/interfaces/cli.go +++ b/pkg/cmd/interactive/interactive.go @@ -1,4 +1,4 @@ -package interfaces +package interactive import ( "bufio" @@ -10,9 +10,8 @@ import ( prompt "github.com/c-bata/go-prompt" "github.com/pkg/errors" - "github.com/takashabe/btcli/api/application" - "github.com/takashabe/btcli/api/config" - "github.com/takashabe/btcli/api/infrastructure/bigtable" + "github.com/takashabe/btcli/pkg/bigtable" + "github.com/takashabe/btcli/pkg/config" ) // exit codes @@ -89,23 +88,17 @@ func usage(w io.Writer) { } func (c *CLI) preparePrompt(conf *config.Config, writer io.Writer, histories []string) (*prompt.Prompt, error) { - repository, err := bigtable.NewBigtableRepository(conf.Project, conf.Instance) + client, err := bigtable.NewClient(conf.Project, conf.Instance) if err != nil { return nil, errors.Wrapf(err, "failed to initialized bigtable repository:%v", err) } - tableInteractor := application.NewTableInteractor(repository) - rowsInteractor := application.NewRowsInteractor(repository) executor := Executor{ - outStream: c.OutStream, - errStream: c.ErrStream, - history: writer, - - rowsInteractor: rowsInteractor, - tableInteractor: tableInteractor, + history: writer, + client: client, } completer := Completer{ - tableInteractor: tableInteractor, + client: client, } return prompt.New( diff --git a/api/interfaces/main_test.go b/pkg/cmd/interactive/main_test.go similarity index 92% rename from api/interfaces/main_test.go rename to pkg/cmd/interactive/main_test.go index 6786ae4..5ffe7e4 100644 --- a/api/interfaces/main_test.go +++ b/pkg/cmd/interactive/main_test.go @@ -1,4 +1,4 @@ -package interfaces +package interactive import ( "os" diff --git a/api/config/config.go b/pkg/config/config.go similarity index 100% rename from api/config/config.go rename to pkg/config/config.go diff --git a/pkg/evaluator/cbt/evaluator.go b/pkg/evaluator/cbt/evaluator.go new file mode 100644 index 0000000..4b6b25e --- /dev/null +++ b/pkg/evaluator/cbt/evaluator.go @@ -0,0 +1,248 @@ +package cbt + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "time" + + "cloud.google.com/go/bigtable" + bt "github.com/takashabe/btcli/pkg/bigtable" + "github.com/takashabe/btcli/pkg/printer" +) + +func DoLS(ctx context.Context, client bt.Client, args ...string) { + tables, err := client.Tables(ctx) + if err != nil { + fmt.Fprintf(client.ErrStream(), "%v", err) + return + } + for _, tbl := range tables { + fmt.Fprintln(client.OutStream(), tbl) + } +} + +func DoCount(ctx context.Context, client bt.Client, args ...string) { + if len(args) < 1 { + fmt.Fprintln(client.ErrStream(), "Invalid args: count
") + return + } + table := args[0] + cnt, err := client.Count(ctx, table) + if err != nil { + fmt.Fprintf(client.ErrStream(), "%v", err) + return + } + fmt.Fprintln(client.OutStream(), cnt) +} + +func DoLookup(ctx context.Context, client bt.Client, args ...string) { + if len(args) < 2 { + fmt.Fprintln(client.ErrStream(), "Invalid args: lookup
") + return + } + table := args[0] + key := args[1] + opts := args[2:] + + parsed := make(map[string]string) + for _, opt := range opts { + i := strings.Index(opt, "=") + if i < 0 { + fmt.Fprintf(client.ErrStream(), "Invalid option: %v\n", opt) + return + } + // TODO: Improve parsing opts + k, v := opt[:i], opt[i+1:] + switch k { + default: + fmt.Fprintf(client.ErrStream(), "Unknown option: %v\n", opt) + return + case "decode", "decode_columns": + parsed[k] = v + case "version": + parsed[k] = v + } + } + + ro, err := readOption(parsed) + if err != nil { + fmt.Fprintf(client.ErrStream(), "Invalid options: %v\n", err) + return + } + + b, err := client.Get(ctx, table, key, ro...) + if err != nil { + fmt.Fprintf(client.OutStream(), "%v", err) + return + } + row := b.Rows[0] + + // decode options + p := &printer.Printer{ + OutStream: client.OutStream(), + DecodeType: decodeGlobalOption(parsed), + DecodeColumnType: decodeColumnOption(parsed), + } + p.PrintRow(row) +} + +func DoRead(ctx context.Context, client bt.Client, args ...string) { + if len(args) < 1 { + fmt.Fprintln(client.ErrStream(), "Invalid args: read
[args ...]") + return + } + table := args[0] + opts := args[1:] + + parsed := make(map[string]string) + for _, opt := range opts { + i := strings.Index(opt, "=") + if i < 0 { + fmt.Fprintf(os.Stderr, "Invalid option: %v\n", opt) + return + } + // TODO: Improve parsing opts + key, val := opt[:i], opt[i+1:] + switch key { + default: + fmt.Fprintf(os.Stderr, "Unknown option: %v\n", opt) + return + case "decode", "decode_columns": + parsed[key] = val + case "count", "start", "end", "prefix", "version", "family", "value", "from", "to": + parsed[key] = val + } + } + + if (parsed["start"] != "" || parsed["end"] != "") && parsed["prefix"] != "" { + fmt.Fprintf(client.ErrStream(), `"start"/"end" may not be mixed with "prefix"`) + return + } + + rr, err := rowRange(parsed) + if err != nil { + fmt.Fprintf(client.ErrStream(), "Invlaid range: %v\n", err) + return + } + ro, err := readOption(parsed) + if err != nil { + fmt.Fprintf(client.ErrStream(), "Invalid options: %v\n", err) + return + } + + b, err := client.GetRows(ctx, table, rr, ro...) + if err != nil { + fmt.Fprintf(client.ErrStream(), "%v\n", err) + return + } + rows := b.Rows + + // decode options + p := &printer.Printer{ + OutStream: client.OutStream(), + DecodeType: decodeGlobalOption(parsed), + DecodeColumnType: decodeColumnOption(parsed), + } + p.PrintRows(rows) +} + +func rowRange(parsedArgs map[string]string) (bigtable.RowRange, error) { + var rr bigtable.RowRange + if start, end := parsedArgs["start"], parsedArgs["end"]; end != "" { + rr = bigtable.NewRange(start, end) + } else if start != "" { + rr = bigtable.InfiniteRange(start) + } + if prefix := parsedArgs["prefix"]; prefix != "" { + rr = bigtable.PrefixRange(prefix) + } + + return rr, nil +} + +func readOption(parsedArgs map[string]string) ([]bigtable.ReadOption, error) { + var ( + opts []bigtable.ReadOption + fils []bigtable.Filter + ) + + // filters + if regex := parsedArgs["regex"]; regex != "" { + fils = append(fils, bigtable.RowKeyFilter(regex)) + } + if family := parsedArgs["family"]; family != "" { + fils = append(fils, bigtable.FamilyFilter(fmt.Sprintf("^%s$", family))) + } + if version := parsedArgs["version"]; version != "" { + n, err := strconv.ParseInt(version, 0, 64) + if err != nil { + return nil, err + } + fils = append(fils, bigtable.LatestNFilter(int(n))) + } + var startTime, endTime time.Time + if from := parsedArgs["from"]; from != "" { + t, err := strconv.ParseInt(from, 0, 64) + if err != nil { + return nil, err + } + startTime = time.Unix(t, 0) + } + if to := parsedArgs["to"]; to != "" { + t, err := strconv.ParseInt(to, 0, 64) + if err != nil { + return nil, err + } + endTime = time.Unix(t, 0) + } + if !startTime.IsZero() || !endTime.IsZero() { + fils = append(fils, bigtable.TimestampRangeFilter(startTime, endTime)) + } + if value := parsedArgs["value"]; value != "" { + fils = append(fils, bigtable.ValueFilter(fmt.Sprintf("%s", value))) + } + + if len(fils) == 1 { + opts = append(opts, bigtable.RowFilter(fils[0])) + } else if len(fils) > 1 { + opts = append(opts, bigtable.RowFilter(bigtable.ChainFilters(fils...))) + } + + // isolated readOption + if count := parsedArgs["count"]; count != "" { + n, err := strconv.ParseInt(count, 0, 64) + if err != nil { + return nil, err + } + opts = append(opts, bigtable.LimitRows(n)) + } + return opts, nil +} + +func decodeGlobalOption(parsedArgs map[string]string) string { + if d := parsedArgs["decode"]; d != "" { + return d + } + return os.Getenv("BTCLI_DECODE_TYPE") +} + +func decodeColumnOption(parsedArgs map[string]string) map[string]string { + arg := parsedArgs["decode_columns"] + if len(arg) == 0 { + return map[string]string{} + } + + ds := strings.Split(arg, ",") + ret := map[string]string{} + for _, d := range ds { + ct := strings.SplitN(d, ":", 2) + if len(ct) != 2 { + continue + } + ret[ct[0]] = ct[1] + } + return ret +} diff --git a/api/interfaces/executor_test.go b/pkg/evaluator/cbt/evaluator_test.go similarity index 69% rename from api/interfaces/executor_test.go rename to pkg/evaluator/cbt/evaluator_test.go index 69e30bb..52bc347 100644 --- a/api/interfaces/executor_test.go +++ b/pkg/evaluator/cbt/evaluator_test.go @@ -1,7 +1,8 @@ -package interfaces +package cbt import ( "bytes" + "context" "os" "testing" "time" @@ -9,9 +10,7 @@ import ( "cloud.google.com/go/bigtable" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - "github.com/takashabe/btcli/api/application" - "github.com/takashabe/btcli/api/domain" - "github.com/takashabe/btcli/api/domain/repository" + bt "github.com/takashabe/btcli/pkg/bigtable" ) func TestRowRange(t *testing.T) { @@ -88,42 +87,36 @@ func TestReadOption(t *testing.T) { } } -func TestDoReadRowExecutor(t *testing.T) { +func TestDoRead(t *testing.T) { tm, _ := time.Parse("2006-01-02 15:04:05", "2018-01-01 00:00:00") ctrl := gomock.NewController(t) defer ctrl.Finish() cases := []struct { env map[string]string - input string + input []string expect string - prepare func(*repository.MockBigtable) + prepare func(*bt.MockClient) }{ { map[string]string{}, - "ls", - "a\nb\n", - func(mock *repository.MockBigtable) { - mock.EXPECT().Tables(gomock.Any()).Return([]string{"a", "b"}, nil).Times(1) + []string{ + "table", "prefix=a", "version=1", "decode=int", "decode_columns=row:string,404:float", }, - }, - { - map[string]string{}, - "read table prefix=a version=1 decode=int decode_columns=row:string,404:float", "----------------------------------------\na\n d:row @ 2018/01/01-00:00:00.000000\n \"a1\"\n", - func(mock *repository.MockBigtable) { + func(mock *bt.MockClient) { mock.EXPECT().GetRows( gomock.Any(), "table", bigtable.PrefixRange("a"), bigtable.RowFilter(bigtable.LatestNFilter(1)), ).Return( - &domain.Bigtable{ + &bt.Bigtable{ Table: "table", - Rows: []*domain.Row{ + Rows: []*bt.Row{ { Key: "a", - Columns: []*domain.Column{ + Columns: []*bt.Column{ { Family: "d", Qualifier: "d:row", @@ -140,24 +133,26 @@ func TestDoReadRowExecutor(t *testing.T) { map[string]string{ "BTCLI_DECODE_TYPE": "int", }, - "read table version=1 family=d", + []string{ + "table", "version=1", "family=d", + }, "----------------------------------------\na\n d:row @ 2018/01/01-00:00:00.000000\n 1\n", - func(mock *repository.MockBigtable) { + func(mock *bt.MockClient) { mock.EXPECT().GetRows( gomock.Any(), "table", bigtable.RowRange{}, - chainFilters( + filtersToReadOption( bigtable.FamilyFilter("^d$"), bigtable.LatestNFilter(1), ), ).Return( - &domain.Bigtable{ + &bt.Bigtable{ Table: "table", - Rows: []*domain.Row{ + Rows: []*bt.Row{ { Key: "a", - Columns: []*domain.Column{ + Columns: []*bt.Column{ { Family: "d", Qualifier: "d:row", @@ -174,24 +169,26 @@ func TestDoReadRowExecutor(t *testing.T) { map[string]string{ "BTCLI_DECODE_TYPE": "string", }, - "read table version=1 family=d decode=int", + []string{ + "table", "version=1", "family=d", "decode=int", + }, "----------------------------------------\na\n d:row @ 2018/01/01-00:00:00.000000\n 1\n", - func(mock *repository.MockBigtable) { + func(mock *bt.MockClient) { mock.EXPECT().GetRows( gomock.Any(), "table", bigtable.RowRange{}, - chainFilters( + filtersToReadOption( bigtable.FamilyFilter("^d$"), bigtable.LatestNFilter(1), ), ).Return( - &domain.Bigtable{ + &bt.Bigtable{ Table: "table", - Rows: []*domain.Row{ + Rows: []*bt.Row{ { Key: "a", - Columns: []*domain.Column{ + Columns: []*bt.Column{ { Family: "d", Qualifier: "d:row", @@ -206,8 +203,8 @@ func TestDoReadRowExecutor(t *testing.T) { }, } for _, c := range cases { - mockBtRepo := repository.NewMockBigtable(ctrl) - c.prepare(mockBtRepo) + mockClient := bt.NewMockClient(ctrl) + c.prepare(mockClient) for k, v := range c.env { os.Setenv(k, v) @@ -215,17 +212,10 @@ func TestDoReadRowExecutor(t *testing.T) { } var buf bytes.Buffer - // TODO: debug - // var r io.Reader = &buf - // r = io.TeeReader(r, os.Stdout) - executor := Executor{ - outStream: &buf, - errStream: &buf, - tableInteractor: application.NewTableInteractor(mockBtRepo), - rowsInteractor: application.NewRowsInteractor(mockBtRepo), - } + mockClient.EXPECT().OutStream().Return(&buf).AnyTimes() + mockClient.EXPECT().ErrStream().Return(&buf).AnyTimes() - executor.Do(c.input) + DoRead(context.Background(), mockClient, c.input...) assert.Equal(t, c.expect, buf.String()) } } @@ -235,33 +225,27 @@ func TestDoCountExecutor(t *testing.T) { defer ctrl.Finish() cases := []struct { - input string + input []string expect string - prepare func(*repository.MockBigtable) + prepare func(*bt.MockClient) }{ { - "count table", + []string{"table"}, "1\n", - func(mock *repository.MockBigtable) { + func(mock *bt.MockClient) { mock.EXPECT().Count(gomock.Any(), "table").Return(1, nil) }, }, } for _, c := range cases { - mockBtRepo := repository.NewMockBigtable(ctrl) - c.prepare(mockBtRepo) + mockClient := bt.NewMockClient(ctrl) + c.prepare(mockClient) var buf bytes.Buffer - // TODO: debug - // var r io.Reader = &buf - // r = io.TeeReader(r, os.Stdout) - executor := Executor{ - outStream: &buf, - errStream: &buf, - rowsInteractor: application.NewRowsInteractor(mockBtRepo), - } + mockClient.EXPECT().OutStream().Return(&buf).AnyTimes() + mockClient.EXPECT().ErrStream().Return(&buf).AnyTimes() - executor.Do(c.input) + DoCount(context.Background(), mockClient, c.input...) assert.Equal(t, c.expect, buf.String()) } } diff --git a/pkg/evaluator/cbt/main_test.go b/pkg/evaluator/cbt/main_test.go new file mode 100644 index 0000000..267098d --- /dev/null +++ b/pkg/evaluator/cbt/main_test.go @@ -0,0 +1,15 @@ +package cbt + +import ( + "testing" + + "cloud.google.com/go/bigtable" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func filtersToReadOption(fs ...bigtable.Filter) bigtable.ReadOption { + return bigtable.RowFilter(bigtable.ChainFilters(fs...)) +} diff --git a/pkg/printer/printer.go b/pkg/printer/printer.go new file mode 100644 index 0000000..fec2af9 --- /dev/null +++ b/pkg/printer/printer.go @@ -0,0 +1,87 @@ +package printer + +import ( + "encoding/binary" + "fmt" + "io" + "math" + "strings" + + "github.com/takashabe/btcli/pkg/bigtable" +) + +// decode to specific type. +const ( + DecodeTypeString = "string" + DecodeTypeInt = "int" + DecodeTypeFloat = "float" +) + +// Printer print the bigtable items to stream +type Printer struct { + OutStream io.Writer + DecodeType string + DecodeColumnType map[string]string +} + +// PrintRows prints the list of values. +func (w *Printer) PrintRows(rs []*bigtable.Row) { + for _, r := range rs { + w.PrintRow(r) + } +} + +// PrintRow prints the value. +func (w *Printer) PrintRow(r *bigtable.Row) { + fmt.Fprintln(w.OutStream, strings.Repeat("-", 40)) + fmt.Fprintln(w.OutStream, r.Key) + + for _, c := range r.Columns { + fmt.Fprintf(w.OutStream, " %-40s @ %s\n", c.Qualifier, c.Version.Format("2006/01/02-15:04:05.000000")) + w.printValue(c.Qualifier, c.Value) + } +} + +func (w *Printer) printValue(q string, v []byte) { + // extract columnName in a qualifier + // qualifier format: "columnFamily:columnName" + q = q[strings.Index(q, ":")+1:] + + // retrieve decode each columns + // decodeColumns format "column1:type1,column2:type2,..." + for column, decode := range w.DecodeColumnType { + if q == column { + w.doPrint(decode, v) + return + } + } + + // invoke print with a general DecodeType + w.doPrint(w.DecodeType, v) +} + +func (w *Printer) doPrint(decode string, v []byte) { + if len(v) != 8 { + fmt.Fprintf(w.OutStream, " %q\n", v) + return + } + + switch decode { + case DecodeTypeInt: + fmt.Fprintf(w.OutStream, " %d\n", w.byte2Int(v)) + case DecodeTypeFloat: + fmt.Fprintf(w.OutStream, " %f\n", w.byte2Float(v)) + case DecodeTypeString: + default: + fmt.Fprintf(w.OutStream, " %q\n", v) + } +} + +func (*Printer) byte2Int(b []byte) int64 { + return (int64)(binary.BigEndian.Uint64(b)) +} + +func (*Printer) byte2Float(b []byte) float64 { + bits := binary.BigEndian.Uint64(b) + return math.Float64frombits(bits) +} diff --git a/api/interfaces/printer_test.go b/pkg/printer/printer_test.go similarity index 76% rename from api/interfaces/printer_test.go rename to pkg/printer/printer_test.go index 7b0aef3..345b4dc 100644 --- a/api/interfaces/printer_test.go +++ b/pkg/printer/printer_test.go @@ -1,4 +1,4 @@ -package interfaces +package printer import ( "bytes" @@ -6,18 +6,18 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/takashabe/btcli/api/domain" + "github.com/takashabe/btcli/pkg/bigtable" ) func TestPrintRows(t *testing.T) { cases := []struct { - input *domain.Row + input *bigtable.Row expect string }{ { - &domain.Row{ + &bigtable.Row{ Key: "a", - Columns: []*domain.Column{ + Columns: []*bigtable.Column{ { Family: "d", Qualifier: "d:row", @@ -31,11 +31,10 @@ func TestPrintRows(t *testing.T) { for _, c := range cases { var buf bytes.Buffer printer := &Printer{ - outStream: &buf, - errStream: &buf, + OutStream: &buf, } - printer.printRow(c.input) + printer.PrintRow(c.input) assert.Equal(t, c.expect, buf.String()) } } @@ -50,8 +49,8 @@ func TestPrintValue(t *testing.T) { { // decode string &Printer{ - decodeType: "string", - decodeColumnType: map[string]string{ + DecodeType: "string", + DecodeColumnType: map[string]string{ "r": "int", "ro": "float", }, @@ -63,8 +62,8 @@ func TestPrintValue(t *testing.T) { { // decode float &Printer{ - decodeType: "string", - decodeColumnType: map[string]string{ + DecodeType: "string", + DecodeColumnType: map[string]string{ "r": "int", "ro": "float", }, @@ -76,8 +75,8 @@ func TestPrintValue(t *testing.T) { { // decode int &Printer{ - decodeType: "string", - decodeColumnType: map[string]string{ + DecodeType: "string", + DecodeColumnType: map[string]string{ "r": "int", "ro": "float", }, @@ -96,8 +95,7 @@ func TestPrintValue(t *testing.T) { } for _, c := range cases { var buf bytes.Buffer - c.printer.outStream = &buf - c.printer.errStream = &buf + c.printer.OutStream = &buf c.printer.printValue(c.qualifier, c.value) assert.Equal(t, c.expect, strings.TrimSpace(buf.String()))