diff --git a/db/sql/db.go b/db/sql/db.go new file mode 100644 index 0000000..8be183f --- /dev/null +++ b/db/sql/db.go @@ -0,0 +1,214 @@ +package sql + +import ( + "database/sql" + "fmt" + + sq "github.com/Masterminds/squirrel" + "github.com/blockloop/scan/v2" +) + +type Logger interface { + Debugf(format string, args ...any) +} + +type Database struct { + sql *sql.DB + log Logger + placeholderFormat sq.PlaceholderFormat + DebugEnabled bool +} + +func (db *Database) builder() sq.StatementBuilderType { + return sq.StatementBuilder.PlaceholderFormat(db.placeholderFormat) +} + +func (db *Database) logQuery(query string, args ...interface{}) { + if db.DebugEnabled { + db.log.Debugf("%s\n", query) + } +} + +// ObjectGet retrieves 1 object based on provided criteria +func (db *Database) ObjectGet(table string, pred []interface{}, obj interface{}) (error, bool) { + sb := db.builder().Select("*").From(table) + if len(pred) == 1 { + sb = sb.Where(pred[0]) + } else if len(pred) > 1 { + sb = sb.Where(pred[0], pred[1:]...) + } + + query, args, err := sb.ToSql() + if err != nil { + return err, false + } + + rows, err := db.sql.Query(query, args...) + if err != nil { + if err == sql.ErrNoRows { + return nil, false + } + return NewError(query, err), false + } + defer rows.Close() + + err = scan.Row(obj, rows) + if err != nil { + if err == sql.ErrNoRows { + return nil, false + } + return err, false + } + return nil, true +} + +func (db *Database) Insert(table string, record interface{}) error { + m := GetRecordMap(record) + + query, args, err := db.builder().Insert(table).SetMap(m).ToSql() + if err != nil { + return NewError(query, err) + } + + db.logQuery(query) + + stmt, err := db.sql.Prepare(query) + if err != nil { + return NewError(query, err) + } + + _, err = stmt.Exec(args...) + if err != nil { + return err // Concern maybe no rows? + } + defer stmt.Close() + return nil +} + +// ListRows performs a generic list from a db based on the passed in parameters +func (db *Database) ListRows(table string, pred []interface{}, order []string, scanFunc func(rows *sql.Rows) error) error { + sb := db.builder().Select("*").From(table) + if len(pred) == 1 { + sb = sb.Where(pred[0]) + } else if len(pred) > 1 { + sb = sb.Where(pred[0], pred[1:]...) + } + + if len(order) != 0 { + sb = sb.OrderBy(order...) + } + + query, args, err := sb.ToSql() + if err != nil { + return err + } + + db.logQuery(query, args...) + + rows, err := db.SQL().Query(query, args...) + if err != nil { + if err == sql.ErrNoRows { + return nil + } + return err + } + defer rows.Close() + return scanFunc(rows) +} + +func (db *Database) Update(table string, constraint string, value interface{}, record interface{}) error { + m := GetRecordMap(record) + delete(m, constraint) + + query, args, err := db.builder().Update(table).SetMap(m).Where(sq.Eq{constraint: value}).ToSql() + if err != nil { + return NewError(query, err) + } + + db.logQuery(query, args...) + + stmt, err := db.sql.Prepare(query) + if err != nil { + return NewError(query, err) + } + + result, err := stmt.Exec(args...) + if err != nil { + return NewError(query, err) + } + defer stmt.Close() + + c, err := result.RowsAffected() + if err != nil { + return err + } + + if c == 0 { + return sql.ErrNoRows + } + return nil +} + +func (db *Database) InsertOrUpdate(table string, constraint string, value interface{}, record interface{}) error { + err := db.Update(table, constraint, value, record) + if err == sql.ErrNoRows { + return db.Insert(table, record) + } + return err +} + +// CountRows counts the number of rows matching the where clause +func (db *Database) CountRows(table string, where string) (int, error) { + query := fmt.Sprintf("select count(*) from %s", table) + + if where != "" { + query += fmt.Sprintf(" WHERE %s", where) + } + query += ";" + + db.logQuery(query) + + count := 0 + row := db.SQL().QueryRow(query) + err := row.Scan(&count) + if err != nil { + if err == sql.ErrNoRows { + return 0, nil + } + return 0, NewError(query, err) + } + return count, nil +} + +// Close closes the db connection +func (db *Database) Close() error { + if db.sql != nil { + return db.sql.Close() + } + return nil +} + +// SQL returns the accessor to the sql driver under the covers.. used for importing data +func (db *Database) SQL() *sql.DB { + return db.sql +} + +func New(dbURN string) (*Database, error) { + if dbURN == "" { + return nil, fmt.Errorf("no database URN provided") + } + + sqlDB, err := sql.Open("postgres", dbURN) + if err != nil { + return nil, err + } + + err = sqlDB.Ping() + if err != nil { + _ = sqlDB.Close() + return nil, err + } + return &Database{ + sql: sqlDB, + }, nil +} diff --git a/db/sql/error.go b/db/sql/error.go new file mode 100644 index 0000000..d69f3b0 --- /dev/null +++ b/db/sql/error.go @@ -0,0 +1,21 @@ +package sql + +import ( + "fmt" +) + +type SQLError struct { + Query string + Err error +} + +func (e *SQLError) Error() string { + return fmt.Sprintf("%s [%s]", e.Err, e.Query) +} + +func NewError(query string, err error) *SQLError { + return &SQLError{ + Query: query, + Err: err, + } +} diff --git a/db/sql/utility.go b/db/sql/utility.go new file mode 100644 index 0000000..b79daa7 --- /dev/null +++ b/db/sql/utility.go @@ -0,0 +1,31 @@ +package sql + +import ( + "reflect" +) + +func GetRecordMap(s interface{}) map[string]interface{} { + rv := reflect.ValueOf(s) + rt := reflect.TypeOf(s) + + if rt.Kind() == reflect.Ptr { + rv = rv.Elem() + rt = rt.Elem() + } else { + panic("must be ptr") + } + + fields := make(map[string]interface{}) + // rt := reflect.TypeOf(s) + //rv := reflect.ValueOf(s) + for i := 0; i < rt.NumField(); i++ { + field := rt.Field(i) + dbKey := field.Tag.Get("db") + if dbKey == "" { + continue + + } + fields[dbKey] = rv.Field(i).Interface() + } + return fields +} diff --git a/db/sql/where.go b/db/sql/where.go new file mode 100644 index 0000000..09303a9 --- /dev/null +++ b/db/sql/where.go @@ -0,0 +1,15 @@ +package sql + +/* +type Where struct { +} + +func (w *Where) And() { +} + +func (w *Where) Or() { +} + +func (w *Where) String() string { +} +*/ diff --git a/generics/map.go b/generics/map.go new file mode 100644 index 0000000..0060e9b --- /dev/null +++ b/generics/map.go @@ -0,0 +1,22 @@ +package generics + +// FlattenMap takes a map and flattens it to an array +func FlattenMap[K comparable, V any](m map[K]V) []V { + slice := make([]V, 0, len(m)) + for k := range m { + slice = append(slice, m[k]) + } + return slice +} + +// FlattenMapOrdered takes a map content in a specific order +func FlattenMapOrdered[K comparable, V any](m map[K]V, orderBy []K) []V { + slice := make([]V, 0, len(orderBy)) + + for _, key := range orderBy { + if v, ok := m[key]; ok { + slice = append(slice, v) + } + } + return slice +} diff --git a/go.mod b/go.mod index 9748bfb..3ce276b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module git.twelvetwelve.org/library/core go 1.20 + +require ( + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/blockloop/scan/v2 v2.5.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + golang.org/x/text v0.12.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3f40946 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/blockloop/scan/v2 v2.5.0 h1:/yNcCwftYn3wf5BJsJFO9E9P48l45wThdUnM3WcDF+o= +github.com/blockloop/scan/v2 v2.5.0/go.mod h1:OFYyMocUdRW3DUWehPI/fSsnpNMUNiyUaYXRMY5NMIY= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/iox/writer_progress.go b/iox/writer_progress.go new file mode 100644 index 0000000..d73e9ed --- /dev/null +++ b/iox/writer_progress.go @@ -0,0 +1,31 @@ +package iox + +import ( + "io" + "time" +) + +type WriteProgressFunc func(sofar int64) + +type WriteProgress struct { + WriteTo io.Writer + last time.Time + count int64 + + Progress WriteProgressFunc +} + +func (wc *WriteProgress) Done() { + wc.Progress(wc.count) +} + +func (wc *WriteProgress) Write(p []byte) (int, error) { + wc.count += int64(len(p)) + + now := time.Now() + if now.Sub(wc.last) > time.Duration(time.Second*1) { + wc.last = now + wc.Progress(wc.count) + } + return wc.WriteTo.Write(p) +} diff --git a/json/io.go b/json/io.go new file mode 100644 index 0000000..5b18b99 --- /dev/null +++ b/json/io.go @@ -0,0 +1,26 @@ +package json + +import ( + "encoding/json" + "io/fs" + "os" +) + +func ReadFromFile(filename string, v interface{}) error { + raw, err := os.ReadFile(filename) + if err != nil { + return err + } + + return json.Unmarshal(raw, v) +} + +func WriteToFile(filename string, v interface{}, mode fs.FileMode) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + err = os.WriteFile(filename, b, mode) + return err +} diff --git a/json/io_test.go b/json/io_test.go new file mode 100644 index 0000000..5679f2b --- /dev/null +++ b/json/io_test.go @@ -0,0 +1,36 @@ +package json + +import ( + "os" + "testing" + + "git.twelvetwelve.org/library/core/testutil/assert" +) + +type testIoStruct struct { + Value string + Count int +} + +func TestIo(t *testing.T) { + tmpFile := "io_test.tmp" + + v := testIoStruct{ + Value: "test", + Count: 999, + } + + defer os.Remove(tmpFile) + + err := ReadFromFile(tmpFile, &v) + assert.NotNil(t, err) + + err = WriteToFile(tmpFile, &v, 0644) + assert.NoError(t, err) + + read := testIoStruct{} + + err = ReadFromFile(tmpFile, &read) + assert.NoError(t, err) + assert.Equal(t, v, read) +} diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 0000000..0f206ca --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,10 @@ +package log + +import ( + "testing" +) + +func TestPrintf(t *testing.T) { + Printf("This is a test\n") + Printf("Another test\n") +} diff --git a/reflectutils/utils.go b/reflectutils/utils.go new file mode 100644 index 0000000..0bb45e1 --- /dev/null +++ b/reflectutils/utils.go @@ -0,0 +1,19 @@ +package reflectutils + +import ( + "fmt" + "reflect" +) + +func GetTagValueRequired(t reflect.StructField, name string) (string, error) { + tag, ok := t.Tag.Lookup(name) + if !ok { + return "", fmt.Errorf("field missing required tag: %s", name) + } + return tag, nil +} + +func GetTagValue(t reflect.StructField, name string) string { + tag, _ := t.Tag.Lookup(name) + return tag +} diff --git a/slog/log.go b/slog/log.go new file mode 100644 index 0000000..a5f0fda --- /dev/null +++ b/slog/log.go @@ -0,0 +1,109 @@ +package slog + +import ( + "encoding/json" + "io" + "os" + "strings" +) + +type LogWriter interface { + WriteLog(msg string, kvs map[string]interface{}) +} + +func Printf(format string, args ...any) { + formatLoc := format + + optNum := 0 + + for bofs := strings.Index(formatLoc, "{{"); bofs != -1; bofs = strings.Index(formatLoc, "{{") { + if optNum > len(args) { + break + } + + os.Stdout.Write([]byte(formatLoc[0:bofs])) + + formatLoc = formatLoc[bofs+2:] + + eofs := strings.Index(formatLoc, "}}") + if eofs == -1 { + break + } + + b, _ := json.Marshal(args[optNum]) + + os.Stdout.Write(b) + + optNum++ + + formatLoc = formatLoc[eofs+2:] + + } + os.Stdout.Write([]byte(formatLoc)) +} + +type Logger struct { + w LogWriter +} + +type KV struct { + Key string + Value any +} + +func (p *Logger) Printf(format string, args ...interface{}) { + formatLoc := format + optNum := 0 + + kvs := make(map[string]interface{}) + for bofs := strings.Index(formatLoc, "{{"); bofs != -1; bofs = strings.Index(formatLoc, "{{") { + if optNum > len(args) { + break + } + + formatLoc = formatLoc[bofs+2:] + eofs := strings.Index(formatLoc, "}}") + if eofs == -1 { + break + } + + key := formatLoc[0:eofs] + + kvs[key] = args[optNum] + optNum++ + formatLoc = formatLoc[eofs+2:] + } + + p.w.WriteLog(format, kvs) +} + +func NewLogger(w LogWriter) *Logger { + return &Logger{ + w: w, + } +} + +type JsonAdapter struct { + w io.Writer +} + +func NewJsonAdapter(w io.Writer) LogWriter { + return &JsonAdapter{w: w} +} + +func (j *JsonAdapter) WriteLog(msg string, kvs map[string]interface{}) { + kvs["Log"] = msg + b, _ := json.Marshal(kvs) + j.w.Write(b) +} + +type NullAdapter struct { + w io.Writer +} + +func NewNullAdapter() LogWriter { + return &NullAdapter{} +} + +func (j *NullAdapter) WriteLog(msg string, kvs map[string]interface{}) { +} diff --git a/slog/log_test.go b/slog/log_test.go new file mode 100644 index 0000000..349c88d --- /dev/null +++ b/slog/log_test.go @@ -0,0 +1,9 @@ +package slog + +import ( + "testing" +) + +func TestPrintf(t *testing.T) { + Printf("This is the {{Test}} for {{Idea}} stuff\n", 1, "Fancy Logging") +} diff --git a/testutil/assert/asserts.go b/testutil/assert/asserts.go index 72e6800..eb11204 100644 --- a/testutil/assert/asserts.go +++ b/testutil/assert/asserts.go @@ -28,3 +28,9 @@ func NotNil(t *testing.T, v interface{}) { t.Fatalf("nil") } } + +func NoError(t *testing.T, err error) { + if err != nil { + t.Fatalf(err.Error()) + } +} diff --git a/user/userconfig.go b/user/userconfig.go new file mode 100644 index 0000000..89f0141 --- /dev/null +++ b/user/userconfig.go @@ -0,0 +1,48 @@ +package user + +import ( + "encoding/json" + "os" + "path" + + "github.com/mitchellh/go-homedir" +) + +func ReadUserConfig(appName, configName string, config interface{}) error { + dir, err := homedir.Dir() + if err != nil { + return err + } + + filePath := path.Join(dir, ".config", appName, configName) + + data, err := os.ReadFile(filePath) + if err != nil { + return err + } + + return json.Unmarshal(data, config) +} + +func WriteUserConfig(appName, configName string, config interface{}) error { + dir, err := homedir.Dir() + if err != nil { + return err + } + + data, err := json.Marshal(config) + if err != nil { + return err + } + + filePath := path.Join(dir, ".config", appName) + + err = os.MkdirAll(filePath, 0700) + if err != nil { + return err + } + + filePath = path.Join(filePath, configName) + + return os.WriteFile(filePath, data, 0700) +} diff --git a/writer/table/csv.go b/writer/table/csv.go new file mode 100644 index 0000000..074c076 --- /dev/null +++ b/writer/table/csv.go @@ -0,0 +1,48 @@ +package table + +import ( + "fmt" + "io" +) + +type CSVPrinter struct { + writer io.Writer +} + +// Headers specify the table headers +func (p *CSVPrinter) Headers(headers ...string) error { + for _, header := range headers { + if _, err := fmt.Fprintf(p.writer, "%s,", header); err != nil { + return err + } + } + _, err := fmt.Fprintf(p.writer, "\n") + return err +} + +// Fields add another row +func (p *CSVPrinter) Fields(fields ...interface{}) error { + for _, field := range fields { + if _, err := fmt.Fprintf(p.writer, "%v,", field); err != nil { + return err + } + } + _, err := fmt.Fprintf(p.writer, "\n") + return err +} + +func (p *CSVPrinter) StringFields(fields ...string) error { + for _, field := range fields { + if _, err := fmt.Fprintf(p.writer, "%v,", field); err != nil { + return err + } + + } + _, err := fmt.Fprintf(p.writer, "\n") + return err +} + +// Flush implements the flush interface but for CVS printer is a noop +func (p *CSVPrinter) Flush() error { + return nil +} diff --git a/writer/table/csv_test.go b/writer/table/csv_test.go new file mode 100644 index 0000000..63a416b --- /dev/null +++ b/writer/table/csv_test.go @@ -0,0 +1,51 @@ +package table + +import ( + "bytes" + "testing" +) + +func TestCSVHeaders(t *testing.T) { + buffer := new(bytes.Buffer) + printer := &CSVPrinter{writer: buffer} + + err := printer.Headers("Name", "Age") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := "Name,Age,\n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} + +func TestCSVFields(t *testing.T) { + buffer := new(bytes.Buffer) + printer := &CSVPrinter{writer: buffer} + + err := printer.Fields("John Doe", 30) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := "John Doe,30,\n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} + +func TestCSVStringFields(t *testing.T) { + buffer := new(bytes.Buffer) + printer := &CSVPrinter{writer: buffer} + + err := printer.StringFields("John Doe", "30") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := "John Doe,30,\n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} diff --git a/writer/table/human.go b/writer/table/human.go new file mode 100644 index 0000000..d3511c0 --- /dev/null +++ b/writer/table/human.go @@ -0,0 +1,48 @@ +package table + +import ( + "fmt" + "text/tabwriter" +) + +// HumanPrinter prints tabulated data as a table +type HumanPrinter struct { + writer *tabwriter.Writer + fields int +} + +// Headers specify the table headers +func (p *HumanPrinter) Headers(headers ...string) error { + for _, header := range headers { + if _, err := fmt.Fprintf(p.writer, "%s\t", header); err != nil { + return nil + } + } + _, err := fmt.Fprintf(p.writer, "\n") + return err +} + +func (p *HumanPrinter) Fields(fields ...interface{}) error { + for _, header := range fields { + if _, err := fmt.Fprintf(p.writer, "%v\t", header); err != nil { + return err + } + } + _, err := fmt.Fprintf(p.writer, "\n") + return err +} + +func (p *HumanPrinter) StringFields(fields ...string) error { + for _, header := range fields { + if _, err := fmt.Fprintf(p.writer, "%v\t", header); err != nil { + return err + } + } + _, err := fmt.Fprintf(p.writer, "\n") + return err +} + +// Flush prints the data set +func (p *HumanPrinter) Flush() error { + return p.writer.Flush() +} diff --git a/writer/table/human_test.go b/writer/table/human_test.go new file mode 100644 index 0000000..5ecd028 --- /dev/null +++ b/writer/table/human_test.go @@ -0,0 +1,58 @@ +package table + +import ( + "bytes" + "testing" + "text/tabwriter" +) + +func TestHeaders(t *testing.T) { + buffer := new(bytes.Buffer) + writer := tabwriter.NewWriter(buffer, 0, 0, 1, ' ', 0) + printer := &HumanPrinter{writer: writer} + + err := printer.Headers("Name", "Age") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + printer.Flush() + + expected := "Name Age \n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} + +func TestFields(t *testing.T) { + buffer := new(bytes.Buffer) + writer := tabwriter.NewWriter(buffer, 0, 0, 1, ' ', 0) + printer := &HumanPrinter{writer: writer} + + err := printer.Fields("John Doe", 30) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + printer.Flush() + + expected := "John Doe 30 \n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} + +func TestStringFields(t *testing.T) { + buffer := new(bytes.Buffer) + writer := tabwriter.NewWriter(buffer, 0, 0, 1, ' ', 0) + printer := &HumanPrinter{writer: writer} + + err := printer.StringFields("John Doe", "30") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + printer.Flush() + + expected := "John Doe 30 \n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} diff --git a/writer/table/json.go b/writer/table/json.go new file mode 100644 index 0000000..32535de --- /dev/null +++ b/writer/table/json.go @@ -0,0 +1,93 @@ +package table + +import ( + "fmt" + "io" + "reflect" +) + +type JSONPrinter struct { + headers []string + writer io.Writer + continued bool +} + +// Headers specify the table headers +func (p *JSONPrinter) Headers(headers ...string) error { + p.headers = make([]string, len(headers), len(headers)) + for idx := range headers { + p.headers[idx] = headers[idx] + } + _, err := fmt.Fprintf(p.writer, "[\n") + return err +} + +func (p *JSONPrinter) preamble() error { + if p.continued { + if _, err := fmt.Fprintf(p.writer, ",\n"); err != nil { + return err + } + } + if _, err := fmt.Fprintf(p.writer, "{\n"); err != nil { + return err + } + return nil +} + +// Fields add another row +func (p *JSONPrinter) Fields(fields ...interface{}) error { + if err := p.preamble(); err != nil { + return err + } + + eol := ",\n" + for idx := range fields { + var err error + + if idx == len(fields)-1 { + eol = "\n" + } + + field := fields[idx] + switch field.(type) { + case string: + _, err = fmt.Fprintf(p.writer, "\"%s\":\"%v\"%s", p.headers[idx], field, eol) + case int, uint, int8, uint8, int32, uint32, int64, uint64, float32, float64: + _, err = fmt.Fprintf(p.writer, "\"%s\": %v%s", p.headers[idx], field, eol) + default: + err = fmt.Errorf("unsupported field type : %s", reflect.TypeOf(field).Name()) + } + if err != nil { + return err + } + } + p.continued = true + _, err := fmt.Fprintf(p.writer, "}\n") + return err +} + +func (p *JSONPrinter) StringFields(fields ...string) error { + if err := p.preamble(); err != nil { + return err + } + + eol := ",\n" + for idx, field := range fields { + if idx == len(fields)-1 { + eol = "\n" + } + _, err := fmt.Fprintf(p.writer, "\"%s\":\"%v\"%s", p.headers[idx], field, eol) + if err != nil { + return err + } + } + p.continued = true + _, err := fmt.Fprintf(p.writer, "}\n") + return err +} + +// Flush implements the flush interface but for CVS printer is a noop +func (p *JSONPrinter) Flush() error { + _, err := fmt.Fprintf(p.writer, "]\n") + return err +} diff --git a/writer/table/json_test.go b/writer/table/json_test.go new file mode 100644 index 0000000..f9245fb --- /dev/null +++ b/writer/table/json_test.go @@ -0,0 +1,66 @@ +package table + +import ( + "bytes" + "testing" +) + +func TestJSONHeaders(t *testing.T) { + buffer := new(bytes.Buffer) + printer := &JSONPrinter{writer: buffer} + + err := printer.Headers("Name", "Age") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := "[\n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} + +func TestJSONFields(t *testing.T) { + buffer := new(bytes.Buffer) + printer := &JSONPrinter{writer: buffer, headers: []string{"Name", "Age"}} + + err := printer.Fields("John Doe", 30) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := "{\n\"Name\":\"John Doe\",\n\"Age\": 30\n}\n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} + +func TestJSONStringFields(t *testing.T) { + buffer := new(bytes.Buffer) + printer := &JSONPrinter{writer: buffer, headers: []string{"Name", "Age"}} + + err := printer.StringFields("John Doe", "30") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := "{\n\"Name\":\"John Doe\",\n\"Age\":\"30\"\n}\n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} + +func TestJSONFlush(t *testing.T) { + buffer := new(bytes.Buffer) + printer := &JSONPrinter{writer: buffer} + + err := printer.Flush() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := "]\n" + if buffer.String() != expected { + t.Errorf("Expected '%s', got '%s'", expected, buffer.String()) + } +} diff --git a/writer/table/printer.go b/writer/table/printer.go new file mode 100644 index 0000000..fcf540b --- /dev/null +++ b/writer/table/printer.go @@ -0,0 +1,50 @@ +package table + +import ( + "io" + "os" + "text/tabwriter" +) + +// TabulatedPrinter defines an interface we can use for a common way to print tabulated data +// +// ie we canb use the same interface to display results as table, csv, json, etc +type TabulatedPrinter interface { + // Headers defines the headers for the values + Headers(...string) error + + //Fields appends fields to the printer + Fields(...interface{}) error + StringFields(...string) error + + // Flush outputs the tabulated data + Flush() error +} + +// NewPrinter returns a new tabulated printer type based on the format specified +// format: can be human, csv +func NewPrinter(format string, w io.Writer) TabulatedPrinter { + if w == nil { + w = os.Stdout + } + + switch format { + case "csv": + return &CSVPrinter{ + writer: w, + } + + case "json": + return &JSONPrinter{ + writer: w, + } + + case "human": + p := &HumanPrinter{ + writer: new(tabwriter.Writer), + } + p.writer.Init(w, 0, 8, 2, ' ', 0) + return p + } + return nil +} diff --git a/writer/table/printer_test.go b/writer/table/printer_test.go new file mode 100644 index 0000000..eff5472 --- /dev/null +++ b/writer/table/printer_test.go @@ -0,0 +1 @@ +package table