latest updayes
This commit is contained in:
parent
8f2da7be66
commit
9f42c8f5bc
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package sql
|
||||
|
||||
/*
|
||||
type Where struct {
|
||||
}
|
||||
|
||||
func (w *Where) And() {
|
||||
}
|
||||
|
||||
func (w *Where) Or() {
|
||||
}
|
||||
|
||||
func (w *Where) String() string {
|
||||
}
|
||||
*/
|
|
@ -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
|
||||
}
|
8
go.mod
8
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
|
||||
)
|
||||
|
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrintf(t *testing.T) {
|
||||
Printf("This is a test\n")
|
||||
Printf("Another test\n")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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{}) {
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package table
|
Loading…
Reference in New Issue