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:
25
storage/colourise.go
Normal file
25
storage/colourise.go
Normal 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
208
storage/csv.go
Normal 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
217
storage/csv_test.go
Normal 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
105
storage/json.go
Normal 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
192
storage/json_test.go
Normal 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
53
storage/option.go
Normal 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
198
storage/parser.go
Normal 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
286
storage/parser_test.go
Normal 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
38
storage/plain.go
Normal 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
57
storage/plain_test.go
Normal 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
86
storage/toml.go
Normal 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
141
storage/toml_test.go
Normal 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
101
storage/xml.go
Normal 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
243
storage/xml_test.go
Normal 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% </dev/tty >/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% </dev/tty >/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
119
storage/yaml.go
Normal 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
122
storage/yaml_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user