Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow struct field tags for influxdb with a marshaller #389

Closed
wants to merge 9 commits into from
164 changes: 164 additions & 0 deletions api/marshal_struct_to_write_point.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package api

/*
MarshalStructToWritePoint accepts a value that is a custom struct a user creates. It optionally takes in a timestamp that becomes the *write.Point timestamp.
if the timestamp argument is nil

Example:

package main

import (
"github.com/influxdata/influxdb-client-go/v2/api"
"log"
)

type influxTestType struct {
Measurement string `influxdb:"measurement"`
Name string `influxdb:"name"`
Title string `influxdb:"title,tag"`
Distance int64 `influxdb:"distance"`
Description string `influxdb:"Description"`
}

func main() {
writer := api.NewWriteAPI("org", "bucket", nil, nil)

influxArg := influxTestType{
Measurement: "foo",
Name: "bar",
Title: "test of the struct write point marshaller",
Distance: 39,
Description: "This tests the MarshalStructToWritePoint",
}

point, err := api.MarshalStructToWritePoint(influxArg, nil)
if err != nil {
log.Fatal(err)
}

writer.WritePoint(point)
}
*/

import (
"errors"
"github.com/influxdata/influxdb-client-go/v2/api/write"
"log"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls use "github.com/influxdata/influxdb-client-go/v2/internal/log" as the rest of the client code does.

"reflect"
"regexp"
"strings"
"time"
)

const (
influxdbTag = "influxdb"
tooManyMeasurementsErrorMsg = "more than 1 struct field is tagged as a measurement. Please pick only 1 struct field to be a measurement"
measurementIsNotStringErrorMsg = "the value for the struct field tagged for measurement is not of type string"
tagValueNotStringErrorMsg = "the value for the struct field for a tag is not of type string"
noMeasurementPresentErrorMsg = "no struct field is tagged as a measurement. You must have a measurement"
tooManyTagArgs = "your influx tag contains more than the allowed number of arguments"
secondTagArgPassedButNotTagErrorMsg = "your influx tag has a second argument but it is not for a tag. If you're trying to set a struct field to be a measurement than the only argument that can be passed is 'measurement'"
)

// Tags is exported in case this is a type a user wants to use in their code
type Tags map[string]string

// Fields is exported in case this is a type a user wants to use in their code
type Fields map[string]interface{}
Comment on lines +64 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to export this, and if there is no need, we should better not. If it would have to be exported, it would be already done to support the Point struct.


// MarshalStructToWritePoint accepts an argument that has a custom struct provided by the user & marshals it into a *write.Point
func MarshalStructToWritePoint(arg interface{}, timestamp *time.Time) (*write.Point, error) {
var measurement string
var tags Tags = make(map[string]string)
var fields Fields = make(map[string]interface{})

measurementCount := 0
ts := time.Now().UTC()

if timestamp != nil {
ts = *timestamp
}
log.SetFlags(log.Lshortfile)

argType := reflect.TypeOf(arg)
val := reflect.ValueOf(arg)

numFields := val.NumField()

for i := 0; i < numFields; i++ {
if measurementCount > 1 {
return nil, errors.New(tooManyMeasurementsErrorMsg)
}
structFieldVal := val.Field(i)
structFieldName := argType.Field(i).Tag.Get(influxdbTag)

err := checkEitherTagOrMeasurement(structFieldName)
if err != nil {
return nil, err
}

if structFieldName == "measurement" {
measurementFieldVal, ok := structFieldVal.Interface().(string)
if !ok {
return nil, errors.New(measurementIsNotStringErrorMsg)
}
measurement = measurementFieldVal
measurementCount++
continue
}

if strings.Contains(structFieldName, "tag") {
stringTagVal, ok := structFieldVal.Interface().(string)
if !ok {
return nil, errors.New(tagValueNotStringErrorMsg)
}
tags[structFieldName] = stringTagVal
continue
}

parsedFieldVal := fieldTypeHandler(structFieldVal)
fields[structFieldName] = parsedFieldVal
}

if measurementCount == 0 {
return nil, errors.New(noMeasurementPresentErrorMsg)
}

if measurementCount > 1 {
return nil, errors.New(tooManyMeasurementsErrorMsg)
}

return write.NewPoint(measurement, tags, fields, ts), nil
}

func fieldTypeHandler(fieldVal interface{}) interface{} {
spaces := regexp.MustCompile(`\s+`)

switch fieldValType := fieldVal.(type) {
case string:
lowerVal := strings.ToLower(fieldValType)
influxStringVal := spaces.ReplaceAllString(lowerVal, "_")
return influxStringVal

case time.Time:
return fieldValType.Unix()

default:
return fieldVal
}
}

func checkEitherTagOrMeasurement(influxTag string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a nit: this fn performs a slightly different thing to what the name suggests ... it should be better named checkFieldTag WDYT?

tags := strings.Split(influxTag, ",")

if len(tags) > 2 {
return errors.New(tooManyTagArgs)
}

if len(tags) == 2 && !strings.Contains(tags[1], "tag") {
return errors.New(secondTagArgPassedButNotTagErrorMsg)
}

return nil
}
100 changes: 100 additions & 0 deletions api/marshal_struct_to_write_point_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package api

import (
"github.com/stretchr/testify/assert"
"testing"
)

type goodInfluxTestType struct {
Measurement string `influxdb:"measurement"`
Name string `influxdb:"name"`
Title string `influxdb:"title,tag"`
Distance int64 `influxdb:"distance"`
Description string `influxdb:"Description"`
}

type badInfluxTestType2ndArgNotTag struct {
Measurement string `influxdb:"name,measurement"`
Name string `influxdb:"name"`
Title string `influxdb:"title,tag"`
Distance int64 `influxdb:"distance"`
Description string `influxdb:"Description"`
}

type badInfluxTestTypeTooManyMeasurements struct {
Measurement string `influxdb:"measurement"`
Name string `influxdb:"name"`
Title string `influxdb:"title,tag"`
Distance int64 `influxdb:"distance"`
Description string `influxdb:"measurement"`
}

type badInfluxTestTypeNoMeasurements struct {
Measurement string `influxdb:"none"`
Name string `influxdb:"name"`
Title string `influxdb:"title,tag"`
Distance int64 `influxdb:"distance"`
Description string `influxdb:"Description"`
}

var (
goodInfluxArg = goodInfluxTestType{
Measurement: "foo",
Name: "bar",
Title: "test of the struct write point marshaller",
Distance: 39,
Description: "This tests the MarshalStructToWritePoint",
}

badInfluxArg2ndArgNotTag = badInfluxTestType2ndArgNotTag{
Measurement: "foo",
Name: "bar",
Title: "test of the struct write point marshaller",
Distance: 39,
Description: "This tests the MarshalStructToWritePoint",
}

badInfluxArgTooManyMeasurements = badInfluxTestTypeTooManyMeasurements{
Measurement: "foo",
Name: "bar",
Title: "test of the struct write point marshaller",
Distance: 39,
Description: "This tests the MarshalStructToWritePoint",
}

badInfluxArgNoMeasurements = badInfluxTestTypeNoMeasurements{
Measurement: "foo",
Name: "bar",
Title: "test of the struct write point marshaller",
Distance: 39,
Description: "This tests the MarshalStructToWritePoint",
}
)

func Test_MarshalStructToWritePoint_Happy_Path(t *testing.T) {
point, err := MarshalStructToWritePoint(goodInfluxArg, nil)
assert.NoError(t, err)
assert.NotNil(t, point)

assert.Equal(t, 1, len(point.TagList()))
assert.Equal(t, 3, len(point.FieldList()))
assert.Equal(t, "foo", point.Name())
}

func Test_MarshalStructToWritePoint_Sad_Path_2nd_Arg_Not_Taf(t *testing.T) {
_, err := MarshalStructToWritePoint(badInfluxArg2ndArgNotTag, nil)
assert.Error(t, err)
assert.Equal(t, secondTagArgPassedButNotTagErrorMsg, err.Error())
}

func Test_MarshalStructToWritePoint_Sad_Path_Too_Many_Measurements(t *testing.T) {
_, err := MarshalStructToWritePoint(badInfluxArgTooManyMeasurements, nil)
assert.Error(t, err)
assert.Equal(t, tooManyMeasurementsErrorMsg, err.Error())
}

func Test_MarshalStructToWritePoint_Sad_Path_No_Measurements(t *testing.T) {
_, err := MarshalStructToWritePoint(badInfluxArgNoMeasurements, nil)
assert.Error(t, err)
assert.Equal(t, noMeasurementPresentErrorMsg, err.Error())
}
Loading