components-contrib/metadata/utils.go

490 lines
13 KiB
Go

/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metadata
import (
"errors"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"time"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
"github.com/dapr/components-contrib/internal/utils"
"github.com/dapr/kit/ptr"
)
const (
// TTLMetadataKey defines the metadata key for setting a time to live (in seconds).
TTLMetadataKey = "ttlInSeconds"
// RawPayloadKey defines the metadata key for forcing raw payload in pubsub.
RawPayloadKey = "rawPayload"
// PriorityMetadataKey defines the metadata key for setting a priority.
PriorityMetadataKey = "priority"
// ContentType defines the metadata key for the content type.
ContentType = "contentType"
// QueryIndexName defines the metadata key for the name of query indexing schema (for redis).
QueryIndexName = "queryIndexName"
// MaxBulkPubBytesKey defines the maximum bytes to publish in a bulk publish request metadata.
MaxBulkPubBytesKey string = "maxBulkPubBytes"
)
// TryGetTTL tries to get the ttl as a time.Duration value for pubsub, binding and any other building block.
func TryGetTTL(props map[string]string) (time.Duration, bool, error) {
if val, ok := props[TTLMetadataKey]; ok && val != "" {
valInt64, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return 0, false, fmt.Errorf("%s value must be a valid integer: actual is '%s'", TTLMetadataKey, val)
}
if valInt64 <= 0 {
return 0, false, fmt.Errorf("%s value must be higher than zero: actual is %d", TTLMetadataKey, valInt64)
}
duration := time.Duration(valInt64) * time.Second
if duration < 0 {
// Overflow
duration = math.MaxInt64
}
return duration, true, nil
}
return 0, false, nil
}
// TryGetPriority tries to get the priority for binding and any other building block.
func TryGetPriority(props map[string]string) (uint8, bool, error) {
if val, ok := props[PriorityMetadataKey]; ok && val != "" {
intVal, err := strconv.Atoi(val)
if err != nil {
return 0, false, fmt.Errorf("%s value must be a valid integer: actual is '%s'", PriorityMetadataKey, val)
}
priority := uint8(intVal)
if intVal < 0 {
priority = 0
} else if intVal > 255 {
priority = math.MaxUint8
}
return priority, true, nil
}
return 0, false, nil
}
// IsRawPayload determines if payload should be used as-is.
func IsRawPayload(props map[string]string) (bool, error) {
if val, ok := props[RawPayloadKey]; ok && val != "" {
boolVal, err := strconv.ParseBool(val)
if err != nil {
return false, fmt.Errorf("%s value must be a valid boolean: actual is '%s'", RawPayloadKey, val)
}
return boolVal, nil
}
return false, nil
}
func TryGetContentType(props map[string]string) (string, bool) {
if val, ok := props[ContentType]; ok && val != "" {
return val, true
}
return "", false
}
func TryGetQueryIndexName(props map[string]string) (string, bool) {
if val, ok := props[QueryIndexName]; ok && val != "" {
return val, true
}
return "", false
}
// GetMetadataProperty returns a property from the metadata map, with support for aliases
func GetMetadataProperty(props map[string]string, keys ...string) (val string, ok bool) {
lcProps := make(map[string]string, len(props))
for k, v := range props {
lcProps[strings.ToLower(k)] = v
}
for _, k := range keys {
val, ok = lcProps[strings.ToLower(k)]
if ok {
return val, true
}
}
return "", false
}
// DecodeMetadata decodes metadata into a struct
// This is an extension of mitchellh/mapstructure which also supports decoding durations
func DecodeMetadata(input any, result any) error {
// avoids a common mistake of passing the metadata struct, instead of the properties map
// if input is of type struct, cast it to metadata.Base and access the Properties instead
v := reflect.ValueOf(input)
if v.Kind() == reflect.Struct {
f := v.FieldByName("Properties")
if f.IsValid() && f.Kind() == reflect.Map {
input = f.Interface().(map[string]string)
}
}
inputMap, err := cast.ToStringMapStringE(input)
if err != nil {
return fmt.Errorf("input object cannot be cast to map[string]string: %w", err)
}
// Handle aliases
err = resolveAliases(inputMap, reflect.TypeOf(result))
if err != nil {
return fmt.Errorf("failed to resolve aliases: %w", err)
}
// Finally, decode the metadata using mapstructure
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
toTimeDurationArrayHookFunc(),
toTimeDurationHookFunc(),
toTruthyBoolHookFunc(),
toStringArrayHookFunc(),
toByteSizeHookFunc(),
),
Metadata: nil,
Result: result,
WeaklyTypedInput: true,
})
if err != nil {
return err
}
err = decoder.Decode(inputMap)
return err
}
func resolveAliases(md map[string]string, t reflect.Type) error {
// Get the list of all keys in the map
keys := make(map[string]string, len(md))
for k := range md {
lk := strings.ToLower(k)
// Check if there are duplicate keys after lowercasing
_, ok := keys[lk]
if ok {
return fmt.Errorf("key %s is duplicate in the metadata", lk)
}
keys[lk] = k
}
// Error if result is not pointer to struct, or pointer to pointer to struct
if t.Kind() != reflect.Pointer {
return fmt.Errorf("not a pointer: %s", t.Kind().String())
}
t = t.Elem()
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return fmt.Errorf("not a struct: %s", t.Kind().String())
}
// Iterate through all the properties, possibly recursively
resolveAliasesInType(md, keys, t)
return nil
}
func resolveAliasesInType(md map[string]string, keys map[string]string, t reflect.Type) {
// Iterate through all the properties of the type to see if anyone has the "mapstructurealiases" property
for i := 0; i < t.NumField(); i++ {
currentField := t.Field(i)
// Ignored fields that are not exported or that don't have a "mapstructure" tag
mapstructureTag := currentField.Tag.Get("mapstructure")
if !currentField.IsExported() || mapstructureTag == "" {
continue
}
// Check if this is an embedded struct
if mapstructureTag == ",squash" {
resolveAliasesInType(md, keys, currentField.Type)
continue
}
// If the current property has a value in the metadata, then we don't need to handle aliases
_, ok := keys[strings.ToLower(mapstructureTag)]
if ok {
continue
}
// Check if there's a "mapstructurealiases" tag
aliasesTag := strings.ToLower(currentField.Tag.Get("mapstructurealiases"))
if aliasesTag == "" {
continue
}
// Look for the first alias that has a value
var mdKey string
for _, alias := range strings.Split(aliasesTag, ",") {
mdKey, ok = keys[alias]
if !ok {
continue
}
// We found an alias
md[mapstructureTag] = md[mdKey]
break
}
}
}
func toTruthyBoolHookFunc() mapstructure.DecodeHookFunc {
stringType := reflect.TypeOf("")
boolType := reflect.TypeOf(true)
boolPtrType := reflect.TypeOf(ptr.Of(true))
return func(
f reflect.Type,
t reflect.Type,
data any,
) (any, error) {
if f == stringType && t == boolType {
return utils.IsTruthy(data.(string)), nil
}
if f == stringType && t == boolPtrType {
return ptr.Of(utils.IsTruthy(data.(string))), nil
}
return data, nil
}
}
func toStringArrayHookFunc() mapstructure.DecodeHookFunc {
stringType := reflect.TypeOf("")
stringSliceType := reflect.TypeOf([]string{})
stringSlicePtrType := reflect.TypeOf(ptr.Of([]string{}))
return func(
f reflect.Type,
t reflect.Type,
data any,
) (any, error) {
if f == stringType && t == stringSliceType {
return strings.Split(data.(string), ","), nil
}
if f == stringType && t == stringSlicePtrType {
return ptr.Of(strings.Split(data.(string), ",")), nil
}
return data, nil
}
}
func toTimeDurationArrayHookFunc() mapstructure.DecodeHookFunc {
convert := func(input string) ([]time.Duration, error) {
parts := strings.Split(input, ",")
res := make([]time.Duration, 0, len(parts))
for _, v := range parts {
input := strings.TrimSpace(v)
if input == "" {
continue
}
val, err := time.ParseDuration(input)
if err != nil {
// If we can't parse the duration, try parsing it as int64 seconds
seconds, errParse := strconv.ParseInt(input, 10, 0)
if errParse != nil {
return nil, errors.Join(err, errParse)
}
val = time.Duration(seconds * int64(time.Second))
}
res = append(res, val)
}
return res, nil
}
stringType := reflect.TypeOf("")
durationSliceType := reflect.TypeOf([]time.Duration{})
durationSlicePtrType := reflect.TypeOf(ptr.Of([]time.Duration{}))
return func(
f reflect.Type,
t reflect.Type,
data any,
) (any, error) {
if f == stringType && t == durationSliceType {
inputArrayString := data.(string)
return convert(inputArrayString)
}
if f == stringType && t == durationSlicePtrType {
inputArrayString := data.(string)
res, err := convert(inputArrayString)
if err != nil {
return nil, err
}
return ptr.Of(res), nil
}
return data, nil
}
}
type ComponentType string
const (
BindingType ComponentType = "bindings"
StateStoreType ComponentType = "state"
SecretStoreType ComponentType = "secretstores"
PubSubType ComponentType = "pubsub"
LockStoreType ComponentType = "lock"
ConfigurationStoreType ComponentType = "configuration"
MiddlewareType ComponentType = "middleware"
CryptoType ComponentType = "crypto"
NameResolutionType ComponentType = "nameresolution"
WorkflowType ComponentType = "workflows"
)
// IsValid returns true if the component type is valid.
func (t ComponentType) IsValid() bool {
switch t {
case BindingType, StateStoreType,
SecretStoreType, PubSubType,
LockStoreType, ConfigurationStoreType,
MiddlewareType, CryptoType,
NameResolutionType, WorkflowType:
return true
default:
return false
}
}
// BuiltInMetadataProperties returns the built-in metadata properties for the given component type.
// These are normally parsed by the runtime.
func (t ComponentType) BuiltInMetadataProperties() []string {
switch t {
case StateStoreType:
return []string{
"actorStateStore",
"keyPrefix",
}
case LockStoreType:
return []string{
"keyPrefix",
}
default:
return nil
}
}
type MetadataField struct {
// Field type
Type string
// True if the field should be ignored by the metadata analyzer
Ignored bool
// True if the field is deprecated
Deprecated bool
// Aliases used for old, deprecated names
Aliases []string
}
type MetadataMap map[string]MetadataField
// GetMetadataInfoFromStructType converts a struct to a map of field name (or struct tag) to field type.
// This is used to generate metadata documentation for components.
func GetMetadataInfoFromStructType(t reflect.Type, metadataMap *MetadataMap, componentType ComponentType) error {
// Return if not struct or pointer to struct.
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return fmt.Errorf("not a struct: %s", t.Kind().String())
}
if *metadataMap == nil {
*metadataMap = MetadataMap{}
}
for i := 0; i < t.NumField(); i++ {
currentField := t.Field(i)
// fields that are not exported cannot be set via the mapstructure metadata decoding mechanism
if !currentField.IsExported() {
continue
}
mapStructureTag := currentField.Tag.Get("mapstructure")
// we are not exporting this field using the mapstructure tag mechanism
if mapStructureTag == "-" {
continue
}
// If there's a "mdonly" tag, that metadata option is only included for certain component types
if mdOnlyTag := currentField.Tag.Get("mdonly"); mdOnlyTag != "" {
include := false
onlyTags := strings.Split(mdOnlyTag, ",")
for _, tag := range onlyTags {
if tag == string(componentType) {
include = true
break
}
}
if !include {
continue
}
}
mdField := MetadataField{
Type: currentField.Type.String(),
}
// If there's a mdignore tag and that's truthy, the field should be ignored by the metadata analyzer
mdField.Ignored = utils.IsTruthy(currentField.Tag.Get("mdignore"))
// If there's a "mddeprecated" tag, the field may be deprecated
mdField.Deprecated = utils.IsTruthy(currentField.Tag.Get("mddeprecated"))
// If there's a "mdaliases" tag, the field contains aliases
// The value is a comma-separated string
if mdAliasesTag := currentField.Tag.Get("mdaliases"); mdAliasesTag != "" {
mdField.Aliases = strings.Split(mdAliasesTag, ",")
}
// Handle mapstructure tags and get the field name
mapStructureTags := strings.Split(mapStructureTag, ",")
numTags := len(mapStructureTags)
if numTags > 1 && mapStructureTags[numTags-1] == "squash" && currentField.Anonymous {
// traverse embedded struct
GetMetadataInfoFromStructType(currentField.Type, metadataMap, componentType)
continue
}
var fieldName string
if numTags > 0 && mapStructureTags[0] != "" {
fieldName = mapStructureTags[0]
} else {
fieldName = currentField.Name
}
// Add the field
(*metadataMap)[fieldName] = mdField
}
return nil
}