From 66f10b892d87a6b30bebd24f0e55d264890e80ec Mon Sep 17 00:00:00 2001 From: zob456 Date: Wed, 20 Sep 2023 13:48:49 -0400 Subject: [PATCH 1/5] Adds marshal.go & marshal_test.go to the api package --- api/marshal.go | 97 +++++++++++++++++++++++++++++++++++++++++++++ api/marshal_test.go | 9 +++++ 2 files changed, 106 insertions(+) create mode 100644 api/marshal.go create mode 100644 api/marshal_test.go diff --git a/api/marshal.go b/api/marshal.go new file mode 100644 index 00000000..2bf881ba --- /dev/null +++ b/api/marshal.go @@ -0,0 +1,97 @@ +package api + +import ( + "fmt" + "log" + "reflect" + "regexp" + "strings" + "time" +) + +const ( + influxdbTag = "influxdb" + protoTag = "protobuf" + protoOneOfTag = "protobuf_oneof" +) + +func MarshalTag(arg any) string { + log.SetFlags(log.Lshortfile) + + argType := reflect.TypeOf(arg) + val := reflect.ValueOf(arg) + + var influxPoint string + + numFields := val.NumField() + count := 0 + + for i := 0; i < numFields; i++ { + fieldVal := val.Field(i) + fieldName := argType.Field(i).Tag.Get(influxdbTag) + + updatedInfluxPoint := typeHandler(fieldName, fieldVal, nil) + // add a "," at the end of the string unless the loop is at the last value + if count < numFields-1 { + updatedInfluxPoint += "," + } + influxPoint += updatedInfluxPoint + count++ + } + return influxPoint +} + +func MarshalProto(arg any) { + log.SetFlags(log.Lshortfile) + var influxPoint string + + //argType := reflect.TypeOf(arg) + val := reflect.ValueOf(arg) + argType := reflect.TypeOf(arg) + + numFields := val.NumField() + count := 0 + + for i := 0; i < numFields; i++ { + fieldVal := reflect.Indirect(val).Field(i) + fieldProtoTagName := argType.Field(i).Tag.Get(protoTag) + fieldProtoOneOfTag := argType.Field(i).Tag.Get(protoOneOfTag) + protoTagNames := strings.Split(fieldProtoTagName, ",") + + if len(protoTagNames) > 1 { + influxdbTagName := strings.Split(protoTagNames[3], "=") + updatedInfluxPoint := typeHandler(influxdbTagName[1], fieldVal, fieldVal) + // add a "," at the end of the string unless the loop is at the last value + if count < numFields-1 { + updatedInfluxPoint += "," + } + influxPoint += updatedInfluxPoint + } else { + updatedInfluxPoint := typeHandler(fieldProtoOneOfTag, fieldVal, fieldVal) + // add a "," at the end of the string unless the loop is at the last value + if count < numFields-1 { + updatedInfluxPoint += "," + } + influxPoint += updatedInfluxPoint + } + count++ + } + log.Println(influxPoint) +} + +func typeHandler(fieldName string, fieldVal reflect.Value, val any) string { + spaces := regexp.MustCompile(`\s+`) + + switch fieldVal.Type().String() { + case "string": + lowerVal := strings.ToLower(fieldVal.String()) + influxStringVal := spaces.ReplaceAllString(lowerVal, "_") + return fmt.Sprintf("%v=%v", fieldName, influxStringVal) + + case "time.Time": + return fmt.Sprintf("%v=%v", fieldName, fieldVal.Interface().(time.Time).Unix()) + + default: + return fmt.Sprintf("%v=%v", fieldName, val) + } +} diff --git a/api/marshal_test.go b/api/marshal_test.go new file mode 100644 index 00000000..40b217be --- /dev/null +++ b/api/marshal_test.go @@ -0,0 +1,9 @@ +package api + +import "time" + +type InfluxTestType struct { + Name string `influxdb:"name"` + Title string `influxdb:"title"` + Timestamp time.Time `influxdb:"timestamp"` +} From d6a531a44fad36f45955d7a52ae4bc7755976b51 Mon Sep 17 00:00:00 2001 From: zob456 Date: Tue, 17 Oct 2023 18:13:42 -0400 Subject: [PATCH 2/5] Suggested MarshalStructToWritePoint func - Adds marshal_struct_to_write_point.go - Adds marshal_struct_to_write_point_test.go --- api/marshal.go | 97 ------------------ api/marshal_struct_to_write_point.go | 119 ++++++++++++++++++++++ api/marshal_struct_to_write_point_test.go | 100 ++++++++++++++++++ api/marshal_test.go | 9 -- 4 files changed, 219 insertions(+), 106 deletions(-) delete mode 100644 api/marshal.go create mode 100644 api/marshal_struct_to_write_point.go create mode 100644 api/marshal_struct_to_write_point_test.go delete mode 100644 api/marshal_test.go diff --git a/api/marshal.go b/api/marshal.go deleted file mode 100644 index 2bf881ba..00000000 --- a/api/marshal.go +++ /dev/null @@ -1,97 +0,0 @@ -package api - -import ( - "fmt" - "log" - "reflect" - "regexp" - "strings" - "time" -) - -const ( - influxdbTag = "influxdb" - protoTag = "protobuf" - protoOneOfTag = "protobuf_oneof" -) - -func MarshalTag(arg any) string { - log.SetFlags(log.Lshortfile) - - argType := reflect.TypeOf(arg) - val := reflect.ValueOf(arg) - - var influxPoint string - - numFields := val.NumField() - count := 0 - - for i := 0; i < numFields; i++ { - fieldVal := val.Field(i) - fieldName := argType.Field(i).Tag.Get(influxdbTag) - - updatedInfluxPoint := typeHandler(fieldName, fieldVal, nil) - // add a "," at the end of the string unless the loop is at the last value - if count < numFields-1 { - updatedInfluxPoint += "," - } - influxPoint += updatedInfluxPoint - count++ - } - return influxPoint -} - -func MarshalProto(arg any) { - log.SetFlags(log.Lshortfile) - var influxPoint string - - //argType := reflect.TypeOf(arg) - val := reflect.ValueOf(arg) - argType := reflect.TypeOf(arg) - - numFields := val.NumField() - count := 0 - - for i := 0; i < numFields; i++ { - fieldVal := reflect.Indirect(val).Field(i) - fieldProtoTagName := argType.Field(i).Tag.Get(protoTag) - fieldProtoOneOfTag := argType.Field(i).Tag.Get(protoOneOfTag) - protoTagNames := strings.Split(fieldProtoTagName, ",") - - if len(protoTagNames) > 1 { - influxdbTagName := strings.Split(protoTagNames[3], "=") - updatedInfluxPoint := typeHandler(influxdbTagName[1], fieldVal, fieldVal) - // add a "," at the end of the string unless the loop is at the last value - if count < numFields-1 { - updatedInfluxPoint += "," - } - influxPoint += updatedInfluxPoint - } else { - updatedInfluxPoint := typeHandler(fieldProtoOneOfTag, fieldVal, fieldVal) - // add a "," at the end of the string unless the loop is at the last value - if count < numFields-1 { - updatedInfluxPoint += "," - } - influxPoint += updatedInfluxPoint - } - count++ - } - log.Println(influxPoint) -} - -func typeHandler(fieldName string, fieldVal reflect.Value, val any) string { - spaces := regexp.MustCompile(`\s+`) - - switch fieldVal.Type().String() { - case "string": - lowerVal := strings.ToLower(fieldVal.String()) - influxStringVal := spaces.ReplaceAllString(lowerVal, "_") - return fmt.Sprintf("%v=%v", fieldName, influxStringVal) - - case "time.Time": - return fmt.Sprintf("%v=%v", fieldName, fieldVal.Interface().(time.Time).Unix()) - - default: - return fmt.Sprintf("%v=%v", fieldName, val) - } -} diff --git a/api/marshal_struct_to_write_point.go b/api/marshal_struct_to_write_point.go new file mode 100644 index 00000000..fdb202d3 --- /dev/null +++ b/api/marshal_struct_to_write_point.go @@ -0,0 +1,119 @@ +package api + +import ( + "errors" + "github.com/influxdata/influxdb-client-go/v2/api/write" + "log" + "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'" +) + +type Tags map[string]string +type Fields map[string]interface{} + +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 fieldVal.(type) { + case string: + lowerVal := strings.ToLower(fieldVal.(string)) + influxStringVal := spaces.ReplaceAllString(lowerVal, "_") + return influxStringVal + + case time.Time: + return fieldVal.(time.Time).Unix() + + default: + return fieldVal + } +} + +func checkEitherTagOrMeasurement(influxTag string) error { + 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 +} diff --git a/api/marshal_struct_to_write_point_test.go b/api/marshal_struct_to_write_point_test.go new file mode 100644 index 00000000..41abfd55 --- /dev/null +++ b/api/marshal_struct_to_write_point_test.go @@ -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()) +} diff --git a/api/marshal_test.go b/api/marshal_test.go deleted file mode 100644 index 40b217be..00000000 --- a/api/marshal_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package api - -import "time" - -type InfluxTestType struct { - Name string `influxdb:"name"` - Title string `influxdb:"title"` - Timestamp time.Time `influxdb:"timestamp"` -} From cf6cb728b52d7482370e0f748cf030ea9b88742e Mon Sep 17 00:00:00 2001 From: zob456 Date: Tue, 17 Oct 2023 18:40:54 -0400 Subject: [PATCH 3/5] Fixes switch case for better type assertion in linting --- api/marshal_struct_to_write_point.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/marshal_struct_to_write_point.go b/api/marshal_struct_to_write_point.go index fdb202d3..d45183ed 100644 --- a/api/marshal_struct_to_write_point.go +++ b/api/marshal_struct_to_write_point.go @@ -90,14 +90,14 @@ func MarshalStructToWritePoint(arg interface{}, timestamp *time.Time) (*write.Po func fieldTypeHandler(fieldVal interface{}) interface{} { spaces := regexp.MustCompile(`\s+`) - switch fieldVal.(type) { + switch fieldValType := fieldVal.(type) { case string: - lowerVal := strings.ToLower(fieldVal.(string)) + lowerVal := strings.ToLower(fieldValType) influxStringVal := spaces.ReplaceAllString(lowerVal, "_") return influxStringVal case time.Time: - return fieldVal.(time.Time).Unix() + return fieldValType.Unix() default: return fieldVal From 6b78fbc363b611e7ded7d03dde0d05a43380b2df Mon Sep 17 00:00:00 2001 From: zob456 Date: Tue, 17 Oct 2023 19:11:09 -0400 Subject: [PATCH 4/5] Adds better documentation along with an example --- api/marshal_struct_to_write_point.go | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/api/marshal_struct_to_write_point.go b/api/marshal_struct_to_write_point.go index d45183ed..0219817d 100644 --- a/api/marshal_struct_to_write_point.go +++ b/api/marshal_struct_to_write_point.go @@ -1,5 +1,47 @@ 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" @@ -20,9 +62,13 @@ const ( 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{} +// 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) From 9ff9f0fb4cd8fa28e6c018aec530b8123fdb498c Mon Sep 17 00:00:00 2001 From: zob456 Date: Tue, 17 Oct 2023 19:40:10 -0400 Subject: [PATCH 5/5] Removes unnecessary space in example comment --- api/marshal_struct_to_write_point.go | 1 - 1 file changed, 1 deletion(-) diff --git a/api/marshal_struct_to_write_point.go b/api/marshal_struct_to_write_point.go index 0219817d..b7240a8b 100644 --- a/api/marshal_struct_to_write_point.go +++ b/api/marshal_struct_to_write_point.go @@ -39,7 +39,6 @@ Example: writer.WritePoint(point) } - */ import (