1
0
mirror of https://github.com/TomWright/dasel.git synced 2022-05-22 02:32:45 +03:00

Expose the storage package

This commit is contained in:
Tom Wright
2022-03-28 13:46:44 +01:00
parent eabe44d3f6
commit 425a6463d5
27 changed files with 30 additions and 20 deletions

25
storage/colourise.go Normal file
View File

@@ -0,0 +1,25 @@
package storage
import (
"bytes"
"github.com/alecthomas/chroma/quick"
)
// ColouriseStyle is the style used when colourising output.
const ColouriseStyle = "solarized-dark256"
// ColouriseFormatter is the formatter used when colourising output.
const ColouriseFormatter = "terminal"
// ColouriseBuffer colourises the given buffer in-place.
func ColouriseBuffer(content *bytes.Buffer, lexer string) error {
contentString := content.String()
content.Reset()
return quick.Highlight(content, contentString, lexer, ColouriseFormatter, ColouriseStyle)
}
// Colourise colourises the given string.
func Colourise(content string, lexer string) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
return buf, quick.Highlight(buf, content, lexer, ColouriseFormatter, ColouriseStyle)
}

208
storage/csv.go Normal file
View File

@@ -0,0 +1,208 @@
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
}

217
storage/csv_test.go Normal file
View File

@@ -0,0 +1,217 @@
package storage_test
import (
"github.com/tomwright/dasel/storage"
"reflect"
"testing"
)
var csvBytes = []byte(`id,name
1,Tom
2,Jim
`)
var csvMap = []map[string]interface{}{
{
"id": "1",
"name": "Tom",
},
{
"id": "2",
"name": "Jim",
},
}
func TestCSVParser_FromBytes(t *testing.T) {
got, err := (&storage.CSVParser{}).FromBytes(csvBytes)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(&storage.CSVDocument{
Value: csvMap,
Headers: []string{"id", "name"},
}, got) {
t.Errorf("expected %v, got %v", csvMap, got)
}
}
func TestCSVParser_FromBytes_Error(t *testing.T) {
_, err := (&storage.CSVParser{}).FromBytes(nil)
if err == nil {
t.Errorf("expected error but got none")
return
}
_, err = (&storage.CSVParser{}).FromBytes([]byte(`a,b
a,b,c`))
if err == nil {
t.Errorf("expected error but got none")
return
}
_, err = (&storage.CSVParser{}).FromBytes([]byte(`a,b,c
a,b`))
if err == nil {
t.Errorf("expected error but got none")
return
}
}
func TestCSVParser_ToBytes(t *testing.T) {
t.Run("CSVDocument", func(t *testing.T) {
got, err := (&storage.CSVParser{}).ToBytes(&storage.CSVDocument{
Value: csvMap,
Headers: []string{"id", "name"},
})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(csvBytes, got) {
t.Errorf("expected %v, got %v", string(csvBytes), string(got))
}
})
t.Run("SingleDocument", func(t *testing.T) {
got, err := (&storage.CSVParser{}).ToBytes(&storage.BasicSingleDocument{
Value: map[string]interface{}{
"id": "1",
"name": "Tom",
},
})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
deepEqualOneOf(t, got, []byte(`id,name
1,Tom
`), []byte(`name,id
Tom,1
`))
})
t.Run("SingleDocumentSlice", func(t *testing.T) {
got, err := (&storage.CSVParser{}).ToBytes(&storage.BasicSingleDocument{
Value: []interface{}{
map[string]interface{}{
"id": "1",
"name": "Tom",
},
map[string]interface{}{
"id": "2",
"name": "Tommy",
},
},
})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
deepEqualOneOf(t, got, []byte(`id,name
1,Tom
2,Tommy
`), []byte(`name,id
Tom,1
`))
})
t.Run("MultiDocument", func(t *testing.T) {
got, err := (&storage.CSVParser{}).ToBytes(&storage.BasicMultiDocument{
Values: []interface{}{
map[string]interface{}{
"id": "1",
"name": "Tom",
},
map[string]interface{}{
"id": "2",
"name": "Jim",
},
},
})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
deepEqualOneOf(t, got, []byte(`id,name
1,Tom
id,name
2,Jim
`), []byte(`name,id
Tom,1
id,name
2,Jim
`), []byte(`id,name
1,Tom
name,id
Jim,2
`), []byte(`name,id
Tom,1
name,id
Jim,2
`))
})
t.Run("DefaultDocType", func(t *testing.T) {
got, err := (&storage.CSVParser{}).ToBytes([]interface{}{"x", "y"})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
deepEqualOneOf(t, got, []byte(`[x y]
`))
})
}
func deepEqualOneOf(t *testing.T, got []byte, exps ...[]byte) {
for _, exp := range exps {
if reflect.DeepEqual(exp, got) {
return
}
}
t.Errorf("%s did not match any of the expected values", string(got))
}
func TestCSVDocument_Documents(t *testing.T) {
in := &storage.CSVDocument{
Value: []map[string]interface{}{
{
"id": 1,
"name": "Tom",
},
{
"id": 2,
"name": "Jim",
},
},
Headers: []string{"id", "name"},
}
exp := []interface{}{
map[string]interface{}{
"id": 1,
"name": "Tom",
},
map[string]interface{}{
"id": 2,
"name": "Jim",
},
}
got := in.Documents()
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
}
func TestCSVDocument_RealValue(t *testing.T) {
exp := []map[string]interface{}{
{
"id": 1,
"name": "Tom",
},
{
"id": 2,
"name": "Jim",
},
}
in := &storage.CSVDocument{
Value: exp,
Headers: []string{"id", "name"},
}
got := in.RealValue()
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
}

105
storage/json.go Normal file
View File

@@ -0,0 +1,105 @@
package storage
import (
"bytes"
"encoding/json"
"fmt"
"io"
)
func init() {
registerReadParser([]string{"json"}, []string{".json"}, &JSONParser{})
registerWriteParser([]string{"json"}, []string{".json"}, &JSONParser{})
}
// JSONParser is a Parser implementation to handle json files.
type JSONParser struct {
}
// FromBytes returns some data that is represented by the given bytes.
func (p *JSONParser) FromBytes(byteData []byte) (interface{}, error) {
res := make([]interface{}, 0)
decoder := json.NewDecoder(bytes.NewBuffer(byteData))
docLoop:
for {
var docData interface{}
if err := decoder.Decode(&docData); err != nil {
if err == io.EOF {
break docLoop
}
return nil, fmt.Errorf("could not unmarshal data: %w", err)
}
res = append(res, docData)
}
switch len(res) {
case 0:
return nil, nil
case 1:
return &BasicSingleDocument{Value: res[0]}, nil
default:
return &BasicMultiDocument{Values: res}, nil
}
}
// ToBytes returns a slice of bytes that represents the given value.
func (p *JSONParser) ToBytes(value interface{}, options ...ReadWriteOption) ([]byte, error) {
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
indent := " "
prettyPrint := true
colourise := false
for _, o := range options {
switch o.Key {
case OptionIndent:
if value, ok := o.Value.(string); ok {
indent = value
}
case OptionPrettyPrint:
if value, ok := o.Value.(bool); ok {
prettyPrint = value
}
case OptionColourise:
if value, ok := o.Value.(bool); ok {
colourise = value
}
case OptionEscapeHTML:
if value, ok := o.Value.(bool); ok {
encoder.SetEscapeHTML(value)
}
}
}
if !prettyPrint {
indent = ""
}
encoder.SetIndent("", indent)
switch v := value.(type) {
case SingleDocument:
if err := encoder.Encode(v.Document()); err != nil {
return nil, fmt.Errorf("could not encode single document: %w", err)
}
case MultiDocument:
for index, d := range v.Documents() {
if err := encoder.Encode(d); err != nil {
return nil, fmt.Errorf("could not encode multi document [%d]: %w", index, err)
}
}
default:
if err := encoder.Encode(v); err != nil {
return nil, fmt.Errorf("could not encode default document type: %w", err)
}
}
if colourise {
if err := ColouriseBuffer(buffer, "json"); err != nil {
return nil, fmt.Errorf("could not colourise output: %w", err)
}
}
return buffer.Bytes(), nil
}

192
storage/json_test.go Normal file
View File

@@ -0,0 +1,192 @@
package storage_test
import (
"github.com/tomwright/dasel/storage"
"reflect"
"testing"
)
var jsonBytes = []byte(`{
"name": "Tom"
}
`)
var jsonMap = map[string]interface{}{
"name": "Tom",
}
func TestJSONParser_FromBytes(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
got, err := (&storage.JSONParser{}).FromBytes(jsonBytes)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := &storage.BasicSingleDocument{Value: jsonMap}
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
})
t.Run("ValidMultiDocument", func(t *testing.T) {
got, err := (&storage.JSONParser{}).FromBytes(jsonBytesMulti)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := &storage.BasicMultiDocument{
Values: jsonMapMulti,
}
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", jsonMap, got)
}
})
t.Run("ValidMultiDocumentMixed", func(t *testing.T) {
got, err := (&storage.JSONParser{}).FromBytes(jsonBytesMultiMixed)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := &storage.BasicMultiDocument{
Values: jsonMapMultiMixed,
}
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", jsonMap, got)
}
})
t.Run("Empty", func(t *testing.T) {
got, err := (&storage.JSONParser{}).FromBytes([]byte(``))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(nil, got) {
t.Errorf("expected %v, got %v", nil, got)
}
})
}
func TestJSONParser_FromBytes_Error(t *testing.T) {
_, err := (&storage.JSONParser{}).FromBytes(yamlBytes)
if err == nil {
t.Errorf("expected error but got none")
return
}
}
func TestJSONParser_ToBytes(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
got, err := (&storage.JSONParser{}).ToBytes(jsonMap)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(jsonBytes) != string(got) {
t.Errorf("expected %v, got %v", string(jsonBytes), string(got))
}
})
t.Run("ValidSingle", func(t *testing.T) {
got, err := (&storage.JSONParser{}).ToBytes(&storage.BasicSingleDocument{Value: jsonMap})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(jsonBytes) != string(got) {
t.Errorf("expected %v, got %v", string(jsonBytes), string(got))
}
})
t.Run("ValidSingleNoPrettyPrint", func(t *testing.T) {
res, err := (&storage.JSONParser{}).ToBytes(&storage.BasicSingleDocument{Value: jsonMap}, storage.PrettyPrintOption(false))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
got := string(res)
exp := `{"name":"Tom"}
`
if exp != got {
t.Errorf("expected %v, got %v", exp, got)
}
})
t.Run("ValidSingleColourise", func(t *testing.T) {
got, err := (&storage.JSONParser{}).ToBytes(&storage.BasicSingleDocument{Value: jsonMap}, storage.ColouriseOption(true))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
expBuf, _ := storage.Colourise(`{
"name": "Tom"
}
`, "json")
exp := expBuf.Bytes()
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
})
t.Run("ValidSingleCustomIndent", func(t *testing.T) {
res, err := (&storage.JSONParser{}).ToBytes(&storage.BasicSingleDocument{Value: jsonMap}, storage.IndentOption(" "))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
got := string(res)
exp := `{
"name": "Tom"
}
`
if exp != got {
t.Errorf("expected %v, got %v", exp, got)
}
})
t.Run("ValidMulti", func(t *testing.T) {
got, err := (&storage.JSONParser{}).ToBytes(&storage.BasicMultiDocument{Values: jsonMapMulti})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(jsonBytesMulti) != string(got) {
t.Errorf("expected %v, got %v", string(jsonBytesMulti), string(got))
}
})
t.Run("ValidMultiMixed", func(t *testing.T) {
got, err := (&storage.JSONParser{}).ToBytes(&storage.BasicMultiDocument{Values: jsonMapMultiMixed})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(jsonBytesMultiMixed) != string(got) {
t.Errorf("expected %v, got %v", string(jsonBytesMultiMixed), string(got))
}
})
}
var jsonBytesMulti = []byte(`{
"name": "Tom"
}
{
"name": "Ellis"
}
`)
var jsonMapMulti = []interface{}{
map[string]interface{}{"name": "Tom"},
map[string]interface{}{"name": "Ellis"},
}
var jsonBytesMultiMixed = []byte(`{
"name": "Tom",
"other": true
}
{
"name": "Ellis"
}
`)
var jsonMapMultiMixed = []interface{}{
map[string]interface{}{"name": "Tom", "other": true},
map[string]interface{}{"name": "Ellis"},
}

53
storage/option.go Normal file
View File

@@ -0,0 +1,53 @@
package storage
// IndentOption returns a write option that sets the given indent.
func IndentOption(indent string) ReadWriteOption {
return ReadWriteOption{
Key: OptionIndent,
Value: indent,
}
}
// PrettyPrintOption returns an option that enables or disables pretty printing.
func PrettyPrintOption(enabled bool) ReadWriteOption {
return ReadWriteOption{
Key: OptionPrettyPrint,
Value: enabled,
}
}
// ColouriseOption returns an option that enables or disables colourised output.
func ColouriseOption(enabled bool) ReadWriteOption {
return ReadWriteOption{
Key: OptionColourise,
Value: enabled,
}
}
// EscapeHTMLOption returns an option that enables or disables HTML escaping.
func EscapeHTMLOption(enabled bool) ReadWriteOption {
return ReadWriteOption{
Key: OptionEscapeHTML,
Value: enabled,
}
}
// OptionKey is a defined type for keys within a ReadWriteOption.
type OptionKey string
const (
// OptionIndent is the key used with IndentOption.
OptionIndent OptionKey = "indent"
// OptionPrettyPrint is the key used with PrettyPrintOption.
OptionPrettyPrint OptionKey = "prettyPrint"
// OptionColourise is the key used with ColouriseOption.
OptionColourise OptionKey = "colourise"
// OptionEscapeHTML is the key used with EscapeHTMLOption.
OptionEscapeHTML OptionKey = "escapeHtml"
)
// ReadWriteOption is an option to be used when writing.
type ReadWriteOption struct {
Key OptionKey
Value interface{}
}

198
storage/parser.go Normal file
View File

@@ -0,0 +1,198 @@
package storage
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
var readParsersByExtension = map[string]ReadParser{}
var writeParsersByExtension = map[string]WriteParser{}
var readParsersByName = map[string]ReadParser{}
var writeParsersByName = map[string]WriteParser{}
func registerReadParser(names []string, extensions []string, parser ReadParser) {
for _, n := range names {
readParsersByName[n] = parser
}
for _, e := range extensions {
readParsersByExtension[e] = parser
}
}
func registerWriteParser(names []string, extensions []string, parser WriteParser) {
for _, n := range names {
writeParsersByName[n] = parser
}
for _, e := range extensions {
writeParsersByExtension[e] = parser
}
}
// UnknownParserErr is returned when an invalid parser name is given.
type UnknownParserErr struct {
Parser string
}
// Error returns the error message.
func (e UnknownParserErr) Error() string {
return fmt.Sprintf("unknown parser: %s", e.Parser)
}
// ReadParser can be used to convert bytes to data.
type ReadParser interface {
// FromBytes returns some data that is represented by the given bytes.
FromBytes(byteData []byte) (interface{}, error)
}
// WriteParser can be used to convert data to bytes.
type WriteParser interface {
// ToBytes returns a slice of bytes that represents the given value.
ToBytes(value interface{}, options ...ReadWriteOption) ([]byte, error)
}
// Parser can be used to load and save files from/to disk.
type Parser interface {
ReadParser
WriteParser
}
// NewReadParserFromFilename returns a ReadParser from the given filename.
func NewReadParserFromFilename(filename string) (ReadParser, error) {
ext := strings.ToLower(filepath.Ext(filename))
p, ok := readParsersByExtension[ext]
if !ok {
return nil, &UnknownParserErr{Parser: ext}
}
return p, nil
}
// NewReadParserFromString returns a ReadParser from the given parser name.
func NewReadParserFromString(parser string) (ReadParser, error) {
p, ok := readParsersByName[parser]
if !ok {
return nil, &UnknownParserErr{Parser: parser}
}
return p, nil
}
// NewWriteParserFromFilename returns a WriteParser from the given filename.
func NewWriteParserFromFilename(filename string) (WriteParser, error) {
ext := strings.ToLower(filepath.Ext(filename))
p, ok := writeParsersByExtension[ext]
if !ok {
return nil, &UnknownParserErr{Parser: ext}
}
return p, nil
}
// NewWriteParserFromString returns a WriteParser from the given parser name.
func NewWriteParserFromString(parser string) (WriteParser, error) {
p, ok := writeParsersByName[parser]
if !ok {
return nil, &UnknownParserErr{Parser: parser}
}
return p, nil
}
// LoadFromFile loads data from the given file.
func LoadFromFile(filename string, p ReadParser) (interface{}, error) {
f, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("could not open file: %w", err)
}
return Load(p, f)
}
// Load loads data from the given io.Reader.
func Load(p ReadParser, reader io.Reader) (interface{}, error) {
byteData, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("could not read data: %w", err)
}
return p.FromBytes(byteData)
}
// Write writes the value to the given io.Writer.
func Write(p WriteParser, value interface{}, originalValue interface{}, writer io.Writer, options ...ReadWriteOption) error {
switch typed := originalValue.(type) {
case OriginalRequired:
if typed.OriginalRequired() {
value = originalValue
}
}
byteData, err := p.ToBytes(value, options...)
if err != nil {
return fmt.Errorf("could not get byte data for file: %w", err)
}
if _, err := writer.Write(byteData); err != nil {
return fmt.Errorf("could not write data: %w", err)
}
return nil
}
// OriginalRequired can be used in conjunction with RealValue to allow parsers to be more intelligent
// with the data they read/write.
type OriginalRequired interface {
// OriginalRequired tells dasel if the parser requires the original value when converting to bytes.
OriginalRequired() bool
}
// RealValue can be used in conjunction with OriginalRequired to allow parsers to be more intelligent
// with the data they read/write.
type RealValue interface {
// RealValue returns the real value that dasel should use when processing data.
RealValue() interface{}
}
type originalRequired struct {
}
// OriginalRequired tells dasel if the parser requires the original value when converting to bytes.
func (d originalRequired) OriginalRequired() bool {
return true
}
// SingleDocument is a parser result that contains a single document.
type SingleDocument interface {
Document() interface{}
}
// MultiDocument is a parser result that contains multiple documents.
type MultiDocument interface {
Documents() []interface{}
}
// BasicSingleDocument represents a single document file.
type BasicSingleDocument struct {
originalRequired
Value interface{}
}
// RealValue returns the real value that dasel should use when processing data.
func (d *BasicSingleDocument) RealValue() interface{} {
return d.Value
}
// Document returns the document that should be written to output.
func (d *BasicSingleDocument) Document() interface{} {
return d.Value
}
// BasicMultiDocument represents a multi-document file.
type BasicMultiDocument struct {
originalRequired
Values []interface{}
}
// RealValue returns the real value that dasel should use when processing data.
func (d *BasicMultiDocument) RealValue() interface{} {
return d.Values
}
// Documents returns the documents that should be written to output.
func (d *BasicMultiDocument) Documents() []interface{} {
return d.Values
}

286
storage/parser_test.go Normal file
View File

@@ -0,0 +1,286 @@
package storage_test
import (
"bytes"
"errors"
"github.com/tomwright/dasel/storage"
"reflect"
"strings"
"testing"
)
func TestUnknownParserErr_Error(t *testing.T) {
if exp, got := "unknown parser: x", (&storage.UnknownParserErr{Parser: "x"}).Error(); exp != got {
t.Errorf("expected error %s, got %s", exp, got)
}
}
func TestNewReadParserFromString(t *testing.T) {
tests := []struct {
In string
Out storage.Parser
Err error
}{
{In: "json", Out: &storage.JSONParser{}},
{In: "yaml", Out: &storage.YAMLParser{}},
{In: "yml", Out: &storage.YAMLParser{}},
{In: "toml", Out: &storage.TOMLParser{}},
{In: "xml", Out: &storage.XMLParser{}},
{In: "csv", Out: &storage.CSVParser{}},
{In: "bad", Out: nil, Err: &storage.UnknownParserErr{Parser: "bad"}},
{In: "-", Out: nil, Err: &storage.UnknownParserErr{Parser: "-"}},
}
for _, testCase := range tests {
tc := testCase
t.Run(tc.In, func(t *testing.T) {
got, err := storage.NewReadParserFromString(tc.In)
if tc.Err == nil && err != nil {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Err != nil && err == nil {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Out != got {
t.Errorf("expected result %v, got %v", tc.Out, got)
}
})
}
}
func TestNewWriteParserFromString(t *testing.T) {
tests := []struct {
In string
Out storage.Parser
Err error
}{
{In: "json", Out: &storage.JSONParser{}},
{In: "yaml", Out: &storage.YAMLParser{}},
{In: "yml", Out: &storage.YAMLParser{}},
{In: "toml", Out: &storage.TOMLParser{}},
{In: "xml", Out: &storage.XMLParser{}},
{In: "csv", Out: &storage.CSVParser{}},
{In: "-", Out: &storage.PlainParser{}},
{In: "plain", Out: &storage.PlainParser{}},
{In: "bad", Out: nil, Err: &storage.UnknownParserErr{Parser: "bad"}},
}
for _, testCase := range tests {
tc := testCase
t.Run(tc.In, func(t *testing.T) {
got, err := storage.NewWriteParserFromString(tc.In)
if tc.Err == nil && err != nil {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Err != nil && err == nil {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Out != got {
t.Errorf("expected result %v, got %v", tc.Out, got)
}
})
}
}
func TestNewReadParserFromFilename(t *testing.T) {
tests := []struct {
In string
Out storage.Parser
Err error
}{
{In: "a.json", Out: &storage.JSONParser{}},
{In: "a.yaml", Out: &storage.YAMLParser{}},
{In: "a.yml", Out: &storage.YAMLParser{}},
{In: "a.toml", Out: &storage.TOMLParser{}},
{In: "a.xml", Out: &storage.XMLParser{}},
{In: "a.csv", Out: &storage.CSVParser{}},
{In: "a.txt", Out: nil, Err: &storage.UnknownParserErr{Parser: ".txt"}},
}
for _, testCase := range tests {
tc := testCase
t.Run(tc.In, func(t *testing.T) {
got, err := storage.NewReadParserFromFilename(tc.In)
if tc.Err == nil && err != nil {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Err != nil && err == nil {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Out != got {
t.Errorf("expected result %v, got %v", tc.Out, got)
}
})
}
}
func TestNewWriteParserFromFilename(t *testing.T) {
tests := []struct {
In string
Out storage.Parser
Err error
}{
{In: "a.json", Out: &storage.JSONParser{}},
{In: "a.yaml", Out: &storage.YAMLParser{}},
{In: "a.yml", Out: &storage.YAMLParser{}},
{In: "a.toml", Out: &storage.TOMLParser{}},
{In: "a.xml", Out: &storage.XMLParser{}},
{In: "a.csv", Out: &storage.CSVParser{}},
{In: "a.txt", Out: nil, Err: &storage.UnknownParserErr{Parser: ".txt"}},
}
for _, testCase := range tests {
tc := testCase
t.Run(tc.In, func(t *testing.T) {
got, err := storage.NewWriteParserFromFilename(tc.In)
if tc.Err == nil && err != nil {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Err != nil && err == nil {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() {
t.Errorf("expected err %v, got %v", tc.Err, err)
return
}
if tc.Out != got {
t.Errorf("expected result %v, got %v", tc.Out, got)
}
})
}
}
var jsonData = map[string]interface{}{
"name": "Tom",
"preferences": map[string]interface{}{
"favouriteColour": "red",
},
"colours": []interface{}{"red", "green", "blue"},
"colourCodes": []interface{}{
map[string]interface{}{
"name": "red",
"rgb": "ff0000",
},
map[string]interface{}{
"name": "green",
"rgb": "00ff00",
},
map[string]interface{}{
"name": "blue",
"rgb": "0000ff",
},
},
}
func TestLoadFromFile(t *testing.T) {
t.Run("ValidJSON", func(t *testing.T) {
data, err := storage.LoadFromFile("../tests/assets/example.json", &storage.JSONParser{})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := &storage.BasicSingleDocument{Value: jsonData}
if !reflect.DeepEqual(exp, data) {
t.Errorf("data does not match: exp %v, got %v", exp, data)
}
})
t.Run("BaseFilePath", func(t *testing.T) {
_, err := storage.LoadFromFile("x.json", &storage.JSONParser{})
if err == nil || !strings.Contains(err.Error(), "could not open file") {
t.Errorf("unexpected error: %v", err)
return
}
})
}
func TestLoad(t *testing.T) {
t.Run("ReaderErrHandled", func(t *testing.T) {
if _, err := storage.Load(&storage.JSONParser{}, &failingReader{}); !errors.Is(err, errFailingReaderErr) {
t.Errorf("unexpected error: %v", err)
return
}
})
}
var errFailingParserErr = errors.New("i am meant to fail at parsing")
type failingParser struct {
}
func (fp *failingParser) FromBytes(_ []byte) (interface{}, error) {
return nil, errFailingParserErr
}
func (fp *failingParser) ToBytes(_ interface{}, options ...storage.ReadWriteOption) ([]byte, error) {
return nil, errFailingParserErr
}
var errFailingWriterErr = errors.New("i am meant to fail at writing")
type failingWriter struct {
}
func (fp *failingWriter) Write(_ []byte) (int, error) {
return 0, errFailingWriterErr
}
var errFailingReaderErr = errors.New("i am meant to fail at reading")
type failingReader struct {
}
func (fp *failingReader) Read(_ []byte) (n int, err error) {
return 0, errFailingReaderErr
}
func TestWrite(t *testing.T) {
t.Run("Success", func(t *testing.T) {
var buf bytes.Buffer
if err := storage.Write(&storage.JSONParser{}, map[string]interface{}{"name": "Tom"}, nil, &buf); err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if exp, got := `{
"name": "Tom"
}
`, buf.String(); exp != got {
t.Errorf("unexpected output:\n%s\ngot:\n%s", exp, got)
}
})
t.Run("ParserErrHandled", func(t *testing.T) {
var buf bytes.Buffer
if err := storage.Write(&failingParser{}, map[string]interface{}{"name": "Tom"}, nil, &buf); !errors.Is(err, errFailingParserErr) {
t.Errorf("unexpected error: %v", err)
return
}
})
t.Run("WriterErrHandled", func(t *testing.T) {
if err := storage.Write(&storage.JSONParser{}, map[string]interface{}{"name": "Tom"}, nil, &failingWriter{}); !errors.Is(err, errFailingWriterErr) {
t.Errorf("unexpected error: %v", err)
return
}
})
}

38
storage/plain.go Normal file
View File

@@ -0,0 +1,38 @@
package storage
import (
"bytes"
"fmt"
)
func init() {
registerWriteParser([]string{"-", "plain"}, []string{}, &PlainParser{})
}
// PlainParser is a Parser implementation to handle plain files.
type PlainParser struct {
}
// ErrPlainParserNotImplemented is returned when you try to use the PlainParser.FromBytes func.
var ErrPlainParserNotImplemented = fmt.Errorf("PlainParser.FromBytes not implemented")
// FromBytes returns some data that is represented by the given bytes.
func (p *PlainParser) FromBytes(byteData []byte) (interface{}, error) {
return nil, ErrPlainParserNotImplemented
}
// ToBytes returns a slice of bytes that represents the given value.
func (p *PlainParser) ToBytes(value interface{}, options ...ReadWriteOption) ([]byte, error) {
buf := new(bytes.Buffer)
switch val := value.(type) {
case SingleDocument:
buf.Write([]byte(fmt.Sprintf("%v\n", val.Document())))
case MultiDocument:
for _, doc := range val.Documents() {
buf.Write([]byte(fmt.Sprintf("%v\n", doc)))
}
default:
buf.Write([]byte(fmt.Sprintf("%v\n", val)))
}
return buf.Bytes(), nil
}

57
storage/plain_test.go Normal file
View File

@@ -0,0 +1,57 @@
package storage_test
import (
"errors"
"github.com/tomwright/dasel/storage"
"testing"
)
func TestPlainParser_FromBytes(t *testing.T) {
_, err := (&storage.PlainParser{}).FromBytes(nil)
if !errors.Is(err, storage.ErrPlainParserNotImplemented) {
t.Errorf("unexpected error: %v", err)
}
}
func TestPlainParser_ToBytes(t *testing.T) {
t.Run("Basic", func(t *testing.T) {
gotVal, err := (&storage.PlainParser{}).ToBytes("asd")
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := `asd
`
got := string(gotVal)
if exp != got {
t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
}
})
t.Run("SingleDocument", func(t *testing.T) {
gotVal, err := (&storage.PlainParser{}).ToBytes(&storage.BasicSingleDocument{Value: "asd"})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := `asd
`
got := string(gotVal)
if exp != got {
t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
}
})
t.Run("MultiDocument", func(t *testing.T) {
gotVal, err := (&storage.PlainParser{}).ToBytes(&storage.BasicMultiDocument{Values: []interface{}{"asd", "123"}})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := `asd
123
`
got := string(gotVal)
if exp != got {
t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
}
})
}

86
storage/toml.go Normal file
View File

@@ -0,0 +1,86 @@
package storage
import (
"bytes"
"fmt"
"github.com/pelletier/go-toml"
)
func init() {
registerReadParser([]string{"toml"}, []string{".toml"}, &TOMLParser{})
registerWriteParser([]string{"toml"}, []string{".toml"}, &TOMLParser{})
}
// TOMLParser is a Parser implementation to handle toml files.
type TOMLParser struct {
}
// FromBytes returns some data that is represented by the given bytes.
func (p *TOMLParser) FromBytes(byteData []byte) (interface{}, error) {
var data interface{}
if err := toml.Unmarshal(byteData, &data); err != nil {
return data, fmt.Errorf("could not unmarshal data: %w", err)
}
return &BasicSingleDocument{
Value: data,
}, nil
}
// ToBytes returns a slice of bytes that represents the given value.
func (p *TOMLParser) ToBytes(value interface{}, options ...ReadWriteOption) ([]byte, error) {
buf := new(bytes.Buffer)
enc := toml.NewEncoder(buf)
colourise := false
for _, o := range options {
switch o.Key {
case OptionIndent:
if indent, ok := o.Value.(string); ok {
enc.Indentation(indent)
}
case OptionColourise:
if value, ok := o.Value.(bool); ok {
colourise = value
}
}
}
switch d := value.(type) {
case SingleDocument:
if err := enc.Encode(d.Document()); err != nil {
if err.Error() == "Only a struct or map can be marshaled to TOML" {
buf.Write([]byte(fmt.Sprintf("%v\n", d.Document())))
} else {
return nil, err
}
}
case MultiDocument:
for _, dd := range d.Documents() {
if err := enc.Encode(dd); err != nil {
if err.Error() == "Only a struct or map can be marshaled to TOML" {
buf.Write([]byte(fmt.Sprintf("%v\n", dd)))
} else {
return nil, err
}
}
}
default:
if err := enc.Encode(d); err != nil {
if err.Error() == "Only a struct or map can be marshaled to TOML" {
buf.Write([]byte(fmt.Sprintf("%v\n", d)))
} else {
return nil, err
}
}
}
if colourise {
if err := ColouriseBuffer(buf, "toml"); err != nil {
return nil, fmt.Errorf("could not colourise output: %w", err)
}
}
return buf.Bytes(), nil
}

141
storage/toml_test.go Normal file
View File

@@ -0,0 +1,141 @@
package storage_test
import (
"github.com/tomwright/dasel/storage"
"reflect"
"strings"
"testing"
)
var tomlBytes = []byte(`names = ["John", "Frank"]
[person]
name = "Tom"
`)
var tomlMap = map[string]interface{}{
"person": map[string]interface{}{
"name": "Tom",
},
"names": []interface{}{"John", "Frank"},
}
func TestTOMLParser_FromBytes(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
got, err := (&storage.TOMLParser{}).FromBytes(tomlBytes)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(&storage.BasicSingleDocument{Value: tomlMap}, got) {
t.Errorf("expected %v, got %v", tomlMap, got)
}
})
t.Run("Invalid", func(t *testing.T) {
_, err := (&storage.TOMLParser{}).FromBytes([]byte(`x:x`))
if err == nil || !strings.Contains(err.Error(), "could not unmarshal data") {
t.Errorf("unexpected error: %v", err)
return
}
})
}
func TestTOMLParser_ToBytes(t *testing.T) {
t.Run("Default", func(t *testing.T) {
got, err := (&storage.TOMLParser{}).ToBytes(tomlMap)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(tomlBytes) != string(got) {
t.Errorf("expected:\n%s\ngot:\n%s", tomlBytes, got)
}
})
t.Run("SingleDocument", func(t *testing.T) {
got, err := (&storage.TOMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: tomlMap})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(tomlBytes) != string(got) {
t.Errorf("expected:\n%s\ngot:\n%s", tomlBytes, got)
}
})
t.Run("SingleDocumentColourise", func(t *testing.T) {
got, err := (&storage.TOMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: tomlMap}, storage.ColouriseOption(true))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
expBuf, _ := storage.Colourise(string(tomlBytes), "toml")
exp := expBuf.Bytes()
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
})
t.Run("SingleDocumentCustomIndent", func(t *testing.T) {
res, err := (&storage.TOMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: tomlMap}, storage.IndentOption(" "))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
got := string(res)
exp := `names = ["John", "Frank"]
[person]
name = "Tom"
`
if exp != got {
t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
}
})
t.Run("MultiDocument", func(t *testing.T) {
got, err := (&storage.TOMLParser{}).ToBytes(&storage.BasicMultiDocument{Values: []interface{}{tomlMap, tomlMap}})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := append([]byte{}, tomlBytes...)
exp = append(exp, tomlBytes...)
if string(exp) != string(got) {
t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
}
})
t.Run("SingleDocumentValue", func(t *testing.T) {
got, err := (&storage.TOMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: "asd"})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := `asd
`
if exp != string(got) {
t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
}
})
t.Run("DefaultValue", func(t *testing.T) {
got, err := (&storage.TOMLParser{}).ToBytes("asd")
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := `asd
`
if exp != string(got) {
t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
}
})
t.Run("MultiDocumentValue", func(t *testing.T) {
got, err := (&storage.TOMLParser{}).ToBytes(&storage.BasicMultiDocument{Values: []interface{}{"asd", "123"}})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := `asd
123
`
if exp != string(got) {
t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
}
})
}

101
storage/xml.go Normal file
View File

@@ -0,0 +1,101 @@
package storage
import (
"bytes"
"fmt"
"strings"
"github.com/clbanning/mxj/v2"
"golang.org/x/net/html/charset"
)
func init() {
// Required for https://github.com/TomWright/dasel/issues/61
mxj.XMLEscapeCharsDecoder(true)
// Required for https://github.com/TomWright/dasel/issues/164
mxj.XmlCharsetReader = charset.NewReaderLabel
registerReadParser([]string{"xml"}, []string{".xml"}, &XMLParser{})
registerWriteParser([]string{"xml"}, []string{".xml"}, &XMLParser{})
}
// XMLParser is a Parser implementation to handle xml files.
type XMLParser struct {
}
// FromBytes returns some data that is represented by the given bytes.
func (p *XMLParser) FromBytes(byteData []byte) (interface{}, error) {
if byteData == nil {
return nil, fmt.Errorf("cannot parse nil xml data")
}
if len(byteData) == 0 || strings.TrimSpace(string(byteData)) == "" {
return nil, nil
}
data, err := mxj.NewMapXml(byteData)
if err != nil {
return data, fmt.Errorf("could not unmarshal data: %w", err)
}
return &BasicSingleDocument{
Value: map[string]interface{}(data),
}, nil
}
// ToBytes returns a slice of bytes that represents the given value.
func (p *XMLParser) ToBytes(value interface{}, options ...ReadWriteOption) ([]byte, error) {
buf := new(bytes.Buffer)
colourise := false
for _, o := range options {
switch o.Key {
case OptionColourise:
if value, ok := o.Value.(bool); ok {
colourise = value
}
}
}
writeMap := func(val interface{}) error {
if m, ok := val.(map[string]interface{}); ok {
mv := mxj.New()
for k, v := range m {
mv[k] = v
}
byteData, err := mv.XmlIndent("", " ")
if err != nil {
return err
}
buf.Write(byteData)
buf.Write([]byte("\n"))
return nil
}
buf.Write([]byte(fmt.Sprintf("%v\n", val)))
return nil
}
switch d := value.(type) {
case SingleDocument:
if err := writeMap(d.Document()); err != nil {
return nil, err
}
case MultiDocument:
for _, dd := range d.Documents() {
if err := writeMap(dd); err != nil {
return nil, err
}
}
default:
if err := writeMap(d); err != nil {
return nil, err
}
}
if colourise {
if err := ColouriseBuffer(buf, "xml"); err != nil {
return nil, fmt.Errorf("could not colourise output: %w", err)
}
}
return buf.Bytes(), nil
}

243
storage/xml_test.go Normal file
View File

@@ -0,0 +1,243 @@
package storage_test
import (
"bytes"
"fmt"
"github.com/tomwright/dasel/storage"
"io"
"reflect"
"testing"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/encoding/unicode"
)
var xmlBytes = []byte(`<user>
<name>Tom</name>
</user>
`)
var xmlMap = map[string]interface{}{
"user": map[string]interface{}{
"name": "Tom",
},
}
var encodedXmlMap = map[string]interface{}{
"user": map[string]interface{}{
"name": "Tõm",
},
}
func TestXMLParser_FromBytes(t *testing.T) {
got, err := (&storage.XMLParser{}).FromBytes(xmlBytes)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(&storage.BasicSingleDocument{Value: xmlMap}, got) {
t.Errorf("expected %v, got %v", xmlMap, got)
}
}
func TestXMLParser_FromBytes_Empty(t *testing.T) {
got, err := (&storage.XMLParser{}).FromBytes([]byte{})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if got != nil {
t.Errorf("expected %v, got %v", nil, got)
}
}
func TestXMLParser_FromBytes_Error(t *testing.T) {
_, err := (&storage.XMLParser{}).FromBytes(nil)
if err == nil {
t.Errorf("expected error but got none")
return
}
_, err = (&storage.XMLParser{}).FromBytes(yamlBytes)
if err == nil {
t.Errorf("expected error but got none")
return
}
}
func TestXMLParser_ToBytes_Default(t *testing.T) {
got, err := (&storage.XMLParser{}).ToBytes(xmlMap)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(xmlBytes, got) {
t.Errorf("expected %v, got %v", string(xmlBytes), string(got))
}
}
func TestXMLParser_ToBytes_SingleDocument(t *testing.T) {
got, err := (&storage.XMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: xmlMap})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(xmlBytes, got) {
t.Errorf("expected %v, got %v", string(xmlBytes), string(got))
}
}
func TestXMLParser_ToBytes_SingleDocument_Colourise(t *testing.T) {
got, err := (&storage.XMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: xmlMap}, storage.ColouriseOption(true))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
expBuf, _ := storage.Colourise(string(xmlBytes), "xml")
exp := expBuf.Bytes()
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
}
func TestXMLParser_ToBytes_MultiDocument(t *testing.T) {
got, err := (&storage.XMLParser{}).ToBytes(&storage.BasicMultiDocument{Values: []interface{}{xmlMap, xmlMap}})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := append([]byte{}, xmlBytes...)
exp = append(exp, xmlBytes...)
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", string(exp), string(got))
}
}
func TestXMLParser_ToBytes_DefaultValue(t *testing.T) {
got, err := (&storage.XMLParser{}).ToBytes("asd")
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := []byte(`asd
`)
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", string(exp), string(got))
}
}
func TestXMLParser_ToBytes_SingleDocumentValue(t *testing.T) {
got, err := (&storage.XMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: "asd"})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := []byte(`asd
`)
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", string(exp), string(got))
}
}
func TestXMLParser_ToBytes_MultiDocumentValue(t *testing.T) {
got, err := (&storage.XMLParser{}).ToBytes(&storage.BasicMultiDocument{Values: []interface{}{"asd", "123"}})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := []byte(`asd
123
`)
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", string(exp), string(got))
}
}
func TestXMLParser_ToBytes_Entities(t *testing.T) {
bytes := []byte(`<systemList>
<system>
<command>sudo /home/fozz/RetroPie-Setup/retropie_packages.sh retropiemenu launch %ROM% &lt;/dev/tty &gt;/dev/tty</command>
<extension>.rp .sh</extension>
<fullname>RetroPie</fullname>
<name>retropie</name>
<path>/home/fozz/RetroPie/retropiemenu</path>
<platform/>
<theme>retropie</theme>
</system>
</systemList>
`)
p := &storage.XMLParser{}
var doc interface{}
t.Run("FromBytes", func(t *testing.T) {
res, err := p.FromBytes(bytes)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
doc = res.(storage.SingleDocument).Document()
got := doc.(map[string]interface{})["systemList"].(map[string]interface{})["system"].(map[string]interface{})["command"]
exp := "sudo /home/fozz/RetroPie-Setup/retropie_packages.sh retropiemenu launch %ROM% &lt;/dev/tty &gt;/dev/tty"
if exp != got {
t.Errorf("expected %s, got %s", exp, got)
}
})
t.Run("ToBytes", func(t *testing.T) {
gotBytes, err := p.ToBytes(doc)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
got := string(gotBytes)
exp := string(bytes)
if exp != got {
t.Errorf("expected %s, got %s", exp, got)
}
})
}
func TestXMLParser_DifferentEncodings(t *testing.T) {
newXmlBytes := func(newWriter func(io.Writer) io.Writer, encoding, text string) []byte {
const encodedXmlBytesFmt = `<?xml version='1.0' encoding='%s'?>`
const xmlBody = `<user><name>%s</name></user>`
var buf bytes.Buffer
w := newWriter(&buf)
fmt.Fprintf(w, xmlBody, text)
return []byte(fmt.Sprintf(encodedXmlBytesFmt, encoding) + buf.String())
}
testCases := []struct {
name string
xml []byte
}{
{
name: "supports ISO-8859-1",
xml: newXmlBytes(charmap.ISO8859_1.NewEncoder().Writer, "ISO-8859-1", "Tõm"),
},
{
name: "supports UTF-8",
xml: newXmlBytes(unicode.UTF8.NewEncoder().Writer, "UTF-8", "Tõm"),
},
{
name: "supports latin1",
xml: newXmlBytes(charmap.Windows1252.NewEncoder().Writer, "latin1", "Tõm"),
},
{
name: "supports UTF-16",
xml: newXmlBytes(unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewEncoder().Writer, "UTF-16", "Tõm"),
},
{
name: "supports UTF-16 (big endian)",
xml: newXmlBytes(unicode.UTF16(unicode.BigEndian, unicode.UseBOM).NewEncoder().Writer, "UTF-16BE", "Tõm"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := (&storage.XMLParser{}).FromBytes(tc.xml)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(&storage.BasicSingleDocument{Value: encodedXmlMap}, got) {
t.Errorf("expected %v, got %v", encodedXmlMap, got)
}
})
}
}

119
storage/yaml.go Normal file
View File

@@ -0,0 +1,119 @@
package storage
import (
"bytes"
"fmt"
"gopkg.in/yaml.v2"
"io"
)
func init() {
registerReadParser([]string{"yaml", "yml"}, []string{".yaml", ".yml"}, &YAMLParser{})
registerWriteParser([]string{"yaml", "yml"}, []string{".yaml", ".yml"}, &YAMLParser{})
}
// YAMLParser is a Parser implementation to handle yaml files.
type YAMLParser struct {
}
// FromBytes returns some data that is represented by the given bytes.
func (p *YAMLParser) FromBytes(byteData []byte) (interface{}, error) {
res := make([]interface{}, 0)
decoder := yaml.NewDecoder(bytes.NewBuffer(byteData))
docLoop:
for {
var docData interface{}
if err := decoder.Decode(&docData); err != nil {
if err == io.EOF {
break docLoop
}
return nil, fmt.Errorf("could not unmarshal data: %w", err)
}
formattedDocData := cleanupYamlMapValue(docData)
res = append(res, formattedDocData)
}
switch len(res) {
case 0:
return nil, nil
case 1:
return &BasicSingleDocument{Value: res[0]}, nil
default:
return &BasicMultiDocument{Values: res}, nil
}
}
func cleanupYamlInterfaceArray(in []interface{}) []interface{} {
res := make([]interface{}, len(in))
for i, v := range in {
res[i] = cleanupYamlMapValue(v)
}
return res
}
func cleanupYamlInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
res := make(map[string]interface{})
for k, v := range in {
res[fmt.Sprint(k)] = cleanupYamlMapValue(v)
}
return res
}
func cleanupYamlMapValue(v interface{}) interface{} {
switch v := v.(type) {
case []interface{}:
return cleanupYamlInterfaceArray(v)
case map[interface{}]interface{}:
return cleanupYamlInterfaceMap(v)
case string:
return v
default:
return v
}
}
// ToBytes returns a slice of bytes that represents the given value.
func (p *YAMLParser) ToBytes(value interface{}, options ...ReadWriteOption) ([]byte, error) {
buffer := new(bytes.Buffer)
encoder := yaml.NewEncoder(buffer)
defer encoder.Close()
colourise := false
for _, o := range options {
switch o.Key {
case OptionColourise:
if value, ok := o.Value.(bool); ok {
colourise = value
}
}
}
switch v := value.(type) {
case SingleDocument:
if err := encoder.Encode(v.Document()); err != nil {
return nil, fmt.Errorf("could not encode single document: %w", err)
}
case MultiDocument:
for index, d := range v.Documents() {
if err := encoder.Encode(d); err != nil {
return nil, fmt.Errorf("could not encode multi document [%d]: %w", index, err)
}
}
default:
if err := encoder.Encode(v); err != nil {
return nil, fmt.Errorf("could not encode default document type: %w", err)
}
}
if colourise {
if err := ColouriseBuffer(buffer, "yaml"); err != nil {
return nil, fmt.Errorf("could not colourise output: %w", err)
}
}
return buffer.Bytes(), nil
}

122
storage/yaml_test.go Normal file
View File

@@ -0,0 +1,122 @@
package storage_test
import (
"github.com/tomwright/dasel/storage"
"reflect"
"strings"
"testing"
)
var yamlBytes = []byte(`name: Tom
numbers:
- 1
- 2
`)
var yamlMap = map[string]interface{}{
"name": "Tom",
"numbers": []interface{}{
1,
2,
},
}
var yamlBytesMulti = []byte(`name: Tom
---
name: Jim
`)
var yamlMapMulti = []interface{}{
map[string]interface{}{
"name": "Tom",
},
map[string]interface{}{
"name": "Jim",
},
}
func TestYAMLParser_FromBytes(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
got, err := (&storage.YAMLParser{}).FromBytes(yamlBytes)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := &storage.BasicSingleDocument{Value: yamlMap}
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
})
t.Run("ValidMultiDocument", func(t *testing.T) {
got, err := (&storage.YAMLParser{}).FromBytes(yamlBytesMulti)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
exp := &storage.BasicMultiDocument{Values: yamlMapMulti}
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
})
t.Run("Invalid", func(t *testing.T) {
_, err := (&storage.YAMLParser{}).FromBytes([]byte(`{1:asd`))
if err == nil || !strings.Contains(err.Error(), "could not unmarshal data") {
t.Errorf("unexpected error: %v", err)
return
}
})
t.Run("Empty", func(t *testing.T) {
got, err := (&storage.YAMLParser{}).FromBytes([]byte(``))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(nil, got) {
t.Errorf("expected %v, got %v", nil, got)
}
})
}
func TestYAMLParser_ToBytes(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
got, err := (&storage.YAMLParser{}).ToBytes(yamlMap)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(yamlBytes) != string(got) {
t.Errorf("expected %s, got %s", yamlBytes, got)
}
})
t.Run("ValidSingle", func(t *testing.T) {
got, err := (&storage.YAMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: yamlMap})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(yamlBytes) != string(got) {
t.Errorf("expected %s, got %s", yamlBytes, got)
}
})
t.Run("ValidSingleColourise", func(t *testing.T) {
got, err := (&storage.YAMLParser{}).ToBytes(&storage.BasicSingleDocument{Value: yamlMap}, storage.ColouriseOption(true))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
expBuf, _ := storage.Colourise(string(yamlBytes), "yaml")
exp := expBuf.Bytes()
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
}
})
t.Run("ValidMulti", func(t *testing.T) {
got, err := (&storage.YAMLParser{}).ToBytes(&storage.BasicMultiDocument{Values: yamlMapMulti})
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if string(yamlBytesMulti) != string(got) {
t.Errorf("expected %s, got %s", yamlBytesMulti, got)
}
})
}