diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e3d411..37bfa9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/api/data_to_point.go b/api/data_to_point.go new file mode 100644 index 0000000..4977958 --- /dev/null +++ b/api/data_to_point.go @@ -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 +} diff --git a/api/data_to_point_test.go b/api/data_to_point_test.go new file mode 100644 index 0000000..d78317d --- /dev/null +++ b/api/data_to_point_test.go @@ -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()) + } + }) + } +} diff --git a/api/query.go b/api/query.go index bce3e96..a547148 100644 --- a/api/query.go +++ b/api/query.go @@ -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) || diff --git a/api/reflection.go b/api/reflection.go new file mode 100644 index 0000000..8becc40 --- /dev/null +++ b/api/reflection.go @@ -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 +}