diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f96dc8..7f42da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/influxdb3/reflection.go b/influxdb3/reflection.go deleted file mode 100644 index ec1bb28..0000000 --- a/influxdb3/reflection.go +++ /dev/null @@ -1,93 +0,0 @@ -/* - The MIT License - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -package influxdb3 - -import ( - "fmt" - "reflect" - "time" -) - -// checkContainerType validates the value is struct with simple type fields -// or a map with key as string and value as a simple type -func checkContainerType(p interface{}, alsoMap bool, usage string) error { - if p == nil { - return nil - } - t := reflect.TypeOf(p) - v := reflect.ValueOf(p) - if t.Kind() == reflect.Ptr { - t = t.Elem() - v = v.Elem() - } - if t.Kind() != reflect.Struct && (!alsoMap || t.Kind() != reflect.Map) { - return fmt.Errorf("cannot use %v as %s", t, usage) - } - switch t.Kind() { - case reflect.Struct: - fields := reflect.VisibleFields(t) - for _, f := range fields { - fv := v.FieldByIndex(f.Index) - t := getFieldType(fv) - if !validFieldType(t) { - return fmt.Errorf("cannot use field '%s' of type '%v' as a %s", f.Name, t, usage) - } - } - case reflect.Map: - key := t.Key() - if key.Kind() != reflect.String { - return fmt.Errorf("cannot use map key of type '%v' for %s name", key, usage) - } - for _, k := range v.MapKeys() { - f := v.MapIndex(k) - t := getFieldType(f) - if !validFieldType(t) { - return fmt.Errorf("cannot use map value type '%v' as a %s", t, usage) - } - } - } - return nil -} - -// getFieldType extracts type of value -func getFieldType(v reflect.Value) reflect.Type { - t := v.Type() - if t.Kind() == reflect.Ptr { - t = t.Elem() - v = v.Elem() - } - if t.Kind() == reflect.Interface && !v.IsNil() { - t = reflect.ValueOf(v.Interface()).Type() - } - return t -} - -// timeType is the exact type for the Time -var timeType = reflect.TypeOf(time.Time{}) - -// validFieldType validates that t is primitive type or string or interface -func validFieldType(t reflect.Type) bool { - return (t.Kind() > reflect.Invalid && t.Kind() < reflect.Complex64) || - t.Kind() == reflect.String || - t == timeType -} diff --git a/influxdb3/write.go b/influxdb3/write.go index 743d90b..2a9467e 100644 --- a/influxdb3/write.go +++ b/influxdb3/write.go @@ -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. // @@ -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{ @@ -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() == "" { @@ -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 +} diff --git a/influxdb3/write_test.go b/influxdb3/write_test.go index 63b67fe..268c056 100644 --- a/influxdb3/write_test.go +++ b/influxdb3/write_test.go @@ -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) { @@ -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 { @@ -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"`