Upstream gocarina/gocsv state of the touched files before ddaa038afb
This commit is contained in:
1 parent
5f7829feee
commit
391e8f0876
3 files changed
+321
-9
No files matched your search
@@ -149,15 +149,6 @@ func MarshalBytes(in interface{}) (out []byte, err error) {
|
||||
return bufferString.Bytes(), nil
|
||||
}
|
||||
|
||||
// MarshalBytesWithoutHeaders returns the CSV bytes from the interface.
|
||||
func MarshalBytesWithoutHeaders(in interface{}) (out []byte, err error) {
|
||||
bufferString := bytes.NewBuffer(out)
|
||||
if err := MarshalWithoutHeaders(in, bufferString); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bufferString.Bytes(), nil
|
||||
}
|
||||
|
||||
// Marshal returns the CSV in writer from the interface.
|
||||
func Marshal(in interface{}, out io.Writer) (err error) {
|
||||
writer := getCSVWriter(out)
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package gocsv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnmarshalToCallback_ReaderError(t *testing.T) {
|
||||
type Dummy struct{}
|
||||
var reader = &errorReader{}
|
||||
|
||||
err := UnmarshalToCallback(reader, func(Dummy) {})
|
||||
if !errors.Is(err, readerErr) {
|
||||
t.Error("UnmarshalToCallback should return first reader error")
|
||||
}
|
||||
|
||||
err = UnmarshalDecoderToCallback(newSimpleDecoderFromReader(reader), func(Dummy) {})
|
||||
if !errors.Is(err, readerErr) {
|
||||
t.Error("UnmarshalDecoderToCallback should return first reader error")
|
||||
}
|
||||
|
||||
err = UnmarshalToCallbackWithError(reader, func(Dummy) error { return nil })
|
||||
if !errors.Is(err, readerErr) {
|
||||
t.Error("UnmarshalToCallbackWithError should return first reader error")
|
||||
}
|
||||
}
|
||||
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read([]byte) (n int, err error) {
|
||||
return 0, readerErr
|
||||
}
|
||||
|
||||
var readerErr = errors.New("reader error")
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package gocsv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Reflection helpers
|
||||
|
||||
type structInfo struct {
|
||||
Fields []fieldInfo
|
||||
Err error
|
||||
}
|
||||
|
||||
// fieldInfo is a struct field that should be mapped to a CSV column, or vice-versa
|
||||
// Each IndexChain element before the last is the index of an the embedded struct field
|
||||
// that defines Key as a tag
|
||||
type fieldInfo struct {
|
||||
keys []string
|
||||
omitEmpty bool
|
||||
IndexChain []int
|
||||
defaultValue string
|
||||
partial bool
|
||||
inline bool
|
||||
}
|
||||
|
||||
func (f fieldInfo) getFirstKey() string {
|
||||
return f.keys[0]
|
||||
}
|
||||
|
||||
func (f fieldInfo) matchesKey(key string) bool {
|
||||
for _, k := range f.keys {
|
||||
if key == k || strings.TrimSpace(key) == k || (f.partial && strings.Contains(key, k)) || removeZeroWidthChars(key) == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// zwchs is Zero Width Characters map
|
||||
var zwchs = map[rune]struct{}{
|
||||
'\u200B': {}, // zero width space (U+200B)
|
||||
'\uFEFF': {}, // zero width no-break space (U+FEFF)
|
||||
'\u200D': {}, // zero width joiner (U+200D)
|
||||
'\u200C': {}, // zero width non-joiner (U+200C)
|
||||
}
|
||||
|
||||
func removeZeroWidthChars(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if _, ok := zwchs[r]; ok {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
|
||||
var structInfoCache sync.Map
|
||||
var structMap = make(map[reflect.Type]*structInfo)
|
||||
var structMapMutex sync.RWMutex
|
||||
|
||||
func getStructInfo(rType reflect.Type) *structInfo {
|
||||
stInfo, ok := structInfoCache.Load(rType)
|
||||
if ok {
|
||||
return stInfo.(*structInfo)
|
||||
}
|
||||
|
||||
fieldsList, err := getFieldInfos(rType, []int{}, []string{}, make(map[reflect.Type]bool))
|
||||
stInfo = &structInfo{Fields: fieldsList, Err: err}
|
||||
structInfoCache.Store(rType, stInfo)
|
||||
|
||||
return stInfo.(*structInfo)
|
||||
}
|
||||
|
||||
func getFieldInfos(rType reflect.Type, parentIndexChain []int, parentKeys []string, ancestors map[reflect.Type]bool) ([]fieldInfo, error) {
|
||||
if ancestors[rType] {
|
||||
return nil, fmt.Errorf("cyclic struct type %s: exclude the recursive field with csv:\"-\"", rType)
|
||||
}
|
||||
ancestors[rType] = true
|
||||
defer delete(ancestors, rType)
|
||||
|
||||
fieldsCount := rType.NumField()
|
||||
fieldsList := make([]fieldInfo, 0, fieldsCount)
|
||||
for i := 0; i < fieldsCount; i++ {
|
||||
field := rType.Field(i)
|
||||
if field.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var cpy = make([]int, len(parentIndexChain))
|
||||
copy(cpy, parentIndexChain)
|
||||
indexChain := append(cpy, i)
|
||||
|
||||
// Determine whether it's a struct we can expand into nested columns.
|
||||
// Structs that implement any of the text or CSV marshaling methods
|
||||
// should result in one value and not have their fields exposed
|
||||
fieldType := field.Type
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
isExpandableStruct := fieldType.Kind() == reflect.Struct && !canMarshal(fieldType)
|
||||
|
||||
var currFieldInfo *fieldInfo
|
||||
if !field.Anonymous {
|
||||
filteredTags := []string{}
|
||||
currFieldInfo, filteredTags = filterTags(TagName, indexChain, field)
|
||||
|
||||
if len(filteredTags) == 1 && filteredTags[0] == "-" {
|
||||
// ignore nested structs with - tag
|
||||
continue
|
||||
} else if isExpandableStruct && len(filteredTags) == 1 && filteredTags[0] == "." {
|
||||
// inline nested structs with . tag
|
||||
currFieldInfo.inline = true
|
||||
currFieldInfo.keys = parentKeys
|
||||
} else if len(filteredTags) > 0 && filteredTags[0] != "" {
|
||||
currFieldInfo.keys = filteredTags
|
||||
} else {
|
||||
currFieldInfo.keys = []string{normalizeName(field.Name)}
|
||||
}
|
||||
|
||||
if len(parentKeys) > 0 && currFieldInfo != nil && !currFieldInfo.inline {
|
||||
// create cartesian product of keys
|
||||
// eg: parent keys x field keys
|
||||
keys := make([]string, 0, len(parentKeys)*len(currFieldInfo.keys))
|
||||
for _, pkey := range parentKeys {
|
||||
for _, ckey := range currFieldInfo.keys {
|
||||
keys = append(keys, normalizeName(fmt.Sprintf("%s%s%s", pkey, FieldsCombiner, ckey)))
|
||||
}
|
||||
currFieldInfo.keys = keys
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle struct
|
||||
if isExpandableStruct {
|
||||
// if the field is an embedded struct, pass along parent keys
|
||||
keys := parentKeys
|
||||
if currFieldInfo != nil {
|
||||
keys = currFieldInfo.keys
|
||||
}
|
||||
childFields, err := getFieldInfos(fieldType, indexChain, keys, ancestors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fieldsList = append(fieldsList, childFields...)
|
||||
continue
|
||||
}
|
||||
|
||||
// if the field is an embedded struct, ignore the csv tag
|
||||
if currFieldInfo == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
|
||||
var arrayLength = -1
|
||||
// if the field is a slice or an array, see if it has a `csv[n]` tag
|
||||
if arrayTag, ok := field.Tag.Lookup(TagName + "[]"); ok {
|
||||
arrayLength, _ = strconv.Atoi(arrayTag)
|
||||
}
|
||||
|
||||
// slices or arrays of Struct get special handling
|
||||
if field.Type.Elem().Kind() == reflect.Struct {
|
||||
fieldInfos, err := getFieldInfos(field.Type.Elem(), []int{}, []string{}, ancestors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if no special csv[] tag was supplied, just include the field directly
|
||||
if arrayLength == -1 {
|
||||
fieldsList = append(fieldsList, *currFieldInfo)
|
||||
} else {
|
||||
// When the field is a slice/array of structs, create a fieldInfo for each index and each field
|
||||
for idx := 0; idx < arrayLength; idx++ {
|
||||
// copy index chain and append array index
|
||||
var cpy2 = make([]int, len(indexChain))
|
||||
copy(cpy2, indexChain)
|
||||
arrayIndexChain := append(cpy2, idx)
|
||||
for _, childFieldInfo := range fieldInfos {
|
||||
// copy array index chain and append array index
|
||||
var cpy3 = make([]int, len(arrayIndexChain))
|
||||
copy(cpy3, arrayIndexChain)
|
||||
|
||||
arrayFieldInfo := fieldInfo{
|
||||
IndexChain: append(cpy3, childFieldInfo.IndexChain...),
|
||||
omitEmpty: childFieldInfo.omitEmpty,
|
||||
defaultValue: childFieldInfo.defaultValue,
|
||||
partial: childFieldInfo.partial,
|
||||
}
|
||||
|
||||
// create cartesian product of keys
|
||||
// eg: array field keys x struct field keys
|
||||
for _, akey := range currFieldInfo.keys {
|
||||
for _, fkey := range childFieldInfo.keys {
|
||||
arrayFieldInfo.keys = append(arrayFieldInfo.keys, normalizeName(fmt.Sprintf("%s[%d].%s", akey, idx, fkey)))
|
||||
}
|
||||
}
|
||||
|
||||
fieldsList = append(fieldsList, arrayFieldInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if arrayLength > 0 {
|
||||
// When the field is a slice/array of primitives, create a fieldInfo for each index
|
||||
for idx := 0; idx < arrayLength; idx++ {
|
||||
// copy index chain and append array index
|
||||
var cpy2 = make([]int, len(indexChain))
|
||||
copy(cpy2, indexChain)
|
||||
|
||||
arrayFieldInfo := fieldInfo{
|
||||
IndexChain: append(cpy2, idx),
|
||||
omitEmpty: currFieldInfo.omitEmpty,
|
||||
defaultValue: currFieldInfo.defaultValue,
|
||||
partial: currFieldInfo.partial,
|
||||
}
|
||||
|
||||
for _, akey := range currFieldInfo.keys {
|
||||
arrayFieldInfo.keys = append(arrayFieldInfo.keys, normalizeName(fmt.Sprintf("%s[%d]", akey, idx)))
|
||||
}
|
||||
|
||||
fieldsList = append(fieldsList, arrayFieldInfo)
|
||||
}
|
||||
} else {
|
||||
fieldsList = append(fieldsList, *currFieldInfo)
|
||||
}
|
||||
} else {
|
||||
fieldsList = append(fieldsList, *currFieldInfo)
|
||||
}
|
||||
}
|
||||
return fieldsList, nil
|
||||
}
|
||||
|
||||
func filterTags(tagName string, indexChain []int, field reflect.StructField) (*fieldInfo, []string) {
|
||||
currFieldInfo := fieldInfo{IndexChain: indexChain}
|
||||
|
||||
fieldTag := field.Tag.Get(tagName)
|
||||
fieldTags := strings.Split(fieldTag, TagSeparator)
|
||||
|
||||
filteredTags := []string{}
|
||||
for _, fieldTagEntry := range fieldTags {
|
||||
trimmedFieldTagEntry := strings.TrimSpace(fieldTagEntry) // handles cases like `csv:"foo, omitempty, default=test"`
|
||||
if trimmedFieldTagEntry == "omitempty" {
|
||||
currFieldInfo.omitEmpty = true
|
||||
} else if trimmedFieldTagEntry == "partial" {
|
||||
// Must be an exact match: using HasPrefix here would mistake a column name that
|
||||
// happens to start with "partial" (e.g. `csv:"partial_delivery_number"`) for the
|
||||
// "partial" option, dropping the real column name. See issue #274.
|
||||
currFieldInfo.partial = true
|
||||
} else if strings.HasPrefix(trimmedFieldTagEntry, "default=") {
|
||||
currFieldInfo.defaultValue = strings.TrimPrefix(trimmedFieldTagEntry, "default=")
|
||||
} else {
|
||||
filteredTags = append(filteredTags, normalizeName(trimmedFieldTagEntry))
|
||||
}
|
||||
}
|
||||
|
||||
return &currFieldInfo, filteredTags
|
||||
}
|
||||
|
||||
func getConcreteContainerInnerType(in reflect.Type) (inInnerWasPointer bool, inInnerType reflect.Type) {
|
||||
inInnerType = in.Elem()
|
||||
inInnerWasPointer = false
|
||||
if inInnerType.Kind() == reflect.Ptr {
|
||||
inInnerWasPointer = true
|
||||
inInnerType = inInnerType.Elem()
|
||||
}
|
||||
return inInnerWasPointer, inInnerType
|
||||
}
|
||||
|
||||
func getConcreteReflectValueAndType(in interface{}) (reflect.Value, reflect.Type) {
|
||||
value := reflect.ValueOf(in)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
return value, value.Type()
|
||||
}
|
||||
|
||||
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
func isErrorType(outType reflect.Type) bool {
|
||||
if outType.Kind() != reflect.Interface {
|
||||
return false
|
||||
}
|
||||
|
||||
return outType.Implements(errorInterface)
|
||||
}
|
||||
Reference in new issue
Block a user