mirror of
				https://github.com/TomWright/dasel.git
				synced 2022-05-22 02:32:45 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			209 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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
 | |
| }
 | 
