diff --git a/README.md b/README.md index 7025b2b..58a6e8e 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,27 @@ _ := reflections.SetField(&s, "FirstField", "new value") err := reflection.SetField(&s, "FirstField", 123) // err != nil ``` +##### GetFieldNameByTagValue + +*GetFieldNameByTagValue* looks up a field with a matching `{tagKey}:"{tagValue}"` tag in the provided `obj` item. +If `obj` is not a `struct`, nor a `pointer`, or it does not have a field tagged with the `tagKey`, and the matching `tagValue`, this function returns an error. + +```go + s := MyStruct { + FirstField: "first value", `matched:"first tag"` + SecondField: 2, `matched:"second tag"` + ThirdField: "third value", `unmatched:"third tag"` + } + + // Getting field name from external source as json would be a headache to convert it manually, + // so we get it directly from struct tag + // returns fieldName = "FirstField" + fieldName, _ = reflections.GetFieldNameByTagValue(s, "first tag", "matched"); + + // later we can do GetField(s, fieldName) +``` + + ## Important notes - **un-exported fields** can't be accessed nor set using the `reflections` library. The Go lang standard `reflect` library intentionally prohibits un-exported fields values access or modifications. diff --git a/example_test.go b/example_test.go index c715b43..2030854 100644 --- a/example_test.go +++ b/example_test.go @@ -1,6 +1,7 @@ package reflections_test import ( + "encoding/json" "fmt" "log" "reflect" @@ -213,3 +214,51 @@ func ExampleSetField() { log.Fatal(err) } } + +func ExampleGetFieldNameByTagValue() { + type Order struct { + Step string `json:"order_step"` + ID string `json:"id"` + Category string `json:"category"` + } + type Condition struct { + Field string `json:"field"` + Value string `json:"value"` + Next string `json:"next"` + } + + // JSON data from external source + orderJSON := `{ + "order_step": "cooking", + "id": "45457-fv54f54", + "category": "Pizzas" + }` + + conditionJSON := `{ + "field": "order_step", + "value": "cooking", + "next": "serve" + }` + + // Storing JSON in corresponding Variables + var order Order + err := json.Unmarshal([]byte(orderJSON), &order) + if err != nil { + log.Fatal(err) + } + + var condition Condition + err = json.Unmarshal([]byte(conditionJSON), &condition) + if err != nil { + log.Fatal(err) + } + + fieldName, _ := reflections.GetFieldNameByTagValue(order, "json", condition.Field) + fmt.Println(fieldName) + fieldValue, _ := reflections.GetField(order, fieldName) + fmt.Println(fieldValue) + + // Output: + // Step + // cooking +} diff --git a/reflections.go b/reflections.go index be2bc02..f39e02e 100644 --- a/reflections.go +++ b/reflections.go @@ -101,6 +101,28 @@ func GetFieldTag(obj interface{}, fieldName, tagKey string) (string, error) { return field.Tag.Get(tagKey), nil } +// GetFieldNameByTagValue looks up a field with a matching `{tagKey}:"{tagValue}"` tag in the provided `obj` item. +// The `obj` parameter must be a `struct`, or a `pointer` to one. If the `obj` parameter doesn't have a field tagged +// with the `tagKey`, and the matching `tagValue`, this function returns an error. +func GetFieldNameByTagValue(obj interface{}, tagKey, tagValue string) (string, error) { + if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { + return "", fmt.Errorf("cannot use GetFieldByTag on a non-struct interface: %w", ErrUnsupportedType) + } + + objValue := reflectValue(obj) + objType := objValue.Type() + fieldsCount := objType.NumField() + + for i := 0; i < fieldsCount; i++ { + structField := objType.Field(i) + if structField.Tag.Get(tagKey) == tagValue { + return structField.Name, nil + } + } + + return "", errors.New("tag doesn't exist in the given struct") +} + // SetField sets the provided obj field with provided value. // // The `obj` parameter must be a pointer to a struct, otherwise it soundly fails. diff --git a/reflections_test.go b/reflections_test.go index 4b094d4..c601594 100644 --- a/reflections_test.go +++ b/reflections_test.go @@ -513,6 +513,45 @@ func TestItems_deep(t *testing.T) { assert.Equal(t, itemsDeep["Number"], 17) } +func TestGetFieldNameByTagValue(t *testing.T) { + t.Parallel() + + dummyStruct := TestStruct{ + Dummy: "dummy", + Yummy: 123, + } + + tagJSON := "dummytag" + field, err := GetFieldNameByTagValue(dummyStruct, "test", tagJSON) + + assert.NoError(t, err) + assert.Equal(t, field, "Dummy") +} + +func TestGetFieldNameByTagValue_on_non_existing_tag(t *testing.T) { + t.Parallel() + + dummyStruct := TestStruct{ + Dummy: "dummy", + Yummy: 123, + } + + // non existing tag value with an existing tag key + tagJSON := "tag" + _, errTagValue := GetFieldNameByTagValue(dummyStruct, "test", tagJSON) + assert.Error(t, errTagValue) + + // non existing tag key with an existing tag value + tagJSON = "dummytag" + _, errTagKey := GetFieldNameByTagValue(dummyStruct, "json", tagJSON) + assert.Error(t, errTagKey) + + // non existing tag key and value + tagJSON = "tag" + _, errTagKeyValue := GetFieldNameByTagValue(dummyStruct, "json", tagJSON) + assert.Error(t, errTagKeyValue) +} + //nolint:unused func TestTags_deep(t *testing.T) { t.Parallel()