Skip to content

Commit

Permalink
Merge pull request #394 from influxdata/backport_v3_writeData
Browse files Browse the repository at this point in the history
feat: add DataToPoint utility to convert a custom struct to a write Point
  • Loading branch information
sranka authored Dec 1, 2023
2 parents c1da0c5 + 425a783 commit cac777d
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## 2.13.0 [unreleased]

### Features

- [#394](https://github.com/influxdata/influxdb-client-go/pull/394) Add `DataToPoint` utility to convert a struct to a `write.Point`

### Dependencies
- [#393](https://github.com/influxdata/influxdb-client-go/pull/393) Replace deprecated `io/ioutil`
- [#392](https://github.com/influxdata/influxdb-client-go/pull/392) Upgrade `deepmap/oapi-codegen` to new major version
Expand Down
96 changes: 96 additions & 0 deletions api/data_to_point.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package api

import (
"fmt"
"reflect"
"strings"
"time"

"github.com/influxdata/influxdb-client-go/v2/api/write"
)

// DataToPoint converts custom point structures into a Point.
// Each visible field of the point on input must be annotated with
// 'lp' prefix and values measurement,tag, field or timestamp.
// Valid point must contain measurement and at least one field.
//
// A field with timestamp must be of a type time.Time
//
// type TemperatureSensor 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,temperature"`
// Description string `lp:"-"`
// }
func DataToPoint(x interface{}) (*write.Point, error) {
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 measurement = ""
var lpTags = make(map[string]string)
var lpFields = make(map[string]interface{})
var lpTime time.Time

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, fmt.Errorf("multiple tag attributes are not supported")
}
typ := parts[0]
if len(parts) == 2 {
name = parts[1]
}
t := getFieldType(v.FieldByIndex(f.Index))
if !validFieldType(t) {
return nil, fmt.Errorf("cannot use field '%s' of type '%v' as to create a point", f.Name, t)
}
switch typ {
case "measurement":
if measurement != "" {
return nil, fmt.Errorf("multiple measurement fields")
}
measurement = v.FieldByIndex(f.Index).String()
case "tag":
if name == "" {
return nil, fmt.Errorf("cannot use field '%s': invalid lp tag name \"\"", f.Name)
}
lpTags[name] = v.FieldByIndex(f.Index).String()
case "field":
if name == "" {
return nil, fmt.Errorf("cannot use field '%s': invalid lp field name \"\"", f.Name)
}
lpFields[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)
}
lpTime = v.FieldByIndex(f.Index).Interface().(time.Time)
default:
return nil, fmt.Errorf("invalid tag %s", typ)
}
}
}
if measurement == "" {
return nil, fmt.Errorf("no struct field with tag 'measurement'")
}
if len(lpFields) == 0 {
return nil, fmt.Errorf("no struct field with tag 'field'")
}
return write.NewPoint(measurement, lpTags, lpFields, lpTime), nil
}
238 changes: 238 additions & 0 deletions api/data_to_point_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package api

import (
"bytes"
"fmt"
"testing"
"time"

"github.com/influxdata/influxdb-client-go/v2/api/write"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

lp "github.com/influxdata/line-protocol"
)

func TestDataToPoint(t *testing.T) {
pointToLine := func(point *write.Point) string {
var buffer bytes.Buffer
e := lp.NewEncoder(&buffer)
e.SetFieldTypeSupport(lp.UintSupport)
e.FailOnFieldErr(true)
_, err := e.Encode(point)
if err != nil {
panic(err)
}
return buffer.String()
}
now := time.Now()
tests := []struct {
name string
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",
},
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 {
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()),
}, {
name: "test no tag, no timestamp",
s: &struct {
Measurement string `lp:"measurement"`
Temp float64 `lp:"field,temperature"`
}{
"air",
23.5,
},
line: "air temperature=23.5\n",
},
{
name: "test default struct field name",
s: &struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag"`
Temp float64 `lp:"field"`
}{
"air",
"SHT31",
23.5,
},
line: "air,Sensor=SHT31 Temp=23.5\n",
},
{
name: "test missing struct field tag name",
s: &struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag,"`
Temp float64 `lp:"field"`
}{
"air",
"SHT31",
23.5,
},
error: `cannot use field 'Sensor': invalid lp tag name ""`,
},
{
name: "test missing struct field field name",
s: &struct {
Measurement string `lp:"measurement"`
Temp float64 `lp:"field,"`
}{
"air",
23.5,
},
error: `cannot use field 'Temp': invalid lp field name ""`,
},
{
name: "test missing measurement",
s: &struct {
Measurement string `lp:"tag"`
Sensor string `lp:"tag"`
Temp float64 `lp:"field"`
}{
"air",
"SHT31",
23.5,
},
error: `no struct field with tag 'measurement'`,
},
{
name: "test no field",
s: &struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag"`
Temp float64 `lp:"tag"`
}{
"air",
"SHT31",
23.5,
},
error: `no struct field with tag 'field'`,
},
{
name: "test double measurement",
s: &struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"measurement"`
Temp float64 `lp:"field,a"`
Hum float64 `lp:"field,a"`
}{
"air",
"SHT31",
23.5,
43.1,
},
error: `multiple measurement fields`,
},
{
name: "test multiple tag attributes",
s: &struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag,a,a"`
Temp float64 `lp:"field,a"`
Hum float64 `lp:"field,a"`
}{
"air",
"SHT31",
23.5,
43.1,
},
error: `multiple tag attributes are not supported`,
},
{
name: "test wrong timestamp type",
s: &struct {
Measurement string `lp:"measurement"`
Sensor string `lp:"tag,sensor"`
Temp float64 `lp:"field,a"`
Hum float64 `lp:"timestamp"`
}{
"air",
"SHT31",
23.5,
43.1,
},
error: `cannot use field 'Hum' as a timestamp`,
},
{
name: "test map",
s: map[string]interface{}{
"measurement": "air",
"sensor": "SHT31",
"temp": 23.5,
},
error: `cannot use map[string]interface {} as point`,
},
{
name: "test unsupported field type",
s: &struct {
Measurement string `lp:"measurement"`
Temp complex64 `lp:"field,a"`
}{
"air",
complex(1, 1),
},
error: `cannot use field 'Temp' of type 'complex64' as to create a point`,
},
{
name: "test unsupported lp tag value",
s: &struct {
Measurement string `lp:"measurement"`
Temp float64 `lp:"data,a"`
}{
"air",
1.0,
},
error: `invalid tag data`,
},
}
for _, ts := range tests {
t.Run(ts.name, func(t *testing.T) {
point, err := DataToPoint(ts.s)
if ts.error == "" {
require.NoError(t, err)
assert.Equal(t, ts.line, pointToLine(point))
} else {
require.Error(t, err)
assert.Equal(t, ts.error, err.Error())
}
})
}
}
16 changes: 0 additions & 16 deletions api/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,22 +289,6 @@ func checkParamsType(p interface{}) error {
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{})

// validParamType validates that t is primitive type or string or interface
func validParamType(t reflect.Type) bool {
return (t.Kind() > reflect.Invalid && t.Kind() < reflect.Complex64) ||
Expand Down
29 changes: 29 additions & 0 deletions api/reflection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package api

import (
"reflect"
"time"
)

// 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
}

0 comments on commit cac777d

Please sign in to comment.