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()))