490 lines
13 KiB
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
|
|
}
|