reflection - Dynamic struct field enumeration and attribute parsing in go - Stack Overflow

I am trying to create custom attribute tags for my go program. These tags will be used with fields, tha

I am trying to create custom attribute tags for my go program. These tags will be used with fields, that will pull their values from vault, for e.g. Password string \vault:"password"``. The functions described below should crawl through all the struct's fields, including nested structs, and record all the tags in TagParser.ParsedTagMap, with pointer to the destination value. The ParseTags func will recieve ServerConfiguration struct, with some values filled in.

Custom struct is looking like this:

type ServerConfiguration struct {
    Server1 EndpointConfiguration `yaml:"server1"`
    Server2 EndpointConfiguration `yaml:"server2"`
}

type EndpointConfiguration struct {
    Rest  RestEndpoint     `yaml:"rest"`
    Login LoginCredentials `yaml:"login"`
}

type LoginCredentials struct {
    Username string `vault:"keycloak_username"`
    Password string `vault:"keycloak_password"`
}

The code that should do the parsing of custom structs:

type TagParser struct {
    SourceStruct interface{}
    ParsedTagMap map[string]interface{}
}

func ParseTags(tagName string, data interface{}) map[string]interface{} {
    var tagParser TagParser
    tagParser.ParsedTagMap = make(map[string]interface{})
    tagParser.GetNestedTag(tagName, data)
    slog.Debug("TagParser[" + tagName + "]: Parsed " + strconv.Itoa(len(tagParser.ParsedTagMap)) + " tags")
    return tagParser.ParsedTagMap
}

func (tp *TagParser) GetNestedTag(tagName string, data interface{}) {
    val := reflect.ValueOf(data)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }
    if val.Kind() != reflect.Struct {
        return
    }

    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)

        tagValue := val.Type().Field(i).Tag.Get(tagName)
        if tagValue != "" {
            tp.ParsedTagMap[tagValue] = field.Interface()
        }
        if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) {
            tp.GetNestedTag(tagName, field.Interface())
        }
    }
}

After the fields are extracted, a separate function will call vault REST API, and retrieve their respective values (with value assertion);

// var secret *vault.KVSecret retrieved before
for key, value := range vaultSecretMapping {
    secretValue, ok := secret.Data["data"].(map[string]interface{})[key].(string)
    if !ok {
        panic("Unable to parse " + key + " from vault")
    }
    switch p := value.(type) {
    case *[]byte:
        *p = []byte(secretValue)
    case *string:
        *p = secretValue
    default: // Should never happen
        panic("Unknown type for secret value")
    }
}

I need the parsing function to be dynamic (not just strings), as i will have to use other data types in the future (sych as []byte).

I tried fiddling around with go's reflection library, without much luck. In TagParser.ParsedTagMap, with current setup, i need to have something like: {"password": *interface{metadata:{type: string}, data: {*"pointer to the proper ServerConfiguration nested struct"}}}

I can achieve this result manually: TagParser.ParsedTagMap\["password"\]= &ServerConfiguration.Server1.LoginCredentials.Password

But using reflection obfuscates debugging, and go's pass-by-value dereferences reflect.Value passing in-between the functions (i think). Calling field.Addr() yields panic.

I am trying to create custom attribute tags for my go program. These tags will be used with fields, that will pull their values from vault, for e.g. Password string \vault:"password"``. The functions described below should crawl through all the struct's fields, including nested structs, and record all the tags in TagParser.ParsedTagMap, with pointer to the destination value. The ParseTags func will recieve ServerConfiguration struct, with some values filled in.

Custom struct is looking like this:

type ServerConfiguration struct {
    Server1 EndpointConfiguration `yaml:"server1"`
    Server2 EndpointConfiguration `yaml:"server2"`
}

type EndpointConfiguration struct {
    Rest  RestEndpoint     `yaml:"rest"`
    Login LoginCredentials `yaml:"login"`
}

type LoginCredentials struct {
    Username string `vault:"keycloak_username"`
    Password string `vault:"keycloak_password"`
}

The code that should do the parsing of custom structs:

type TagParser struct {
    SourceStruct interface{}
    ParsedTagMap map[string]interface{}
}

func ParseTags(tagName string, data interface{}) map[string]interface{} {
    var tagParser TagParser
    tagParser.ParsedTagMap = make(map[string]interface{})
    tagParser.GetNestedTag(tagName, data)
    slog.Debug("TagParser[" + tagName + "]: Parsed " + strconv.Itoa(len(tagParser.ParsedTagMap)) + " tags")
    return tagParser.ParsedTagMap
}

func (tp *TagParser) GetNestedTag(tagName string, data interface{}) {
    val := reflect.ValueOf(data)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }
    if val.Kind() != reflect.Struct {
        return
    }

    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)

        tagValue := val.Type().Field(i).Tag.Get(tagName)
        if tagValue != "" {
            tp.ParsedTagMap[tagValue] = field.Interface()
        }
        if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) {
            tp.GetNestedTag(tagName, field.Interface())
        }
    }
}

After the fields are extracted, a separate function will call vault REST API, and retrieve their respective values (with value assertion);

// var secret *vault.KVSecret retrieved before
for key, value := range vaultSecretMapping {
    secretValue, ok := secret.Data["data"].(map[string]interface{})[key].(string)
    if !ok {
        panic("Unable to parse " + key + " from vault")
    }
    switch p := value.(type) {
    case *[]byte:
        *p = []byte(secretValue)
    case *string:
        *p = secretValue
    default: // Should never happen
        panic("Unknown type for secret value")
    }
}

I need the parsing function to be dynamic (not just strings), as i will have to use other data types in the future (sych as []byte).

I tried fiddling around with go's reflection library, without much luck. In TagParser.ParsedTagMap, with current setup, i need to have something like: {"password": *interface{metadata:{type: string}, data: {*"pointer to the proper ServerConfiguration nested struct"}}}

I can achieve this result manually: TagParser.ParsedTagMap\["password"\]= &ServerConfiguration.Server1.LoginCredentials.Password

But using reflection obfuscates debugging, and go's pass-by-value dereferences reflect.Value passing in-between the functions (i think). Calling field.Addr() yields panic.

Share Improve this question asked Nov 19, 2024 at 16:07 Sýkora JakubSýkora Jakub 112 bronze badges 0
Add a comment  | 

1 Answer 1

Reset to default 0

You could do something like this:

type TagParser struct {
    // 1. use [] to collect all fields that have the same key
    // 2. use reflect.Value so that you can utilize f.Set()
    tags map[string][]reflect.Value
}

func ParseTags(tag string, data interface{}) map[string][]reflect.Value {
    var p TagParser
    p.tags = make(map[string][]reflect.Value)
    p.parse(tag, reflect.ValueOf(data))
    return p.tags
}

func (p *TagParser) parse(tag string, rv reflect.Value) {
    if rv.Kind() == reflect.Ptr {
        rv = rv.Elem()
    }
    if rv.Kind() != reflect.Struct {
        return
    }

    rt := rv.Type()
    for i := 0; i < rv.NumField(); i++ {
        f := rv.Field(i)
        if k := rt.Field(i).Tag.Get(tag); k != "" {
            p.tags[k] = append(p.tags[k], f)
        }
        p.parse(tag, f)
    }
}

And then use it like this:

var cfg ServerConfiguration
tags := ParseTags("vault", &cfg)

secretData := map[string]any{
    // ..
}
for k, v := range secretData {
    if ff, ok := tags[k]; ok && len(ff) > 0 {
        for i := range ff {
            ff[i].Set(reflect.ValueOf(v))
        }
    }
}

https://go.dev/play/p/iSQTbzH5-Qb

Note that f.Set will panic if f is non-addressable or if it was obtained by the use of unexported struct fields.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1742415385a4439641.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信