Skip to content

Commit cac777d

Browse files
authored
Merge pull request #394 from influxdata/backport_v3_writeData
feat: add DataToPoint utility to convert a custom struct to a write Point
2 parents c1da0c5 + 425a783 commit cac777d

File tree

5 files changed

+367
-16
lines changed

5 files changed

+367
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## 2.13.0 [unreleased]
22

3+
### Features
4+
5+
- [#394](https://github.com/influxdata/influxdb-client-go/pull/394) Add `DataToPoint` utility to convert a struct to a `write.Point`
6+
37
### Dependencies
48
- [#393](https://github.com/influxdata/influxdb-client-go/pull/393) Replace deprecated `io/ioutil`
59
- [#392](https://github.com/influxdata/influxdb-client-go/pull/392) Upgrade `deepmap/oapi-codegen` to new major version

api/data_to_point.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
"time"
8+
9+
"github.com/influxdata/influxdb-client-go/v2/api/write"
10+
)
11+
12+
// DataToPoint converts custom point structures into a Point.
13+
// Each visible field of the point on input must be annotated with
14+
// 'lp' prefix and values measurement,tag, field or timestamp.
15+
// Valid point must contain measurement and at least one field.
16+
//
17+
// A field with timestamp must be of a type time.Time
18+
//
19+
// type TemperatureSensor struct {
20+
// Measurement string `lp:"measurement"`
21+
// Sensor string `lp:"tag,sensor"`
22+
// ID string `lp:"tag,device_id"`
23+
// Temp float64 `lp:"field,temperature"`
24+
// Hum int `lp:"field,humidity"`
25+
// Time time.Time `lp:"timestamp,temperature"`
26+
// Description string `lp:"-"`
27+
// }
28+
func DataToPoint(x interface{}) (*write.Point, error) {
29+
t := reflect.TypeOf(x)
30+
v := reflect.ValueOf(x)
31+
if t.Kind() == reflect.Ptr {
32+
t = t.Elem()
33+
v = v.Elem()
34+
}
35+
if t.Kind() != reflect.Struct {
36+
return nil, fmt.Errorf("cannot use %v as point", t)
37+
}
38+
fields := reflect.VisibleFields(t)
39+
40+
var measurement = ""
41+
var lpTags = make(map[string]string)
42+
var lpFields = make(map[string]interface{})
43+
var lpTime time.Time
44+
45+
for _, f := range fields {
46+
name := f.Name
47+
if tag, ok := f.Tag.Lookup("lp"); ok {
48+
if tag == "-" {
49+
continue
50+
}
51+
parts := strings.Split(tag, ",")
52+
if len(parts) > 2 {
53+
return nil, fmt.Errorf("multiple tag attributes are not supported")
54+
}
55+
typ := parts[0]
56+
if len(parts) == 2 {
57+
name = parts[1]
58+
}
59+
t := getFieldType(v.FieldByIndex(f.Index))
60+
if !validFieldType(t) {
61+
return nil, fmt.Errorf("cannot use field '%s' of type '%v' as to create a point", f.Name, t)
62+
}
63+
switch typ {
64+
case "measurement":
65+
if measurement != "" {
66+
return nil, fmt.Errorf("multiple measurement fields")
67+
}
68+
measurement = v.FieldByIndex(f.Index).String()
69+
case "tag":
70+
if name == "" {
71+
return nil, fmt.Errorf("cannot use field '%s': invalid lp tag name \"\"", f.Name)
72+
}
73+
lpTags[name] = v.FieldByIndex(f.Index).String()
74+
case "field":
75+
if name == "" {
76+
return nil, fmt.Errorf("cannot use field '%s': invalid lp field name \"\"", f.Name)
77+
}
78+
lpFields[name] = v.FieldByIndex(f.Index).Interface()
79+
case "timestamp":
80+
if f.Type != timeType {
81+
return nil, fmt.Errorf("cannot use field '%s' as a timestamp", f.Name)
82+
}
83+
lpTime = v.FieldByIndex(f.Index).Interface().(time.Time)
84+
default:
85+
return nil, fmt.Errorf("invalid tag %s", typ)
86+
}
87+
}
88+
}
89+
if measurement == "" {
90+
return nil, fmt.Errorf("no struct field with tag 'measurement'")
91+
}
92+
if len(lpFields) == 0 {
93+
return nil, fmt.Errorf("no struct field with tag 'field'")
94+
}
95+
return write.NewPoint(measurement, lpTags, lpFields, lpTime), nil
96+
}

api/data_to_point_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com/influxdata/influxdb-client-go/v2/api/write"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
lp "github.com/influxdata/line-protocol"
14+
)
15+
16+
func TestDataToPoint(t *testing.T) {
17+
pointToLine := func(point *write.Point) string {
18+
var buffer bytes.Buffer
19+
e := lp.NewEncoder(&buffer)
20+
e.SetFieldTypeSupport(lp.UintSupport)
21+
e.FailOnFieldErr(true)
22+
_, err := e.Encode(point)
23+
if err != nil {
24+
panic(err)
25+
}
26+
return buffer.String()
27+
}
28+
now := time.Now()
29+
tests := []struct {
30+
name string
31+
s interface{}
32+
line string
33+
error string
34+
}{{
35+
name: "test normal structure",
36+
s: struct {
37+
Measurement string `lp:"measurement"`
38+
Sensor string `lp:"tag,sensor"`
39+
ID string `lp:"tag,device_id"`
40+
Temp float64 `lp:"field,temperature"`
41+
Hum int `lp:"field,humidity"`
42+
Time time.Time `lp:"timestamp"`
43+
Description string `lp:"-"`
44+
}{
45+
"air",
46+
"SHT31",
47+
"10",
48+
23.5,
49+
55,
50+
now,
51+
"Room temp",
52+
},
53+
line: fmt.Sprintf("air,device_id=10,sensor=SHT31 humidity=55i,temperature=23.5 %d\n", now.UnixNano()),
54+
},
55+
{
56+
name: "test pointer to normal structure",
57+
s: &struct {
58+
Measurement string `lp:"measurement"`
59+
Sensor string `lp:"tag,sensor"`
60+
ID string `lp:"tag,device_id"`
61+
Temp float64 `lp:"field,temperature"`
62+
Hum int `lp:"field,humidity"`
63+
Time time.Time `lp:"timestamp"`
64+
Description string `lp:"-"`
65+
}{
66+
"air",
67+
"SHT31",
68+
"10",
69+
23.5,
70+
55,
71+
now,
72+
"Room temp",
73+
},
74+
line: fmt.Sprintf("air,device_id=10,sensor=SHT31 humidity=55i,temperature=23.5 %d\n", now.UnixNano()),
75+
}, {
76+
name: "test no tag, no timestamp",
77+
s: &struct {
78+
Measurement string `lp:"measurement"`
79+
Temp float64 `lp:"field,temperature"`
80+
}{
81+
"air",
82+
23.5,
83+
},
84+
line: "air temperature=23.5\n",
85+
},
86+
{
87+
name: "test default struct field name",
88+
s: &struct {
89+
Measurement string `lp:"measurement"`
90+
Sensor string `lp:"tag"`
91+
Temp float64 `lp:"field"`
92+
}{
93+
"air",
94+
"SHT31",
95+
23.5,
96+
},
97+
line: "air,Sensor=SHT31 Temp=23.5\n",
98+
},
99+
{
100+
name: "test missing struct field tag name",
101+
s: &struct {
102+
Measurement string `lp:"measurement"`
103+
Sensor string `lp:"tag,"`
104+
Temp float64 `lp:"field"`
105+
}{
106+
"air",
107+
"SHT31",
108+
23.5,
109+
},
110+
error: `cannot use field 'Sensor': invalid lp tag name ""`,
111+
},
112+
{
113+
name: "test missing struct field field name",
114+
s: &struct {
115+
Measurement string `lp:"measurement"`
116+
Temp float64 `lp:"field,"`
117+
}{
118+
"air",
119+
23.5,
120+
},
121+
error: `cannot use field 'Temp': invalid lp field name ""`,
122+
},
123+
{
124+
name: "test missing measurement",
125+
s: &struct {
126+
Measurement string `lp:"tag"`
127+
Sensor string `lp:"tag"`
128+
Temp float64 `lp:"field"`
129+
}{
130+
"air",
131+
"SHT31",
132+
23.5,
133+
},
134+
error: `no struct field with tag 'measurement'`,
135+
},
136+
{
137+
name: "test no field",
138+
s: &struct {
139+
Measurement string `lp:"measurement"`
140+
Sensor string `lp:"tag"`
141+
Temp float64 `lp:"tag"`
142+
}{
143+
"air",
144+
"SHT31",
145+
23.5,
146+
},
147+
error: `no struct field with tag 'field'`,
148+
},
149+
{
150+
name: "test double measurement",
151+
s: &struct {
152+
Measurement string `lp:"measurement"`
153+
Sensor string `lp:"measurement"`
154+
Temp float64 `lp:"field,a"`
155+
Hum float64 `lp:"field,a"`
156+
}{
157+
"air",
158+
"SHT31",
159+
23.5,
160+
43.1,
161+
},
162+
error: `multiple measurement fields`,
163+
},
164+
{
165+
name: "test multiple tag attributes",
166+
s: &struct {
167+
Measurement string `lp:"measurement"`
168+
Sensor string `lp:"tag,a,a"`
169+
Temp float64 `lp:"field,a"`
170+
Hum float64 `lp:"field,a"`
171+
}{
172+
"air",
173+
"SHT31",
174+
23.5,
175+
43.1,
176+
},
177+
error: `multiple tag attributes are not supported`,
178+
},
179+
{
180+
name: "test wrong timestamp type",
181+
s: &struct {
182+
Measurement string `lp:"measurement"`
183+
Sensor string `lp:"tag,sensor"`
184+
Temp float64 `lp:"field,a"`
185+
Hum float64 `lp:"timestamp"`
186+
}{
187+
"air",
188+
"SHT31",
189+
23.5,
190+
43.1,
191+
},
192+
error: `cannot use field 'Hum' as a timestamp`,
193+
},
194+
{
195+
name: "test map",
196+
s: map[string]interface{}{
197+
"measurement": "air",
198+
"sensor": "SHT31",
199+
"temp": 23.5,
200+
},
201+
error: `cannot use map[string]interface {} as point`,
202+
},
203+
{
204+
name: "test unsupported field type",
205+
s: &struct {
206+
Measurement string `lp:"measurement"`
207+
Temp complex64 `lp:"field,a"`
208+
}{
209+
"air",
210+
complex(1, 1),
211+
},
212+
error: `cannot use field 'Temp' of type 'complex64' as to create a point`,
213+
},
214+
{
215+
name: "test unsupported lp tag value",
216+
s: &struct {
217+
Measurement string `lp:"measurement"`
218+
Temp float64 `lp:"data,a"`
219+
}{
220+
"air",
221+
1.0,
222+
},
223+
error: `invalid tag data`,
224+
},
225+
}
226+
for _, ts := range tests {
227+
t.Run(ts.name, func(t *testing.T) {
228+
point, err := DataToPoint(ts.s)
229+
if ts.error == "" {
230+
require.NoError(t, err)
231+
assert.Equal(t, ts.line, pointToLine(point))
232+
} else {
233+
require.Error(t, err)
234+
assert.Equal(t, ts.error, err.Error())
235+
}
236+
})
237+
}
238+
}

api/query.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -289,22 +289,6 @@ func checkParamsType(p interface{}) error {
289289
return nil
290290
}
291291

292-
// getFieldType extracts type of value
293-
func getFieldType(v reflect.Value) reflect.Type {
294-
t := v.Type()
295-
if t.Kind() == reflect.Ptr {
296-
t = t.Elem()
297-
v = v.Elem()
298-
}
299-
if t.Kind() == reflect.Interface && !v.IsNil() {
300-
t = reflect.ValueOf(v.Interface()).Type()
301-
}
302-
return t
303-
}
304-
305-
// timeType is the exact type for the Time
306-
var timeType = reflect.TypeOf(time.Time{})
307-
308292
// validParamType validates that t is primitive type or string or interface
309293
func validParamType(t reflect.Type) bool {
310294
return (t.Kind() > reflect.Invalid && t.Kind() < reflect.Complex64) ||

api/reflection.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package api
2+
3+
import (
4+
"reflect"
5+
"time"
6+
)
7+
8+
// getFieldType extracts type of value
9+
func getFieldType(v reflect.Value) reflect.Type {
10+
t := v.Type()
11+
if t.Kind() == reflect.Ptr {
12+
t = t.Elem()
13+
v = v.Elem()
14+
}
15+
if t.Kind() == reflect.Interface && !v.IsNil() {
16+
t = reflect.ValueOf(v.Interface()).Type()
17+
}
18+
return t
19+
}
20+
21+
// timeType is the exact type for the Time
22+
var timeType = reflect.TypeOf(time.Time{})
23+
24+
// validFieldType validates that t is primitive type or string or interface
25+
func validFieldType(t reflect.Type) bool {
26+
return (t.Kind() > reflect.Invalid && t.Kind() < reflect.Complex64) ||
27+
t.Kind() == reflect.String ||
28+
t == timeType
29+
}

0 commit comments

Comments
 (0)