components-contrib/metadata/utils.go

288 lines
8.2 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 (
"fmt"
"math"
"reflect"
"strconv"
"strings"
"time"
kitmd "github.com/dapr/kit/metadata"
kitstrings "github.com/dapr/kit/strings"
)
const (
// TTLMetadataKey defines the metadata key for setting a time to live (as a Go duration or number of seconds).
TTLMetadataKey = "ttl"
TTLInSecondsMetadataKey = "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) {
val, _ := kitmd.GetMetadataProperty(props, TTLMetadataKey, TTLInSecondsMetadataKey)
if val == "" {
return 0, false, nil
}
// Try to parse as duration string first
duration, err := time.ParseDuration(val)
if err != nil {
// Failed to parse Duration string.
// Let's try Integer and assume the value is in seconds
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
}
} else if duration < 0 {
duration = 0
}
return duration, true, 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)
}
//nolint:gosec
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
}
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"
ConversationType ComponentType = "conversation"
)
// 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 := range t.NumField() {
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 = kitstrings.IsTruthy(currentField.Tag.Get("mdignore"))
// If there's a "mddeprecated" tag, the field may be deprecated
mdField.Deprecated = kitstrings.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
}