1
0
mirror of https://github.com/TomWright/dasel.git synced 2022-05-22 02:32:45 +03:00
Files
dasel-data-selector/storage/csv.go
2022-03-28 13:46:44 +01:00

209 lines
4.7 KiB
Go

package storage
import (
"bytes"
"encoding/csv"
"fmt"
"sort"
)
func init() {
registerReadParser([]string{"csv"}, []string{".csv"}, &CSVParser{})
registerWriteParser([]string{"csv"}, []string{".csv"}, &CSVParser{})
}
// CSVParser is a Parser implementation to handle csv files.
type CSVParser struct {
}
// CSVDocument represents a CSV file.
// This is required to keep headers in the expected order.
type CSVDocument struct {
originalRequired
Value []map[string]interface{}
Headers []string
}
// RealValue returns the real value that dasel should use when processing data.
func (d *CSVDocument) RealValue() interface{} {
return d.Value
}
// Documents returns the documents that should be written to output.
func (d *CSVDocument) Documents() []interface{} {
res := make([]interface{}, len(d.Value))
for i := range d.Value {
res[i] = d.Value[i]
}
return res
}
// FromBytes returns some data that is represented by the given bytes.
func (p *CSVParser) FromBytes(byteData []byte) (interface{}, error) {
if byteData == nil {
return nil, fmt.Errorf("could not read csv file: no data")
}
reader := csv.NewReader(bytes.NewBuffer(byteData))
res := make([]map[string]interface{}, 0)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("could not read csv file: %w", err)
}
if len(records) == 0 {
return nil, nil
}
var headers []string
for i, row := range records {
if i == 0 {
headers = row
continue
}
rowRes := make(map[string]interface{})
allEmpty := true
for index, val := range row {
if val != "" {
allEmpty = false
}
rowRes[headers[index]] = val
}
if !allEmpty {
res = append(res, rowRes)
}
}
return &CSVDocument{
Value: res,
Headers: headers,
}, nil
}
func interfaceToCSVDocument(val interface{}) (*CSVDocument, error) {
switch v := val.(type) {
case map[string]interface{}:
headers := make([]string, 0)
for k := range v {
headers = append(headers, k)
}
sort.Strings(headers)
return &CSVDocument{
Value: []map[string]interface{}{v},
Headers: headers,
}, nil
case []interface{}:
mapVals := make([]map[string]interface{}, 0)
headers := make([]string, 0)
for _, val := range v {
if x, ok := val.(map[string]interface{}); ok {
mapVals = append(mapVals, x)
for objectKey := range x {
found := false
for _, existingHeader := range headers {
if existingHeader == objectKey {
found = true
break
}
}
if !found {
headers = append(headers, objectKey)
}
}
}
}
sort.Strings(headers)
return &CSVDocument{
Value: mapVals,
Headers: headers,
}, nil
default:
return nil, fmt.Errorf("CSVParser.toBytes cannot handle type %T", val)
}
}
// ToBytes returns a slice of bytes that represents the given value.
func (p *CSVParser) ToBytes(value interface{}, options ...ReadWriteOption) ([]byte, error) {
buffer := new(bytes.Buffer)
writer := csv.NewWriter(buffer)
// Allow for multi document output by just appending documents on the end of each other.
// This is really only supported so as we have nicer output when converting to CSV from
// other multi-document formats.
docs := make([]*CSVDocument, 0)
switch d := value.(type) {
case *CSVDocument:
docs = append(docs, d)
case SingleDocument:
doc, err := interfaceToCSVDocument(d.Document())
if err != nil {
return nil, err
}
docs = append(docs, doc)
case MultiDocument:
for _, dd := range d.Documents() {
doc, err := interfaceToCSVDocument(dd)
if err != nil {
return nil, err
}
docs = append(docs, doc)
}
default:
return []byte(fmt.Sprintf("%v\n", value)), nil
}
for _, doc := range docs {
if err := p.toBytesHandleDoc(writer, doc); err != nil {
return nil, err
}
}
return append(buffer.Bytes()), nil
}
func (p *CSVParser) toBytesHandleDoc(writer *csv.Writer, doc *CSVDocument) error {
// Iterate through the rows and detect any new headers.
for _, r := range doc.Value {
for k := range r {
headerExists := false
for _, header := range doc.Headers {
if k == header {
headerExists = true
break
}
}
if !headerExists {
doc.Headers = append(doc.Headers, k)
}
}
}
// Iterate through the rows and write the output.
for i, r := range doc.Value {
if i == 0 {
if err := writer.Write(doc.Headers); err != nil {
return fmt.Errorf("could not write headers: %w", err)
}
}
values := make([]string, 0)
for _, header := range doc.Headers {
val, ok := r[header]
if !ok {
val = ""
}
values = append(values, fmt.Sprint(val))
}
if err := writer.Write(values); err != nil {
return fmt.Errorf("could not write values: %w", err)
}
writer.Flush()
}
return nil
}