Skip to content

Commit

Permalink
Add Header function
Browse files Browse the repository at this point in the history
  • Loading branch information
jszwec committed Jan 25, 2018
1 parent 0e1b2fa commit 2bdbdd9
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 1 deletion.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,52 @@ bob,30,ny,10005`))
// [{alice la 25 map[zip:90005]} {bob ny 30 map[zip:10005]}]
```

### But my CSV file has no header... ###

Some CSV files have no header, but if you know how it should look like, it is
possible to define a struct and generate it. All that is left to do, is to pass
it to a decoder.

```go
type User struct {
ID int
Name string
Age int `csv:",omitempty"`
City string
}

csvReader := csv.NewReader(strings.NewReader(`
1,John,27,la
2,Bob,,ny`))

// in real application this should be done once in init function.
userHeader, err := csvutil.Header(User{}, "csv")
if err != nil {
log.Fatal(err)
}

dec, err := csvutil.NewDecoder(csvReader, userHeader...)
if err != nil {
log.Fatal(err)
}

var users []User
for {
var u User
if err := dec.Decode(&u); err == io.EOF {
break
} else if err != nil {
log.Fatal(err)
}
users = append(users, u)
}

fmt.Printf("%+v", users)

// Output:
// [{ID:1 Name:John Age:27 City:la} {ID:2 Name:Bob Age:0 City:ny}]
```

Performance
------------

Expand Down
50 changes: 50 additions & 0 deletions csvutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,53 @@ func newCSVReader(r io.Reader) *csv.Reader {
rr.ReuseRecord = true
return rr
}

// Header scans the provided struct type and generates a CSV header for it.
//
// Field names are written in the same order as struct fields are defined.
// Embedded struct's fields are treated as if they were part of the outer struct.
// Fields that are embedded types and that are tagged are treated like any
// other field.
//
// Unexported fields and fields with tag "-" are ignored.
//
// Tagged fields have the priority over non tagged fields with the same name.
//
// Following the Go visibility rules if there are multiple fields with the same
// name (tagged or not tagged) on the same level and choice between them is
// ambiguous, then all these fields will be ignored.
//
// It is a good practice to call Header once for each type. The suitable place
// for calling it is init function. Look at Decoder.DecodingDataWithNoHeader
// example.
//
// If tag is left empty the default "csv" will be used.
//
// Header will return UnsupportedTypeError if the provided value is nil or is
// not a struct.
func Header(v interface{}, tag string) ([]string, error) {
val := reflect.ValueOf(v)
if !val.IsValid() {
return nil, &UnsupportedTypeError{}
}

typ := val.Type()
for typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}

if typ.Kind() != reflect.Struct {
return nil, &UnsupportedTypeError{Type: typ}
}

if tag == "" {
tag = defaultTag
}

fields := cachedFields(typeKey{tag, typ})
h := make([]string, len(fields))
for i, f := range fields {
h[i] = f.tag.name
}
return h, nil
}
215 changes: 215 additions & 0 deletions csvutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,218 @@ func TestMarshal(t *testing.T) {
})

}

type TypeJ struct {
String string `csv:"STR" json:"string"`
Int string `csv:"int" json:"-"`
Embedded16
Float string `csv:"float"`
}

type Embedded16 struct {
Bool bool `json:"bool"`
Uint uint `csv:"-"`
Uint8 uint8 `json:"-"`
}

func TestHeader(t *testing.T) {
fixture := []struct {
desc string
v interface{}
tag string
header []string
err error
}{
{
desc: "simple type with default tag",
v: TypeG{},
tag: "",
header: []string{"String", "Int"},
},
{
desc: "simple type",
v: TypeG{},
tag: "csv",
header: []string{"String", "Int"},
},
{
desc: "simple type with ptr value",
v: &TypeG{},
tag: "csv",
header: []string{"String", "Int"},
},
{
desc: "embedded types with conflict",
v: &TypeA{},
tag: "csv",
header: []string{"string", "bool", "int"},
},
{
desc: "embedded type with tag",
v: &TypeB{},
tag: "csv",
header: []string{"json", "string"},
},
{
desc: "embedded ptr type with tag",
v: &TypeD{},
tag: "csv",
header: []string{"json", "string"},
},
{
desc: "embedded ptr type no tag",
v: &TypeC{},
tag: "csv",
header: []string{"float", "string"},
},
{
desc: "type with omitempty tags",
v: TypeI{},
tag: "csv",
header: []string{"String", "int"},
},
{
desc: "embedded with different json tag",
v: TypeJ{},
tag: "json",
header: []string{"string", "bool", "Uint", "Float"},
},
{
desc: "embedded with default csv tag",
v: TypeJ{},
tag: "csv",
header: []string{"STR", "int", "Bool", "Uint8", "float"},
},
{
desc: "not a struct",
v: int(10),
tag: "csv",
err: &UnsupportedTypeError{Type: reflect.TypeOf(int(0))},
},
{
desc: "slice",
v: []TypeJ{{}},
tag: "csv",
err: &UnsupportedTypeError{Type: reflect.TypeOf([]TypeJ{})},
},
{
desc: "nil interface",
v: nilIface,
tag: "csv",
err: &UnsupportedTypeError{},
},
{
desc: "circular reference type",
v: &A{},
tag: "csv",
header: []string{"Y", "X"},
},
{
desc: "conflicting fields",
v: &Embedded10{},
tag: "csv",
header: []string{"Y"},
},
{
desc: "nil ptr of TypeF",
v: nilPtr,
tag: "csv",
header: []string{"int",
"pint",
"int8",
"pint8",
"int16",
"pint16",
"int32",
"pint32",
"int64",
"pint64",
"uint",
"puint",
"uint8",
"puint8",
"uint16",
"puint16",
"uint32",
"puint32",
"uint64",
"puint64",
"float32",
"pfloat32",
"float64",
"pfloat64",
"string",
"pstring",
"bool",
"pbool",
"interface",
"pinterface",
"binary",
"pbinary",
},
},
{
desc: "nil interface ptr of TypeF",
v: nilIfacePtr,
tag: "csv",
header: []string{"int",
"pint",
"int8",
"pint8",
"int16",
"pint16",
"int32",
"pint32",
"int64",
"pint64",
"uint",
"puint",
"uint8",
"puint8",
"uint16",
"puint16",
"uint32",
"puint32",
"uint64",
"puint64",
"float32",
"pfloat32",
"float64",
"pfloat64",
"string",
"pstring",
"bool",
"pbool",
"interface",
"pinterface",
"binary",
"pbinary",
},
},
}

for _, f := range fixture {
t.Run(f.desc, func(t *testing.T) {
h, err := Header(f.v, f.tag)

if !reflect.DeepEqual(err, f.err) {
t.Errorf("want err=%v; got %v", f.err, err)
}

if !reflect.DeepEqual(h, f.header) {
t.Errorf("want header=%v; got %v", f.header, h)
}
})
}

t.Run("test nil value error message", func(t *testing.T) {
const expected = "csvutil: unsupported type: nil"
h, err := Header(nilIface, "")
if h != nil {
t.Errorf("want h=nil; got %v", h)
}
if err.Error() != expected {
t.Errorf("want err=%s; got %s", expected, err.Error())
}
})
}
2 changes: 1 addition & 1 deletion encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func NewEncoder(w Writer) *Encoder {
//
// Tagged fields have the priority over non tagged fields with the same name.
//
// Following the Go vibility rules if there are multiple fields with the same
// Following the Go visibility rules if there are multiple fields with the same
// name (tagged or not tagged) on the same level and choice between them is
// ambiguous, then all these fields will be ignored.
//
Expand Down
3 changes: 3 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type UnsupportedTypeError struct {
}

func (e *UnsupportedTypeError) Error() string {
if e.Type == nil {
return "csvutil: unsupported type: nil"
}
return "csvutil: unsupported type: " + e.Type.String()
}

Expand Down
Loading

0 comments on commit 2bdbdd9

Please sign in to comment.