Skip to content

Commit

Permalink
fix: Honor struct tags on WriteData and avoid panic for unexported fi…
Browse files Browse the repository at this point in the history
…elds (#113)

* fix: Honor struct tags on WriteData and avoid panic for unexported fields

* fix: linter

* fix: linter

* docs: update CHANGELOG.md

* fix: CHANGELOG.md linter

---------

Co-authored-by: Jakub Bednar <[email protected]>
  • Loading branch information
srebhan and bednar authored Nov 6, 2024
1 parent 2147e1f commit 349a67f
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 147 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
## 0.14.0 [unreleased]

### Bug Fixes

1. [#113](https://github.com/InfluxCommunity/influxdb3-go/pull/113): Honor struct tags on WriteData, avoid panic for unexported fields

## 0.13.0 [2024-10-22]

### Features

1. [#108](https://github.com/InfluxCommunity/influxdb3-go/pull/108): Allow Request.GetBody to be set when writing gzipped data to make calls more resilient.
1. [#111](https://github.com/InfluxCommunity/influxdb3-go/pull/111): Support tabs in tag values.
1. [#111](https://github.com/InfluxCommunity/influxdb3-go/pull/111): Support tabs in tag values.

## 0.12.0 [2024-10-02]

Expand Down
93 changes: 0 additions & 93 deletions influxdb3/reflection.go

This file was deleted.

94 changes: 62 additions & 32 deletions influxdb3/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ import (
"github.com/influxdata/line-protocol/v2/lineprotocol"
)

// timeType is the exact type for the Time
var timeType = reflect.TypeOf(time.Time{})

// WritePoints writes all the given points to the server into the given database.
// The data is written synchronously. Empty batch is skipped.
//
Expand Down Expand Up @@ -265,16 +268,17 @@ func (c *Client) writeData(ctx context.Context, points []interface{}, options *W
}

func encode(x interface{}, options *WriteOptions) ([]byte, error) {
if err := checkContainerType(x, false, "point"); err != nil {
return nil, err
}

t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = v.Elem()
}

if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("cannot use %v as point", t)
}

fields := reflect.VisibleFields(t)

var point = &Point{
Expand All @@ -285,37 +289,40 @@ func encode(x interface{}, options *WriteOptions) ([]byte, error) {
}

for _, f := range fields {
name := f.Name
if tag, ok := f.Tag.Lookup("lp"); ok {
if tag == "-" {
continue
}
parts := strings.Split(tag, ",")
if len(parts) > 2 {
return nil, errors.New("multiple tag attributes are not supported")
tag, ok := f.Tag.Lookup("lp")
if !ok || tag == "-" {
continue
}
parts := strings.Split(tag, ",")
if len(parts) > 2 {
return nil, errors.New("multiple tag attributes are not supported")
}
typ, name := parts[0], f.Name
if len(parts) == 2 {
name = parts[1]
}
field := v.FieldByIndex(f.Index)
switch typ {
case "measurement":
if point.GetMeasurement() != "" {
return nil, errors.New("multiple measurement fields")
}
typ := parts[0]
if len(parts) == 2 {
name = parts[1]
point.SetMeasurement(field.String())
case "tag":
point.SetTag(name, field.String())
case "field":
fieldVal, err := fieldValue(name, f, field, t)
if err != nil {
return nil, err
}
switch typ {
case "measurement":
if point.GetMeasurement() != "" {
return nil, errors.New("multiple measurement fields")
}
point.SetMeasurement(v.FieldByIndex(f.Index).String())
case "tag":
point.SetTag(name, v.FieldByIndex(f.Index).String())
case "field":
point.SetField(name, v.FieldByIndex(f.Index).Interface())
case "timestamp":
if f.Type != timeType {
return nil, fmt.Errorf("cannot use field '%s' as a timestamp", f.Name)
}
point.SetTimestamp(v.FieldByIndex(f.Index).Interface().(time.Time))
default:
return nil, fmt.Errorf("invalid tag %s", typ)
point.SetField(name, fieldVal)
case "timestamp":
if f.Type != timeType {
return nil, fmt.Errorf("cannot use field '%s' as a timestamp", f.Name)
}
point.SetTimestamp(field.Interface().(time.Time))
default:
return nil, fmt.Errorf("invalid tag %s", typ)
}
}
if point.GetMeasurement() == "" {
Expand All @@ -327,3 +334,26 @@ func encode(x interface{}, options *WriteOptions) ([]byte, error) {

return point.MarshalBinaryWithDefaultTags(options.Precision, options.DefaultTags)
}

func fieldValue(name string, f reflect.StructField, v reflect.Value, t reflect.Type) (interface{}, error) {
var fieldVal interface{}
if f.IsExported() {
fieldVal = v.Interface()
} else {
switch v.Kind() {
case reflect.Bool:
fieldVal = v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fieldVal = v.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fieldVal = v.Uint()
case reflect.Float32, reflect.Float64:
fieldVal = v.Float()
case reflect.String:
fieldVal = v.String()
default:
return nil, fmt.Errorf("cannot use field '%s' of type '%v' as a field", name, t)
}
}
return fieldVal, nil
}
90 changes: 69 additions & 21 deletions influxdb3/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/influxdata/line-protocol/v2/lineprotocol"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/runtime/protoimpl"
)

func TestEncode(t *testing.T) {
Expand All @@ -45,27 +46,28 @@ func TestEncode(t *testing.T) {
s interface{}
line string
error string
}{{
name: "test normal structure",
s: struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag,sensor"`
ID string `lp:"tag,device_id"`
Temp float64 `lp:"field,temperature"`
Hum int `lp:"field,humidity"`
Time time.Time `lp:"timestamp"`
Description string `lp:"-"`
}{
"air",
"SHT31",
"10",
23.5,
55,
now,
"Room temp",
}{
{
name: "test normal structure",
s: struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag,sensor"`
ID string `lp:"tag,device_id"`
Temp float64 `lp:"field,temperature"`
Hum int `lp:"field,humidity"`
Time time.Time `lp:"timestamp"`
Description string `lp:"-"`
}{
"air",
"SHT31",
"10",
23.5,
55,
now,
"Room temp",
},
line: fmt.Sprintf("air,device_id=10,sensor=SHT31 humidity=55i,temperature=23.5 %d\n", now.UnixNano()),
},
line: fmt.Sprintf("air,device_id=10,sensor=SHT31 humidity=55i,temperature=23.5 %d\n", now.UnixNano()),
},
{
name: "test pointer to normal structure",
s: &struct {
Expand All @@ -86,7 +88,53 @@ func TestEncode(t *testing.T) {
"Room temp",
},
line: fmt.Sprintf("air,device_id=10,sensor=SHT31 humidity=55i,temperature=23.5 %d\n", now.UnixNano()),
}, {
},
{
name: "test normal structure with unexported field",
s: struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag,sensor"`
ID string `lp:"tag,device_id"`
Temp float64 `lp:"field,temperature"`
Hum int64 `lp:"field,humidity"`
Time time.Time `lp:"timestamp"`
Description string `lp:"-"`
}{
"air",
"SHT31",
"10",
23.5,
55,
now,
"Room temp",
},
line: fmt.Sprintf("air,device_id=10,sensor=SHT31 humidity=55i,temperature=23.5 %d\n", now.UnixNano()),
},
{
name: "test protobuf structure",
s: struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag,sensor"`
ID string `lp:"tag,device_id"`
Temp float64 `lp:"field,temperature"`
Hum int64 `lp:"field,humidity"`
Time time.Time `lp:"timestamp"`
Description string `lp:"-"`
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}{
Measurement: "air",
Sensor: "SHT31",
ID: "10",
Temp: 23.5,
Hum: 55,
Time: now,
Description: "Room temp",
},
line: fmt.Sprintf("air,device_id=10,sensor=SHT31 humidity=55i,temperature=23.5 %d\n", now.UnixNano()),
},
{
name: "test no tag, no timestamp",
s: &struct {
Measurement string `lp:"measurement"`
Expand Down

0 comments on commit 349a67f

Please sign in to comment.